From 056073983c2f2b3d477a1587de8ba4d4b5eb5359 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 15 Mar 2024 16:09:23 +0100 Subject: [PATCH 1/4] Allow admin to upload a new logo for the instance --- customize.dist/pages/index.js | 2 +- customize.dist/pre-loading.js | 2 +- .../src/less2/include/sidebar-layout.less | 5 ++ lib/commands/admin-rpc.js | 51 ++++++++++++++++++- lib/decrees.js | 9 ++++ lib/http-worker.js | 12 +++++ www/admin/inner.js | 51 +++++++++++++++++++ 7 files changed, 129 insertions(+), 3 deletions(-) diff --git a/customize.dist/pages/index.js b/customize.dist/pages/index.js index c65ecc93c..a3f03f96f 100644 --- a/customize.dist/pages/index.js +++ b/customize.dist/pages/index.js @@ -176,7 +176,7 @@ define([ h('div.row.cp-home-hero', [ h('div.cp-title.col-lg-6', [ h('img', { - src: '/customize/CryptPad_logo_hero.svg?' + urlArgs, + src: '/api/logo?' + urlArgs, 'aria-hidden': 'true', alt: '' }), diff --git a/customize.dist/pre-loading.js b/customize.dist/pre-loading.js index a5015bae1..8b737922b 100644 --- a/customize.dist/pre-loading.js +++ b/customize.dist/pre-loading.js @@ -5,7 +5,7 @@ (function () { var logoPath = '/customize/CryptPad_logo.svg'; if (location.pathname === '/' || location.pathname === '/index.html') { - logoPath = '/customize/CryptPad_logo_hero.svg'; + logoPath = '/api/logo'; } var elem = document.createElement('div'); diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index a99286b62..398170ba7 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -121,6 +121,11 @@ height: 40px; box-sizing: border-box; } + [type="file"] { // XXX hack, to fix with sidebar layout refactoring + height: auto; + box-sizing: border-box; + padding: 6.5px; + } .cp-sidebarlayout-input-block { display: inline-flex; width: @sidebar_button-width; diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index c491eb0de..1d149bb66 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -17,8 +17,9 @@ const BlockStore = require("../storage/block"); const MFA = require("../storage/mfa"); const ArchiveAccount = require('../archive-account'); const { Worker } = require('node:worker_threads'); +const Fse = require("fs-extra"); -var Fs = require("fs"); +const Fs = require("fs"); var Admin = module.exports; @@ -915,6 +916,52 @@ var deleteInvitation = (Env, Server, cb, data) => { Invitation.delete(Env, id, cb); }; +const MAX_LOGO_SIZE = 200*1024; // 200KB +var uploadLogo = (Env, Server, cb, data, unsafeKey) => { + const args = Array.isArray(data) && data[1]; + if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } + let dataURL = args.dataURL; + + // (size*4/3) + 24 ==> base64 and dataURL overhead + if (!dataURL || dataURL.length > ((MAX_LOGO_SIZE*4/3)+24)) { + return void cb('E_TOO_LARGE'); + } + + let s = dataURL.split(','); + let base64 = s[1]; + let mime = s[0].slice(s[0].indexOf(":")+1, s[0].indexOf(";")); + if (!base64 || !mime) { return void cb('EINVAL'); } + let buf; + try { + buf = Buffer.from(base64, 'base64'); + } catch (e) { + return void cb(e); + } + + nThen(waitFor => { + Fse.mkdirp('customize', {}, waitFor((err) => { + if (!err) { return; } + waitFor.abort(); + return void cb(err); + })); + }).nThen(waitFor => { + Fse.writeFile('./customize/CryptPad_logo_hero.svg', buf, waitFor((err) => { + if (!err) { return; } + waitFor.abort(); + return void cb(err); + })); + }).nThen(() => { + adminDecree(Env, null, function (err) { + if (err) { return void cb(err); } + Env.flushCache(); + cb(void 0, true); + }, ['UPLOAD_LOGO', [ + 'SET_LOGO_MIME', + [mime] + ]], unsafeKey); + }); +}; + var commands = { ACTIVE_SESSIONS: getActiveSessions, ACTIVE_PADS: getActiveChannelCount, @@ -982,6 +1029,8 @@ var commands = { ADD_KNOWN_USER: addKnownUser, DELETE_KNOWN_USER: deleteKnownUser, UPDATE_KNOWN_USER: updateKnownUser, + + UPLOAD_LOGO: uploadLogo, }; // addFirstAdmin is an anon_rpc command diff --git a/lib/decrees.js b/lib/decrees.js index 411b959d2..e8be29696 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -158,10 +158,16 @@ var makeGenericSetter = function (attr, validator) { }; }; +var isString = (str) => { + return str && typeof(str) === "string"; +}; var isInteger = function (n) { return !(typeof(n) !== 'number' || isNaN(n) || (n % 1) !== 0); }; +var args_isString = function (args) { + return !(!Array.isArray(args) || !isString(args[0])); +}; var args_isInteger = function (args) { return !(!Array.isArray(args) || !isInteger(args[0])); }; @@ -174,6 +180,9 @@ var arg_isPositiveInteger = function (args) { return Array.isArray(args) && isInteger(args[0]) && args[0] > 0; }; +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['LOGO_MIME', ['image/png']]], console.log) +commands.SET_LOGO_MIME = makeGenericSetter('logoMimeType', args_isString); + // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENABLE_PROFILING', [true]]], console.log) commands.ENABLE_PROFILING = makeBooleanSetter('enableProfiling'); diff --git a/lib/http-worker.js b/lib/http-worker.js index 2ddf19849..bcd3a7e98 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -506,6 +506,7 @@ app.use("/block", (req, res, next) => { next(); }); + app.use("/customize", Express.static('customize')); app.use("/customize", Express.static('customize.dist')); app.use("/customize.dist", Express.static('customize.dist')); @@ -682,6 +683,17 @@ app.get('/api/profiling', function (req, res) { }); }); +app.get('/api/logo', function (req, res) { + let path = Path.resolve('./customize/CryptPad_logo.svg'); + let base = Path.resolve('./customize.dist/CryptPad_logo.svg'); + Fs.exists(path, function (exists) { + let mime = Env.logoMimeType || 'image/svg+xml'; + res.setHeader('Content-Type', mime + '; charset=utf-8'); + if (exists) { return Fs.createReadStream(path).pipe(res); } + Fs.createReadStream(base).pipe(res); + }); +}); + // This endpoint handles authenticated RPCs over HTTP // via an interactive challenge-response protocol app.use(Express.json()); diff --git a/www/admin/inner.js b/www/admin/inner.js index 7ad80c1c5..8b827e355 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -72,6 +72,9 @@ define([ 'cp-admin-jurisdiction', 'cp-admin-notice', ], + 'customize': [ + 'cp-admin-logo' + ], 'users': [ // Msg.admin_cat_quota 'cp-admin-registration', 'cp-admin-invitation', @@ -3895,6 +3898,54 @@ Example return $div; }; + Messages.admin_logoTitle = "Upload Logo"; + Messages.admin_logoHint = "Max 200KB, svg, png or jpg"; + Messages.admin_logoButton = "Upload"; + create['logo'] = function () { + var key = 'logo'; + var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle + + let $button = $div.find('button'); + + var input = h('input', { + type: 'file', + accept: 'image/*', + 'aria-labelledby': 'cp-admin-logo' + }); + $(h('div', input)).insertBefore($button); + + var spinner = UI.makeSpinner($div); + + Util.onClickEnter($button, function () { + let files = input.files; + if (files.length !== 1) { + UI.warn(Messages.error); + return; + } + spinner.spin(); + $button.attr('disabled', 'disabled'); + let reader = new FileReader(); + reader.onloadend = function () { + let dataURL = this.result; + sframeCommand('UPLOAD_LOGO', {dataURL}, (err, response) => { + $button.removeAttr('disabled'); + if (err) { + UI.warn(Messages.error); + $input.val(''); + console.error(err, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + }); + }; + reader.readAsDataURL(files[0]); + }); + + return $div; + }; + var hideCategories = function () { APP.$rightside.find('> div').hide(); }; From 21025c317fac443b1259b72917611ee1994a2a0a Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 15 Mar 2024 16:38:06 +0100 Subject: [PATCH 2/4] Upload logo: fix issue and add button to restore default logo --- lib/commands/admin-rpc.js | 6 +++++ lib/http-worker.js | 15 +++++++----- www/admin/inner.js | 49 ++++++++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index 1d149bb66..aef0fd564 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -917,6 +917,11 @@ var deleteInvitation = (Env, Server, cb, data) => { }; const MAX_LOGO_SIZE = 200*1024; // 200KB +var removeLogo = (Env, Server, cb) => { + Fse.unlink('./customize/CryptPad_logo_hero.svg', (err) => { + cb(err); + }); +}; var uploadLogo = (Env, Server, cb, data, unsafeKey) => { const args = Array.isArray(data) && data[1]; if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } @@ -1031,6 +1036,7 @@ var commands = { UPDATE_KNOWN_USER: updateKnownUser, UPLOAD_LOGO: uploadLogo, + REMOVE_LOGO: removeLogo, }; // addFirstAdmin is an anon_rpc command diff --git a/lib/http-worker.js b/lib/http-worker.js index bcd3a7e98..f734b4917 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -684,13 +684,16 @@ app.get('/api/profiling', function (req, res) { }); app.get('/api/logo', function (req, res) { - let path = Path.resolve('./customize/CryptPad_logo.svg'); - let base = Path.resolve('./customize.dist/CryptPad_logo.svg'); + let path = Path.resolve('./customize/CryptPad_logo_hero.svg'); + let base = Path.resolve('./customize.dist/CryptPad_logo_hero.svg'); Fs.exists(path, function (exists) { - let mime = Env.logoMimeType || 'image/svg+xml'; - res.setHeader('Content-Type', mime + '; charset=utf-8'); - if (exists) { return Fs.createReadStream(path).pipe(res); } - Fs.createReadStream(base).pipe(res); + res.setHeader('Content-Disposition', 'inline'); + if (exists) { + let mime = Env.logoMimeType || 'image/svg+xml'; + res.setHeader('Content-Type', mime); + return res.sendFile(path); + } + res.sendFile(base); }); }); diff --git a/www/admin/inner.js b/www/admin/inner.js index 8b827e355..e0463bc9c 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -3898,22 +3898,39 @@ Example return $div; }; + // XXX Messages.admin_logoTitle = "Upload Logo"; Messages.admin_logoHint = "Max 200KB, svg, png or jpg"; - Messages.admin_logoButton = "Upload"; + Messages.admin_logoButton = "Upload new"; + Messages.admin_logoRemoveButton = "Restore default"; create['logo'] = function () { var key = 'logo'; - var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle - - let $button = $div.find('button'); + var $div = makeBlock(key, false); // Msg.admin_emailHint, Msg.admin_emailTitle var input = h('input', { type: 'file', accept: 'image/*', 'aria-labelledby': 'cp-admin-logo' }); - $(h('div', input)).insertBefore($button); + var currentContainer = h('div'); + let redraw = () => { + var current = h('img', {src: '/api/logo?'+(+new Date())}); + $(currentContainer).empty().append(current); + }; + redraw(); + + var upload = h('button.btn.btn-primary', Messages.admin_logoButton); + var remove = h('button.btn.btn-danger', Messages.admin_logoRemoveButton); + + $div.append([ + currentContainer, + h('div', input), + h('nav', [upload, remove]) + ]); + + let $button = $(upload); + let $remove = $(remove); var spinner = UI.makeSpinner($div); Util.onClickEnter($button, function () { @@ -3931,17 +3948,37 @@ Example $button.removeAttr('disabled'); if (err) { UI.warn(Messages.error); - $input.val(''); + $(input).val(''); console.error(err, response); spinner.hide(); return; } + redraw(); spinner.done(); UI.log(Messages.saved); }); }; reader.readAsDataURL(files[0]); }); + UI.confirmButton($remove, { + classes: 'btn-danger', + multiple: true + }, function () { + spinner.spin(); + $remove.attr('disabled', 'disabled'); + sframeCommand('REMOVE_LOGO', {}, (err, response) => { + $remove.removeAttr('disabled'); + if (err) { + UI.warn(Messages.error); + console.error(err, response); + spinner.hide(); + return; + } + redraw(); + spinner.done(); + UI.log(Messages.saved); + }); + }); return $div; }; From f147ad49f3c25342c717ac4fa6b03a6acb36f945 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 19 Mar 2024 15:49:33 +0100 Subject: [PATCH 3/4] Add admin customize section to refactored admin page --- www/newadmin/inner.js | 93 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/www/newadmin/inner.js b/www/newadmin/inner.js index 53e0be9a0..2d4b5cecb 100644 --- a/www/newadmin/inner.js +++ b/www/newadmin/inner.js @@ -70,6 +70,13 @@ define([ 'notice', ] }, + 'customize': { + icon: 'fa fa-paint-brush', + content: [ + 'logo', + 'color' + ] + }, 'users' : { icon : 'fa fa-address-card-o', content : [ @@ -747,6 +754,92 @@ define([ cb(form); }); + // XXX + Messages.admin_cat_customize = "Customize"; + Messages.admin_logoTitle = "Upload Logo"; + Messages.admin_logoHint = "Max 200KB, svg, png or jpg"; + Messages.admin_logoButton = "Upload new"; + Messages.admin_logoRemoveButton = "Restore default"; + sidebar.addItem('logo', (cb) => { + // Msg.admin_emailHint, Msg.admin_emailTitle + + let input = blocks.input({ + type: 'file', + accept: 'image/*', + 'aria-labelledby': 'cp-admin-logo' + }); + + var currentContainer = blocks.block(); + let redraw = () => { + var current = h('img', {src: '/api/logo?'+(+new Date())}); + $(currentContainer).empty().append(current); + }; + redraw(); + + var upload = blocks.button('primary', '', Messages.admin_logoButton); + var remove = blocks.button('danger', '', Messages.admin_logoRemoveButton); + + let spinnerBlock = blocks.inline(); + let spinner = UI.makeSpinner($(spinnerBlock)); + let form = blocks.form([ + currentContainer, + blocks.block(input), + blocks.nav([upload, remove, spinnerBlock]) + ]); + + let $button = $(upload); + let $remove = $(remove); + + Util.onClickEnter($button, function () { + let files = input.files; + if (files.length !== 1) { + UI.warn(Messages.error); + return; + } + spinner.spin(); + $button.attr('disabled', 'disabled'); + let reader = new FileReader(); + reader.onloadend = function () { + let dataURL = this.result; + sframeCommand('UPLOAD_LOGO', {dataURL}, (err, response) => { + $button.removeAttr('disabled'); + if (err) { + UI.warn(Messages.error); + $(input).val(''); + console.error(err, response); + spinner.hide(); + return; + } + redraw(); + spinner.done(); + UI.log(Messages.saved); + }); + }; + reader.readAsDataURL(files[0]); + }); + UI.confirmButton($remove, { + classes: 'btn-danger', + multiple: true + }, function () { + spinner.spin(); + $remove.attr('disabled', 'disabled'); + sframeCommand('REMOVE_LOGO', {}, (err, response) => { + $remove.removeAttr('disabled'); + if (err) { + UI.warn(Messages.error); + console.error(err, response); + spinner.hide(); + return; + } + redraw(); + spinner.done(); + UI.log(Messages.saved); + }); + }); + + cb(form); + }); + sidebar.addItem('registration', function(cb){ var refresh = function () {}; From 0dcce94ef0cd7cab007004aa3cd7fb7a2a00a04f Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 19 Mar 2024 18:16:01 +0100 Subject: [PATCH 4/4] Add color customization and add updated logo to loading screen --- customize.dist/loading.js | 2 +- .../src/less2/include/colortheme-dark.less | 20 ++--- .../src/less2/include/colortheme.less | 20 ++--- customize.dist/src/less2/include/loading.less | 3 +- .../src/less2/include/sidebar-layout.less | 4 + .../src/less2/pages/page-index.less | 3 + customize.dist/src/pre-loading.css | 2 +- lib/commands/admin-rpc.js | 16 ++++ lib/decrees.js | 7 +- lib/http-worker.js | 1 + www/common/LessLoader.js | 7 +- www/common/inner/sidebar-layout.js | 6 +- www/newadmin/app-admin.less | 7 ++ www/newadmin/inner.js | 89 ++++++++++++++++++- 14 files changed, 158 insertions(+), 29 deletions(-) diff --git a/customize.dist/loading.js b/customize.dist/loading.js index ce11c5b76..917439dcc 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -15,7 +15,7 @@ define([ elem.innerHTML = [ '', '
', '
', diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 488bc6444..07117b0f9 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -9,9 +9,16 @@ @colortheme_app-font-size-small: 13px; @colortheme_app-font: @colortheme_app-font-size @colortheme_font; +// Colors +@cryptpad_color_brand: #0087FF; +@cryptpad_color_brand_300: lighten(@cryptpad_color_brand, 30%); +@cryptpad_color_brand_fade: fade(@cryptpad_color_brand, 75%); +@cryptpad_color_brand_fader: fade(@cryptpad_color_brand, 50%); +@cryptpad_color_brand_fadest: fade(@cryptpad_color_brand, 25%); + @colortheme_apps: { - default: #0087FF; - drive: #0087FF; // Used as icon color in index.js (index.html) + default: @cryptpad_color_brand; + drive: @cryptpad_color_brand; // Used as icon color in index.js (index.html) pad: #256ad5; code: #EAA000; slide: #e57614; @@ -27,18 +34,11 @@ } @colortheme_static_apps: { - default: #0087FF; + default: @cryptpad_color_brand; teams: #4A3BBD; contacts: #607B8D; } -// Colors -@cryptpad_color_brand: #0087FF; -@cryptpad_color_brand_300: lighten(@cryptpad_color_brand, 30%); -@cryptpad_color_brand_fade: fade(@cryptpad_color_brand, 75%); -@cryptpad_color_brand_fader: fade(@cryptpad_color_brand, 50%); -@cryptpad_color_brand_fadest: fade(@cryptpad_color_brand, 25%); - @cryptpad_color_white: #FFF; @cryptpad_color_grey_50: #FAFAFA; @cryptpad_color_grey_100: #F5F5F5; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 87a8cbb59..faaca5285 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -9,9 +9,16 @@ @colortheme_app-font-size-small: 13px; @colortheme_app-font: @colortheme_app-font-size @colortheme_font; +// Colors +@cryptpad_color_brand: #0087FF; +@cryptpad_color_brand_300: lighten(@cryptpad_color_brand, 30%); +@cryptpad_color_brand_fade: fade(@cryptpad_color_brand, 75%); +@cryptpad_color_brand_fader: fade(@cryptpad_color_brand, 50%); +@cryptpad_color_brand_fadest: fade(@cryptpad_color_brand, 25%); + @colortheme_apps: { - default: #0087FF; - drive: #0087FF; // Used as icon color in index.js (index.html) + default: @cryptpad_color_brand; + drive: @cryptpad_color_brand; // Used as icon color in index.js (index.html) pad: #256ad5; code: #EAA000; slide: #e57614; @@ -27,18 +34,11 @@ } @colortheme_static_apps: { - default: #0087FF; + default: @cryptpad_color_brand; teams: #4A3BBD; contacts: #607B8D; } -// Colors -@cryptpad_color_brand: #0087FF; -@cryptpad_color_brand_300: lighten(@cryptpad_color_brand, 30%); -@cryptpad_color_brand_fade: fade(@cryptpad_color_brand, 75%); -@cryptpad_color_brand_fader: fade(@cryptpad_color_brand, 50%); -@cryptpad_color_brand_fadest: fade(@cryptpad_color_brand, 25%); - @cryptpad_color_white: #FFF; @cryptpad_color_grey_50: #FAFAFA; @cryptpad_color_grey_100: #F5F5F5; diff --git a/customize.dist/src/less2/include/loading.less b/customize.dist/src/less2/include/loading.less index c58f9583a..dd89dff2b 100644 --- a/customize.dist/src/less2/include/loading.less +++ b/customize.dist/src/less2/include/loading.less @@ -75,7 +75,8 @@ margin-left: auto; margin-right: auto; max-width: 90vw; - max-height: 300px; + max-height: ~"max(40vh, 250px)"; + //max-height: 300px; width: auto; height: auto; margin-bottom: 2em; diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index 192c0ccb0..e776e32fb 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -151,6 +151,10 @@ flex-flow: column; align-items: baseline; } + a { + color: @cryptpad_color_link; + text-decoration: underline; + } table { .cp-strong { font-weight: bold; diff --git a/customize.dist/src/less2/pages/page-index.less b/customize.dist/src/less2/pages/page-index.less index 5c333fcba..7873f5f55 100644 --- a/customize.dist/src/less2/pages/page-index.less +++ b/customize.dist/src/less2/pages/page-index.less @@ -61,6 +61,9 @@ @media screen and (max-width: 990px) { justify-content: center; } + img { + max-height: ~"max(40vh, 250px)"; + } } .cp-title { display: flex; diff --git a/customize.dist/src/pre-loading.css b/customize.dist/src/pre-loading.css index e33a6c29a..c7ea7856b 100644 --- a/customize.dist/src/pre-loading.css +++ b/customize.dist/src/pre-loading.css @@ -72,7 +72,7 @@ html:not(.cp-app-noscroll) #placeholder.dark-theme { margin-left: auto; margin-right: auto; max-width: 90vw; - max-height: 300px; + max-height: max(40vh, 250px); width: auto; height: auto; margin-bottom: 2em; diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index aef0fd564..11cccd5fc 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -967,6 +967,21 @@ var uploadLogo = (Env, Server, cb, data, unsafeKey) => { }); }; +var changeColor = (Env, Server, cb, data, unsafeKey) => { + const args = Array.isArray(data) && data[1]; + if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } + let color = args.color; + adminDecree(Env, null, function (err) { + if (err) { return void cb(err); } + Env.flushCache(); + cb(void 0, true); + }, ['CHANGE_COLOR', [ + 'SET_ACCENT_COLOR', + [color] + ]], unsafeKey); +}; + + var commands = { ACTIVE_SESSIONS: getActiveSessions, ACTIVE_PADS: getActiveChannelCount, @@ -1037,6 +1052,7 @@ var commands = { UPLOAD_LOGO: uploadLogo, REMOVE_LOGO: removeLogo, + CHANGE_COLOR: changeColor, }; // addFirstAdmin is an anon_rpc command diff --git a/lib/decrees.js b/lib/decrees.js index e8be29696..7549f2c5b 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -159,7 +159,7 @@ var makeGenericSetter = function (attr, validator) { }; var isString = (str) => { - return str && typeof(str) === "string"; + return typeof(str) === "string"; }; var isInteger = function (n) { return !(typeof(n) !== 'number' || isNaN(n) || (n % 1) !== 0); @@ -180,9 +180,12 @@ var arg_isPositiveInteger = function (args) { return Array.isArray(args) && isInteger(args[0]) && args[0] > 0; }; -// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['LOGO_MIME', ['image/png']]], console.log) +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_LOGO_MIME', ['image/png']]], console.log) commands.SET_LOGO_MIME = makeGenericSetter('logoMimeType', args_isString); +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_ACCENT_COLOR', ['#ff0073']]], console.log) +commands.SET_ACCENT_COLOR = makeGenericSetter('accentColor', args_isString); + // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENABLE_PROFILING', [true]]], console.log) commands.ENABLE_PROFILING = makeBooleanSetter('enableProfiling'); diff --git a/lib/http-worker.js b/lib/http-worker.js index d5d8d3b03..7727e7d98 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -622,6 +622,7 @@ var Define = function (obj) { app.get('/api/instance', function (req, res) { res.setHeader('Content-Type', 'text/javascript'); res.send(Define({ + color: Env.accentColor, name: Env.instanceName, description: Env.instanceDescription, location: Env.instanceJurisdiction, diff --git a/www/common/LessLoader.js b/www/common/LessLoader.js index bf22d6126..8fac17fc1 100644 --- a/www/common/LessLoader.js +++ b/www/common/LessLoader.js @@ -9,8 +9,9 @@ const require = define; */ define([ '/api/config', + '/api/instance', '/components/nthen/index.js' -], function (Config, nThen) { /*::});module.exports = (function() { +], function (Config, Instance, nThen) { /*::});module.exports = (function() { const Config = (undefined:any); const nThen = (undefined:any); */ @@ -153,6 +154,10 @@ define([ ].join('\n'); text += '\n'+custom; } + if (Instance && Instance.color) { + let pattern = /_color_brand: ([0-9a-zA-Z#]+);/gm; + text = text.replace(pattern, `_color_brand: ${Instance.color};`); + } } cached.res = [ text, lastModified ]; var queue = cached.queue; diff --git a/www/common/inner/sidebar-layout.js b/www/common/inner/sidebar-layout.js index 5ae42940b..9c0fae8ad 100644 --- a/www/common/inner/sidebar-layout.js +++ b/www/common/inner/sidebar-layout.js @@ -61,8 +61,10 @@ define([ blocks.code = val => { return h('code', val); }; - blocks.inline = (value) => { - return h('span', value); + blocks.inline = (value, className) => { + let attr = {}; + if (className) { attr.class = className; } + return h('span', attr, value); }; blocks.block = (content, className) => { return h('div', { class: className }, content); diff --git a/www/newadmin/app-admin.less b/www/newadmin/app-admin.less index 242f368a2..2fcf1499f 100644 --- a/www/newadmin/app-admin.less +++ b/www/newadmin/app-admin.less @@ -19,5 +19,12 @@ display: flex; flex-flow: column; font: @colortheme_app-font; + + .cp-admin-color-current { + width: 30px; + height: 30px; + border-radius: 5px; + background: @cryptpad_color_brand; + } } diff --git a/www/newadmin/inner.js b/www/newadmin/inner.js index 2d4b5cecb..3e4bc2611 100644 --- a/www/newadmin/inner.js +++ b/www/newadmin/inner.js @@ -19,6 +19,7 @@ define([ 'json.sortify', '/customize/application_config.js', '/api/config', + '/api/instance', '/lib/datepicker/flatpickr.js', '/common/hyperscript.js', 'css!/lib/datepicker/flatpickr.min.css', @@ -42,6 +43,7 @@ define([ Sortify, AppConfig, ApiConfig, + Instance, Flatpickr ) { var APP = window.APP = {}; @@ -760,8 +762,15 @@ define([ Messages.admin_logoHint = "Max 200KB, svg, png or jpg"; Messages.admin_logoButton = "Upload new"; Messages.admin_logoRemoveButton = "Restore default"; + Messages.admin_colorTitle = "Main color"; + Messages.admin_colorHint = "Change the main color of your CryptPad instance. Please pick a color with good contrast with the rest of CryptPad."; + Messages.admin_colorCurrent = "Current main color"; + Messages.admin_colorChange = "Change color"; + Messages.admin_colorPick = "Pick a color"; + Messages.admin_colorPreview = "Preview color"; + sidebar.addItem('logo', (cb) => { - // Msg.admin_emailHint, Msg.admin_emailTitle + // Msg.admin_logoHint, Msg.admin_logoTitle let input = blocks.input({ type: 'file', @@ -840,6 +849,84 @@ define([ cb(form); }); + sidebar.addItem('color', cb => { + let input = blocks.input({ + type: 'color', + value: (Instance && Instance.color) || '#0087FF' + }); + let label = blocks.labelledInput(Messages.admin_colorPick, input); + let current = blocks.block([], 'cp-admin-color-current'); + let labelCurrent = blocks.labelledInput(Messages.admin_colorCurrent, current); + let preview = blocks.block([ + blocks.block([ + blocks.link('CryptPad', '/admin/#customize') + ]), + blocks.nav([ + blocks.button('primary', 'fa-floppy-o', Messages.settings_save), + blocks.button('secondary', 'fa-floppy-o', Messages.settings_save), + ]) + ], 'cp-admin-color-preview'); + let labelPreview = blocks.labelledInput(Messages.admin_colorPreview, preview); + let $preview = $(preview); + + let remove = blocks.button('danger', '', Messages.admin_logoRemoveButton); + let $remove = $(remove); + + let setColor = (color, done) => { + sframeCommand('CHANGE_COLOR', {color}, (err, response) => { + if (err) { + UI.warn(Messages.error); + console.error(err, response); + done(false); + return; + } + done(true); + flushCacheNotice(); + UI.log(Messages.saved); + }); + }; + + let btn = blocks.activeButton('primary', '', + Messages.admin_colorChange, (done) => { + let color = $input.val(); + setColor(color, done); + }); + + let $input = $(input).on('change', () => { + require(['/lib/less.min.js'], (Less) => { + let color = $input.val(); + let lColor = Less.color(color.slice(1)); + let lighten = Less.functions.functionRegistry._data.lighten; + let lightColor = lighten(lColor, {value:30}).toRGB(); + $preview.find('.btn-primary').css({ + 'background-color': color + }); + $preview.find('.btn-secondary').css({ + 'border-color': lightColor, + 'color': lightColor, + }); + $preview.find('a').css({ + 'color': lightColor, + }); + }); + }); + + UI.confirmButton($remove, { + classes: 'btn-danger', + multiple: true + }, function () { + $remove.attr('disabled', 'disabled'); + setColor('', () => {}); + }); + + let form = blocks.form([ + labelCurrent, + label + ], blocks.nav([btn, remove, btn.spinner])); + + cb([form, labelPreview]); + }); + sidebar.addItem('registration', function(cb){ var refresh = function () {};