From 8f679c141c25d87c6ea0d8824ab5d7af877951be Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 30 Mar 2021 17:41:12 +0200 Subject: [PATCH] Broadcast update --- customize.dist/src/less2/include/toolbar.less | 8 +- lib/decrees.js | 9 +- lib/env.js | 1 + server.js | 7 +- www/admin/app-admin.less | 12 +- www/admin/inner.js | 909 +++++++++--------- www/common/common-ui-elements.js | 48 +- www/common/notifications.js | 26 - www/common/outer/async-store.js | 27 + www/common/outer/mailbox-handlers.js | 65 +- www/common/sframe-common-mailbox.js | 4 +- www/common/toolbar.js | 43 +- 12 files changed, 600 insertions(+), 559 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 24bb09c11..3cb9da2d2 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -400,7 +400,7 @@ button { .toolbar_button; - &.cp-notifications-bell { + &.cp-notifications-bell, &.cp-maintenance-wrench { color: @cryptpad_text_col; } } @@ -506,7 +506,7 @@ } .cp-toolbar-user { height: @toolbar_line-height; - .cp-toolbar-notifications { + .cp-toolbar-notifications, .cp-toolbar-maintenance { height: @toolbar_line-height; width: @toolbar_line-height; margin-left: 0; @@ -709,7 +709,7 @@ height: 43px; } } - .cp-toolbar-link, .cp-toolbar-notifications { + .cp-toolbar-link, .cp-toolbar-notifications, .cp-toolbar-maintenance { line-height: @toolbar_top-height; width: @toolbar_top-height; height: @toolbar_top-height; @@ -717,7 +717,7 @@ box-sizing: border-box; display: inline-block; } - .cp-toolbar-notifications { + .cp-toolbar-notifications, .cp-toolbar-maintenance { text-align: center; font-size: 32px; margin-left: 10px; diff --git a/lib/decrees.js b/lib/decrees.js index 7d2a8e037..0b5e8572e 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -128,10 +128,13 @@ commands.SET_ACCOUNT_RETENTION_TIME = makeIntegerSetter('accountRetentionTime'); var args_isString = function (args) { return Array.isArray(args) && typeof(args[0]) === "string"; }; +var args_isMaintenance = function (args) { + return Array.isArray(args) && args[0] && args[0].end && args[0].start; +}; var makeBroadcastSetter = function (attr) { return function (Env, args) { - if (!args_isString(args)) { + if (!args_isString(args) && !args_isMaintenance(args)) { throw new Error('INVALID_ARGS'); } var str = args[0]; @@ -148,6 +151,10 @@ commands.SET_LAST_BROADCAST_HASH = makeBroadcastSetter('lastBroadcastHash'); // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SURVEY_URL', [url]]], console.log) commands.SET_SURVEY_URL = makeBroadcastSetter('surveyURL'); +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [{start: +Date, end: +Date}]]], console.log) +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [""]]], console.log) +commands.SET_MAINTENANCE = makeBroadcastSetter('maintenance'); + var Quota = require("./commands/quota"); var Keys = require("./keys"); var Util = require("./common-util"); diff --git a/lib/env.js b/lib/env.js index 1a26fa0f9..b879d102f 100644 --- a/lib/env.js +++ b/lib/env.js @@ -70,6 +70,7 @@ module.exports.create = function (config) { // /api/broadcast lastBroadcastHash: '', surveyURL: undefined, + maintenance: undefined, netfluxUsers: {}, diff --git a/server.js b/server.js index 74f72bdb2..7f4c1fcbf 100644 --- a/server.js +++ b/server.js @@ -279,11 +279,16 @@ var serveBroadcast = (function () { }; var template = function (host) { + var maintenance = Env.maintenance; + if (maintenance && maintenance.end && maintenance.end < (+new Date())) { + maintenance = undefined; + } return [ 'define(function(){', 'return ' + JSON.stringify({ lastBroadcastHash: Env.lastBroadcastHash, - surveyURL: Env.surveyURL + surveyURL: Env.surveyURL, + maintenance: maintenance }, null, '\t'), '});' ].join(';\n') diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index ba35e8f15..11ef0c634 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -200,10 +200,20 @@ } .cp-admin-broadcast-form { - margin-top: 30px; input.flatpickr-input { width: 307.875px !important; // same width as flatpickr calendar } + .cp-broadcast-active { + display: flex; + flex-flow: column; + align-items: start; + padding: 10px; + background-color: @cp_sidebar-left-bg; + color: @cp_sidebar-left-fg; + p { + margin: 0; + } + } .cp-broadcast-form-submit { margin-top: 30px; button { diff --git a/www/admin/inner.js b/www/admin/inner.js index aaeeee924..73f836a9f 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -73,8 +73,9 @@ define([ 'cp-admin-support-list', 'cp-admin-support-init' ], - 'broadcast': [ // Msg.admin_cat_support - 'cp-admin-broadcast-delete', + 'broadcast': [ // Msg.admin_cat_broadcast + 'cp-admin-maintenance', + 'cp-admin-survey', 'cp-admin-broadcast', ], 'performance': [ // Msg.admin_cat_performance @@ -941,546 +942,532 @@ define([ }; Messages.admin_cat_broadcast = "Broadcast"; // XXX - // Messages.admin_broadcastHint // XXX - // Messages.admin_broadcastTitle // XXX - - //Messages.admin_broadcastDeleteHint // XXX - //Messages.admin_broadcastDeleteTitle // XXX - - Messages.broadcast_new = "New message"; - Messages.broadcast_maintenance = 'maintenance';// XXX - Messages.broadcast_survey = 'survey'; // XXX - Messages.broadcast_version = 'version'; // XXX - Messages.broadcast_custom = 'custom'; // XXX - Messages.broadcast_delete = 'delete'; // XXX - Messages.broadcast_newVersionReload = 'Force a worker reload on all clients'; // XXX - Messages.broadcast_surveyURL = 'Survey URL'; - Messages.broadcast_translations = 'Translations'; - Messages.broadcast_defaultLanguage = 'Fallback to this language (optional)'; + + Messages.admin_maintenanceTitle = "Maintenance"; // XXX + Messages.admin_maintenanceHint = "Plan, remove or update a maintenance. You can only have one active maintenance at a time."; // XXX + Messages.admin_maintenanceButton = "Plan maintenance"; // XXX + Messages.admin_maintenanceCancel = "Cancel planned maintenance"; // XXX Messages.broadcast_start = 'Start time'; Messages.broadcast_end = 'End time'; + + + Messages.admin_surveyTitle = "Survey"; // XXX + Messages.admin_surveyHint = "Add, update or remove the active survey accessible from the user menu"; // XXX + Messages.admin_surveyButton = "Apply survey"; // XXX + Messages.admin_surveyCancel = "Cancel active survey"; // XXX + Messages.admin_surveyActive = "View the active survey"; // XXX + Messages.broadcast_surveyURL = 'Survey URL'; + + Messages.admin_broadcastTitle = "Broadcast a message"; // XXX + Messages.admin_broadcastHint = "Send a message to all the existing and future users as a notification"; // XXX + Messages.admin_broadcastButton = "Send"; // XXX + Messages.admin_broadcastActive = "Active message"; // XXX + Messages.admin_broadcastCancel = "Delete active message"; // XXX + Messages.broadcast_translations = 'Translations'; + Messages.broadcast_defaultLanguage = 'Fallback to this language'; Messages.broadcast_preview = "Preview in a fake notification"; - Messages.broadcast_deleteBtn = "Delete for all"; - Messages.broadcast_clear = "Clear all for everybody"; - Messages.expired = "Expired"; - Messages.broadcast_empty = "No active message"; - Messages.broadcast_noFallback = "Don't fallback to a default language"; - - var onRefreshBroadcast = Util.mkEvent(); - var broadcast = { - getData: function () { return false; }, - reset: function () {}, - handlers: {} - }; - broadcast.handlers.custom = function ($form, button, send, preview, onPreview) { - // Custom message - var container = h('div.cp-broadcast-container'); - var $container = $(container); - var languages = Messages._languages; - var keys = Object.keys(languages).sort(); - - // Always keep the textarea ordered by language code - var reorder = function () { - $container.find('.cp-broadcast-lang').each(function (i, el) { - var $el = $(el); - var l = $el.attr('data-lang'); - $el.css('order', keys.indexOf(l)); + + var getApi = function (cb) { + return function () { + require(['/api/broadcast?'+ (+new Date())], function (Broadcast) { + cb(Broadcast); }); }; + }; - var noFallbackBtn = h('button.btn.btn-secondary.cp-broadcast-preview', - Messages.broadcast_noFallback); - var $noFallbackBtn = $(noFallbackBtn); - var checkFallbackBtn = function () { - var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length; - if (hasDefault) { - $noFallbackBtn.css('visibility', ''); - } else { - $noFallbackBtn.css('visibility', 'hidden'); - } - }; + // Update the lastBroadcastHash in /api/broadcast if we can do it. + // To do so, find the last "BROADCAST_CUSTOM" in the current history and use the previous + // message's hash. + // If the last BROADCAST_CUSTOM has been deleted by an admin, we can use the most recent + // message's hash. + var checkLastBroadcastHash = function () { + var deleted = []; - // Remove a textarea - var removeLang = function (l) { - $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove(); - checkFallbackBtn(); - }; + require(['/api/broadcast?'+ (+new Date())], function (BCast) { + var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash + common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) { + if (e) { return void console.error(e); } - // Add a textarea - var addLang = function (l) { - if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; } - var preview = h('button.btn.btn-secondary', Messages.broadcast_preview); - $(preview).click(function () { - onPreview(l); - }); - var bcastDefault = Messages.broadcast_defaultLanguage; - var first = !$container.find('.cp-broadcast-lang').length; - var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, { - 'data-lang': l, - label: {class: 'noTitle'} - }); - $(radio).find('input').on('change', function () { - checkFallbackBtn(); - }); - $container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [ - h('h4', languages[l]), - h('label', Messages.kanban_body), - h('textarea'), - radio, - preview - ])); - checkFallbackBtn(); - reorder(); - }; + // No history, nothing to change + if (!Array.isArray(msgs)) { return; } + if (!msgs.length) { return; } - // Checkboxes to select translations - var boxes = keys.map(function (l) { - var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l, - languages[l], false, { label: { class: 'noTitle' } })); - var $check = $cbox.find('input').on('change', function () { - var c = $check.is(':checked'); - if (c) { return void addLang(l); } - removeLang(l); - }); - if (l === 'en') { - setTimeout(function () { - $check.click(); - }); - } - return $cbox[0]; - }); + var lastHash; + var next = false; - // Extract form data - broadcast.getData = function () { - var map = {}; - var defaultLanguage; - var error = false; - $container.find('.cp-broadcast-lang').each(function (i, el) { - var $el = $(el); - var l = $el.attr('data-lang'); - if (!l) { error = true; return; } - var text = $el.find('textarea').val(); - if (!text.trim()) { error = true; return; } - if ($el.find('.cp-checkmark input').is(':checked')) { - defaultLanguage = l; - } - map[l] = text; - }); - if (!Object.keys(map).length) { - console.error('You must select at least one language'); - return false; - } - if (error) { - console.error('One of the selected languages has no data'); - return false; - } - return { - defaultLanguage: defaultLanguage, - content: map - }; - }; - // Clear all the textarea when sent - broadcast.reset = function () { - $container.find('.cp-broadcast-lang textarea').each(function (i, el) { - $(el).val(''); - }); - }; + // Start from the most recent messages until you find a CUSTOM message and + // check if it has been deleted + msgs.reverse().some(function (data) { + var c = data.content; - // "Don't fallback to a default language" button - $noFallbackBtn.click(function () { - $container.find('.cp-checkmark input').prop('checked', false); - $noFallbackBtn.css('visibility', 'hidden'); - }); + // This is the hash we want to keep + if (next) { + if (!c || !c.hash) { return; } + lastHash = c.hash; + next = false; + return true; + } - // Make the form - $form.append([ - h('label', Messages.broadcast_translations), - h('div.cp-broadcast-languages', boxes), - container, - h('div.cp-broadcast-form-submit', [ - noFallbackBtn, - h('br'), - button - ]) - ]); - }; - broadcast.handlers.maintenance = function ($form, button, send, preview) { - // Maintenance message - - // Start and end date pickers - var start = h('input'); - var end = h('input'); - var $start = $(start); - var $end = $(end); - var endPickr = Flatpickr(end, { - enableTime: true, - minDate: new Date() - }); - Flatpickr(start, { - enableTime: true, - minDate: new Date(), - onChange: function () { - endPickr.set('minDate', new Date($start.val())); - } - }); + // initialize with the most recent hash + if (!lastHash && c && c.hash) { lastHash = c.hash; } - // Extract form data - broadcast.getData = function () { - var start = +new Date($start.val()); - var end = +new Date($end.val()); - if (isNaN(start) || isNaN(end)) { - console.error('Invalid dates'); - return false; - } - return { - start: start, - end: end - }; - }; + var msg = c && c.msg; + if (!msg) { return; } - // Clear when sent - broadcast.reset = function () { - $start.val(''); - $end.val(''); - }; - $form.append([ - h('label', Messages.broadcast_start), - start, - h('label', Messages.broadcast_end), - end, - h('br'), - h('div.cp-broadcast-form-submit', [ - button, - preview - ]) - ]); + // Remember all deleted messages + if (msg.type === "BROADCAST_DELETE") { + deleted.push(Util.find(msg, ['content', 'uid'])); + } - }; - broadcast.handlers.version = function ($form, button, send, preview) { - // New version available message - - // This checkbox can be used to trigger a fake "reconnect" event on the clients - // so that they can check api/config and reload the worker in case of a new version - var $cbox = $(UI.createCheckbox('cp-admin-version-reload', - Messages.broadcast_newVersionReload, - false, { label: { class: 'noTitle' } })); - var $checkbox = $cbox.find('input'); - - // Extract the data and make the form - broadcast.getData = function () { - return { - reload: $checkbox.is(':checked') - }; - }; - broadcast.reset = function () { - $checkbox[0].checked = false; - }; - $form.append([ - $cbox[0], - h('br'), - h('div.cp-broadcast-form-submit', [ - button, - preview - ]) - ]); - }; - broadcast.handlers.survey = function ($form, button, send, preview) { - // New survey message - // TODO send different URLs for other languages? - var label = h('label', Messages.broadcast_surveyURL); - var input = h('input'); - var $input = $(input); - broadcast.getData = function () { - var url = $input.val(); - if (!Util.isValidURL(url)) { - console.error('Invalid URL'); - return false; - } - return { - url: url - }; - }; - broadcast.reset = function () { - $input.val(''); - }; - $(button).off('click').click(function () { - var data = broadcast.getData(); - var val = $input.val() || ''; + // Only check custom messages + if (msg.type !== "BROADCAST_CUSTOM") { return; } - // Invalid url: abort - // NOTE: empty strings are allowed to remove a surveyURL from the decrees - // XXX usability... - if (!data && val) { return void UI.warn(Messages.error); } + // If the most recent CUSTOM message has been deleted, it means we don't + // need to keep any message and we can continue with lastHash as the most + // recent broadcast message. + if (deleted.indexOf(msg.uid) !== -1) { return true; } - var url = data ? data.url : val; + // We just found the oldest message we want to keep, move one iteration + // further into the loop to get the next message's hash. + // If this is the end of the loop, don't bump lastBroadcastHash at all. + next = true; + }); - sFrameChan.query('Q_ADMIN_RPC', { - cmd: 'ADMIN_DECREE', - data: ['SET_SURVEY_URL', [url]] - }, function (e) { - if (e) { - UI.warn(Messages.error); console.error(e); - return; - } - if (!url) { return; } - send(); - }); + // If we don't have to bump our lastBroadcastHash, abort + if (next) { return; } + // Otherwise, bump to lastHash + console.warn('Updating last broadcast hash to', lastHash); + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_LAST_BROADCAST_HASH', [lastHash]] + }, function (e) { + if (e) { + console.error(e); + return; + } + console.log('lastBroadcastHash updated'); + }); + }); }); - $form.append([ - label, - input, - h('br'), - h('div.cp-broadcast-form-submit', [ - button, - preview - ]) - ]); + }; - broadcast.handlers.delete = function ($form, button, send) { - require(['/api/broadcast?'+ (+new Date())], function (BCast) { - // Always display the messages from the instance "lastBroadcastHash" - var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash - common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) { - var table = h('table.cp-broadcast-delete'); - var $table = $(table); + create['broadcast'] = function () { + var key = 'broadcast'; + var $div = makeBlock(key); + + var form = h('div.cp-admin-broadcast-form'); + var $form = $(form).appendTo($div); - // Empty history - if (!msgs.length) { - $table.append(h('tr.empty', h('td', Messages.broadcast_empty))); - } + var refresh = getApi(function (Broadcast) { + var button = h('button.btn.btn-primary', Messages.admin_broadcastButton); + var $button = $(button); + var removeButton = h('button.btn.btn-danger', Messages.admin_broadcastCancel); + var active = h('div.cp-broadcast-active', h('p', Messages.admin_broadcastActive)); + var $active = $(active); + var activeUid; + var deleted = []; + + // Render active message (if there is one) + require(['/api/broadcast?'+ (+new Date())], function (BCast) { + var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash + common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) { + if (e) { return void console.error(e); } + if (!Array.isArray(msgs)) { return; } + if (!msgs.length) { + $active.hide(); + } + msgs.reverse().some(function (data) { + var c = data.content; + var msg = c && c.msg; + if (!msg) { return; } + if (msg.type === "BROADCAST_DELETE") { + deleted.push(Util.find(msg, ['content', 'uid'])); + } + if (msg.type !== "BROADCAST_CUSTOM") { return; } + if (deleted.indexOf(msg.uid) !== -1) { return true; } + + // We found an active custom message, show it + var el = common.mailbox.createElement(data); + var table = h('table.cp-broadcast-delete'); + var $table = $(table); + var uid = Util.find(data, ['content', 'msg', 'uid']); + var time = Util.find(data, ['content', 'msg', 'content', 'time']); + var tr = h('tr', { 'data-uid': uid }, [ + h('td', 'ID: '+uid), + h('td', new Date(time || 0).toLocaleString()), + h('td', el), + h('td.delete', removeButton), + ]); + $table.append(tr); + $active.append(table); + activeUid = uid; - // Build the table - msgs.forEach(function (data) { - var el = common.mailbox.createElement(data); - var t = Util.find(data, ['content', 'msg', 'type']); + return true; + }); + if (!activeUid) { $active.hide(); } + }); + }); - // A "DELETE" message is here to disable a previous line - if (t === 'BROADCAST_DELETE') { - var _uid = Util.find(data, ['content', 'msg', 'content', 'uid']); - var $button = $table.find('[data-uid="'+_uid+'"] td.delete button'); - $button.prop('disabled', 'disabled').text(Messages.deleted); - return; + // Custom message + var container = h('div.cp-broadcast-container'); + var $container = $(container); + var languages = Messages._languages; + var keys = Object.keys(languages).sort(); + + // Always keep the textarea ordered by language code + var reorder = function () { + $container.find('.cp-broadcast-lang').each(function (i, el) { + var $el = $(el); + var l = $el.attr('data-lang'); + $el.css('order', keys.indexOf(l)); + }); + }; + + // Remove a textarea + var removeLang = function (l) { + $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove(); + + var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length; + if (!hasDefault) { + $container.find('.cp-broadcast-lang').first().find('.cp-checkmark input').prop('checked', 'checked'); } + }; - // Make the line - var uid = Util.find(data, ['content', 'msg', 'uid']); - var time = Util.find(data, ['content', 'msg', 'content', 'time']); - var deleteBtn = h('button.btn.btn-danger', Messages.broadcast_deleteBtn); - var tr = h('tr', { 'data-uid': uid }, [ - h('td', 'ID: '+uid), - h('td', new Date(time || 0).toLocaleString()), - h('td', el), - h('td.delete', deleteBtn), - ]); + var getData = function () { return false; }; + var onPreview = function (l) { + var data = getData(); + if (data === false) { return void UI.warn(Messages.error); } - // Auto-expire maintenance and survey messages - if (t === 'BROADCAST_MAINTENANCE') { - var end = Util.find(data, ['content', 'msg', 'content', 'end']); - if (end < +new Date()) { - $(deleteBtn).prop('disabled', 'disabled').text(Messages.expired); + var msg = { + uid: Util.uid(), + type: 'BROADCAST_CUSTOM', + content: data + }; + common.mailbox.onMessage({ + lang: l, + type: 'broadcast', + content: { + msg: msg, + hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58) } + }, function () { + UI.log(Messages.saved); + }); + }; + + // Add a textarea + var addLang = function (l) { + if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; } + var preview = h('button.btn.btn-secondary', Messages.broadcast_preview); + $(preview).click(function () { + onPreview(l); + }); + var bcastDefault = Messages.broadcast_defaultLanguage; + var first = !$container.find('.cp-broadcast-lang').length; + var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, { + 'data-lang': l, + label: {class: 'noTitle'} + }); + $container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [ + h('h4', languages[l]), + h('label', Messages.kanban_body), + h('textarea'), + radio, + preview + ])); + reorder(); + }; + + // Checkboxes to select translations + var boxes = keys.map(function (l) { + var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l, + languages[l], false, { label: { class: 'noTitle' } })); + var $check = $cbox.find('input').on('change', function () { + var c = $check.is(':checked'); + if (c) { return void addLang(l); } + removeLang(l); + }); + if (l === 'en') { + setTimeout(function () { + $check.click(); + }); } - if (t === 'BROADCAST_VERSION') { - $(deleteBtn).prop('disabled', 'disabled').text(Messages.expired); + return $cbox[0]; + }); + + // Extract form data + getData = function () { + var map = {}; + var defaultLanguage; + var error = false; + $container.find('.cp-broadcast-lang').each(function (i, el) { + var $el = $(el); + var l = $el.attr('data-lang'); + if (!l) { error = true; return; } + var text = $el.find('textarea').val(); + if (!text.trim()) { error = true; return; } + if ($el.find('.cp-checkmark input').is(':checked')) { + defaultLanguage = l; + } + map[l] = text; + }); + if (!Object.keys(map).length) { + console.error('You must select at least one language'); + return false; + } + if (error) { + console.error('One of the selected languages has no data'); + return false; } + return { + defaultLanguage: defaultLanguage, + content: map + }; + }; - // "Delete this message" button - UI.confirmButton(deleteBtn, { - classes: 'btn-danger', - multiple: true - }, function () { - broadcast.getData = function () { - if (!uid) { return false; } - return { uid: uid }; - }; - broadcast.reset = function () { - $(deleteBtn).prop('disabled', 'disabled').text(Messages.deleted); - }; - send(); + var send = function (data) { + $button.prop('disabled', 'disabled'); + data.time = +new Date(); + common.mailbox.sendTo('BROADCAST_CUSTOM', data, {}, function (err, data) { + if (err) { + $button.prop('disabled', ''); + console.error(err); + return UI.warn(Messages.error); + } + UI.log(Messages.saved); + refresh(); + + checkLastBroadcastHash(); }); + }; - $table.append(tr); + $button.click(function () { + var data = getData(); + if (data === false) { return void UI.warn(Messages.error); } + send(data); }); - // Clear all button: remove all the messages and bump lastBroadcastHash - var clearAll = h('button.btn.btn-danger', Messages.broadcast_clear); - UI.confirmButton(clearAll, { + UI.confirmButton(removeButton, { classes: 'btn-danger', - multiple: true }, function () { - broadcast.getData = function () { - return { all: true }; - }; - broadcast.reset = function () {}; - - // Send a message to all users telling them to wipe the broadcast mailbox - // and on success, send an admin decree to update /api/broadcast - send(function (err, obj) { - if (err) { return; } - if (!obj || !obj.hash) { return; } - /* - sFrameChan.query('Q_ADMIN_RPC', { - cmd: 'ADMIN_DECREE', - data: ['SET_LAST_BROADCAST_HASH', [obj.hash]] - }, function (e) { - if (e) { - UI.warn(Messages.error); console.error(e); - return; - } - // On success, reload the "delete" tab - onRefreshBroadcast.fire(); - }); - */ + if (!activeUid) { return; } + common.mailbox.sendTo('BROADCAST_DELETE', { + uid: activeUid + }, {}, function (err, data) { + if (err) { return UI.warn(Messages.error); } + UI.log(Messages.saved); + refresh(); + checkLastBroadcastHash(); }); }); - $form.append([ - table, - msgs.length ? clearAll : undefined + // Make the form + $form.empty().append([ + active, + h('label', Messages.broadcast_translations), + h('div.cp-broadcast-languages', boxes), + container, + h('div.cp-broadcast-form-submit', [ + h('br'), + button + ]) ]); }); - }); + refresh(); + return $div; }; - var getBroadcastForm = function ($form, key) { - $form.empty(); + create['maintenance'] = function () { + var key = 'maintenance'; + var $div = makeBlock(key); - var button = h('button.btn.btn-primary', Messages.support_formButton); - var $button = $(button); + var form = h('div.cp-admin-broadcast-form'); + var $form = $(form).appendTo($div); - var send = function (_cb) { - var cb = Util.once(_cb || function () {}); - var data = broadcast.getData(); - if (data === false) { - cb('NODATA'); - return void UI.warn(Messages.error); + var refresh = getApi(function (Broadcast) { + var button = h('button.btn.btn-primary', Messages.admin_maintenanceButton); + var $button = $(button); + var removeButton = h('button.btn.btn-danger', Messages.admin_maintenanceCancel); + var active; + + if (Broadcast && Broadcast.maintenance) { + var m = Broadcast.maintenance; + if (m.start && m.end && m.end >= (+new Date())) { + active = h('div.cp-broadcast-active', [ + UI.setHTML(h('p'), Messages._getKey('broadcast_maintenance', [ + new Date(m.start).toLocaleString(), + new Date(m.end).toLocaleString(), + ])), + removeButton + ]); + } } - $button.prop('disabled', 'disabled'); - data.time = +new Date(); - common.mailbox.sendTo('BROADCAST_'+key.toUpperCase(), data, {}, function (err, data) { - $button.prop('disabled', ''); - cb(err, data); - if (err) { - console.error(err); - return UI.warn(Messages.error); + + // Start and end date pickers + var start = h('input'); + var end = h('input'); + var $start = $(start); + var $end = $(end); + var endPickr = Flatpickr(end, { + enableTime: true, + minDate: new Date() + }); + Flatpickr(start, { + enableTime: true, + minDate: new Date(), + onChange: function () { + endPickr.set('minDate', new Date($start.val())); } + }); + // Extract form data + var getData = function () { + var start = +new Date($start.val()); + var end = +new Date($end.val()); + if (isNaN(start) || isNaN(end)) { + console.error('Invalid dates'); + return false; + } + return { + start: start, + end: end + }; + }; + + var send = function (data) { + $button.prop('disabled', 'disabled'); sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', - data: ['SET_LAST_BROADCAST_HASH', [data.hash]] + data: ['SET_MAINTENANCE', [data]] }, function (e) { if (e) { UI.warn(Messages.error); console.error(e); + $button.prop('disabled', ''); return; } - // On success, reload the "delete" tab - onRefreshBroadcast.fire(); + // Maintenance applied, send notification + common.mailbox.sendTo('BROADCAST_MAINTENANCE', {}, {}, function (err, data) { + refresh(); + checkLastBroadcastHash(); + }); }); - // Only print success if there is no callback - if (!_cb) { - UI.log(Messages.saved); - // Clear the UI - broadcast.reset(); - onRefreshBroadcast.fire(); - } - }); - }; - - $button.click(function () { - send(); - }); - - var onPreview = function (l) { - var data = broadcast.getData(); - if (data === false) { return void UI.warn(Messages.error); } - var msg = { - uid: Util.uid(), - type: 'BROADCAST_'+key.toUpperCase(), - content: data }; - common.mailbox.onMessage({ - lang: l, - type: 'broadcast', - content: { - msg: msg, - hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58) - } + $button.click(function () { + var data = getData(); + if (data === false) { return void UI.warn(Messages.error); } + send(data); + }); + UI.confirmButton(removeButton, { + classes: 'btn-danger', }, function () { - UI.log(Messages.saved); + send(""); }); - }; - var preview = h('button.cp-broadcast-preview.btn.btn-secondary', Messages.broadcast_preview); - $(preview).click(function () { - onPreview(); - }); + $form.empty().append([ + active, + h('label', Messages.broadcast_start), + start, + h('label', Messages.broadcast_end), + end, + h('br'), + h('div.cp-broadcast-form-submit', [ + button + ]) + ]); + }); + refresh(); - var handler = broadcast.handlers[key]; - if (!handler) { return; } // XXX - handler($form, button, send, preview, onPreview); + return $div; }; - create['broadcast'] = function () { - var key = 'broadcast'; + create['survey'] = function () { + var key = 'survey'; var $div = makeBlock(key); var form = h('div.cp-admin-broadcast-form'); - var $select = $(h('div.cp-dropdown-container')).appendTo($div); var $form = $(form).appendTo($div); - var categories = [ - 'maintenance', - 'survey', - 'version', - 'custom', - ]; - - // The "version" message only works if the instance is using a manual /api/config - // This is a custom setup for which our team won't provide support and is NOT - // recommended unless you know exactly what you're doing. - if (!AppConfig.customApiConfig) { categories.splice(2,1); } + var refresh = getApi(function (Broadcast) { + var button = h('button.btn.btn-primary', Messages.admin_surveyButton); + var $button = $(button); + var removeButton = h('button.btn.btn-danger', Messages.admin_surveyCancel); + var active; + + if (Broadcast && Broadcast.surveyURL) { + var a = h('a', {href: Broadcast.surveyURL}, Messages.admin_surveyActive); + $(a).click(function (e) { + e.preventDefault(); + common.openUnsafeURL(Broadcast.surveyURL); + }); + active = h('div.cp-broadcast-active', [ + h('p', a), + removeButton + ]); + } - categories = categories.map(function (key) { - return { - tag: 'a', - content: h('span', Messages['broadcast_'+key]), - action: function () { - getBroadcastForm($form, key); + // Survey form + var label = h('label', Messages.broadcast_surveyURL); + var input = h('input'); + var $input = $(input); + + // Extract form data + var getData = function () { + var url = $input.val(); + if (!Util.isValidURL(url)) { + console.error('Invalid URL'); + return false; } + return url; }; - }); - var dropdownCfg = { - text: Messages.broadcast_new, - angleDown: 1, - options: categories, - container: $select, - isSelect: true, - buttonCls: 'btn btn-default' - }; - UIElements.createDropdown(dropdownCfg); - - return $div; - }; + var send = function (data) { + $button.prop('disabled', 'disabled'); + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_SURVEY_URL', [data]] + }, function (e) { + if (e) { + $button.prop('disabled', ''); + UI.warn(Messages.error); console.error(e); + return; + } + // Maintenance applied, send notification + common.mailbox.sendTo('BROADCAST_SURVEY', {}, {}, function (err, data) { + refresh(); + checkLastBroadcastHash(); + }); + }); - create['broadcast-delete'] = function () { - var key = 'broadcast-delete'; - var $div = makeBlock(key); + }; + $button.click(function () { + var data = getData(); + if (data === false) { return void UI.warn(Messages.error); } + send(data); + }); + UI.confirmButton(removeButton, { + classes: 'btn-danger', + }, function () { + send(""); + }); - var form = h('div.cp-admin-broadcast-form'); - var $form = $(form).appendTo($div); - getBroadcastForm($form, 'delete'); - onRefreshBroadcast.reg(function () { - getBroadcastForm($form, 'delete'); + $form.empty().append([ + active, + label, + input, + h('br'), + h('div.cp-broadcast-form-submit', [ + button + ]) + ]); }); + refresh(); + return $div; }; - - var onRefreshPerformance = Util.mkEvent(); create['refresh-performance'] = function () { diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index aa4e50a2e..92ecb4b4e 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1,6 +1,7 @@ define([ 'jquery', '/api/config', + '/api/broadcast', '/common/common-util.js', '/common/common-hash.js', '/common/common-language.js', @@ -17,7 +18,7 @@ define([ '/common/visible.js', 'css!/customize/fonts/cptools/style.css', -], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard, +], function ($, Config, Broadcast, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard, Messages, AppConfig, Pages, NThen, InviteInner, Visible) { var UIElements = {}; var urlArgs = Config.requireConf.urlArgs; @@ -1762,23 +1763,20 @@ define([ }); } - // XXX Admin panel overrides AppConfig // If you set "" in the admin panel, it will remove the AppConfig survey - var surveyURL = typeof(Config.surveyURL) !== "undefined" ? Config.surveyURL + var surveyURL = typeof(Broadcast.surveyURL) !== "undefined" ? Broadcast.surveyURL : AppConfig.surveyURL; - if (surveyURL) { - options.push({ - tag: 'a', - attributes: { - 'class': 'cp-toolbar-survey fa fa-graduation-cap' - }, - content: h('span', Messages.survey), - action: function () { - Common.openUnsafeURL(surveyURL); - Feedback.send('SURVEY_CLICKED'); - }, - }); - } + options.push({ + tag: 'a', + attributes: { + 'class': 'cp-toolbar-survey fa fa-graduation-cap' + }, + content: h('span', Messages.survey), + action: function () { + Common.openUnsafeURL(surveyURL); + Feedback.send('SURVEY_CLICKED'); + }, + }); options.push({ tag: 'hr' }); // Add login or logout button depending on the current status @@ -1845,6 +1843,24 @@ define([ }; var $userAdmin = UIElements.createDropdown(dropdownConfigUser); + var $survey = $userAdmin.find('.cp-toolbar-survey'); + if (!surveyURL) { $survey.hide(); } + Common.makeUniversal('broadcast', { + onEvent: function (obj) { + var cmd = obj.ev; + if (cmd !== "SURVEY") { return; } + var url = obj.data; + if (url === surveyURL) { return; } + if (url && !Util.isValidURL(url)) { return; } + surveyURL = url; + if (!url) { + $survey.hide(); + return; + } + $survey.show(); + } + }); + /* // Uncomment these lines to have a language selector in the admin menu // FIXME clicking on the inner menu hides the outer one diff --git a/www/common/notifications.js b/www/common/notifications.js index 41cfd075e..0804bacbf 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -422,32 +422,6 @@ define([ } }; - Messages.broadcast_newMaintenance = "A maintenance is planned between {0} and {1}"; // XXX - handlers['BROADCAST_MAINTENANCE'] = function (common, data) { - var content = data.content; - var msg = content.msg.content; - content.getFormatText = function () { - return Messages._getKey('broadcast_newMaintenance', [ - new Date(msg.start).toLocaleString(), - new Date(msg.end).toLocaleString(), - ]); - }; - if (!content.archived) { - content.dismissHandler = defaultDismiss(common, data); - } - }; - - Messages.broadcast_newVersion = "A new version is available. Reload the page to discover the new features!"; // XXX - handlers['BROADCAST_VERSION'] = function (common, data) { - var content = data.content; - content.getFormatText = function () { - return Messages.broadcast_newVersion; - }; - if (!content.archived) { - content.dismissHandler = defaultDismiss(common, data); - } - }; - Messages.broadcast_newCustom = "Message from the administrators"; // XXX handlers['BROADCAST_CUSTOM'] = function (common, data) { var content = data.content; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index dcf176218..3ee629b6d 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -626,6 +626,33 @@ define([ cb(JSON.parse(JSON.stringify(metadata))); }; + Store.onMaintenanceUpdate = function (uid) { + // use uid in /api/broadcast so that all connected users will use the same cached + // version on the server + require(['/api/broadcast?'+uid], function (Broadcast) { + broadcast([], 'UNIVERSAL_EVENT', { + type: 'broadcast', + data: { + ev: 'MAINTENANCE', + data: Broadcast.maintenance + } + }); + }); + }; + Store.onSurveyUpdate = function (uid) { + // use uid in /api/broadcast so that all connected users will use the same cached + // version on the server + require(['/api/broadcast?'+uid], function (Broadcast) { + broadcast([], 'UNIVERSAL_EVENT', { + type: 'broadcast', + data: { + ev: 'SURVEY', + data: Broadcast.surveyURL + } + }); + }); + }; + var makePad = function (href, roHref, title) { var now = +new Date(); return { diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 33c87617b..7751fd5d7 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -690,74 +690,47 @@ define([ var broadcasts = {}; handlers['BROADCAST_MAINTENANCE'] = function (ctx, box, data, cb) { var msg = data.msg; - var content = msg.content; - if (content.end < (+new Date())) { - // Expired maintenance: dismiss - return void cb(true); - } - var uid = msg.uid; - broadcasts[uid] = { - type: box.type, - hash: data.hash - }; - cb(false); - }; - handlers['BROADCAST_VERSION'] = function (ctx, box, data, cb) { - var msg = data.msg; - var content = msg.content; - if (!box.ready) { - // This is an old version message: dismiss - return void cb(true); - } - if (content.reload) { - // We're going to force a disconnect, dismiss - ctx.Store.newVersionReload(); - return; // This message will be removed when reloading the worker - } var uid = msg.uid; - broadcasts[uid] = { - type: box.type, - hash: data.hash - }; - cb(false); + ctx.Store.onMaintenanceUpdate(uid); + cb(true); }; + var activeSurvey; handlers['BROADCAST_SURVEY'] = function (ctx, box, data, cb) { var msg = data.msg; var uid = msg.uid; - broadcasts[uid] = { + var old = activeSurvey; + activeSurvey = { type: box.type, hash: data.hash }; - cb(false); + ctx.Store.onSurveyUpdate(uid); + cb(false, old); }; + var activeCustom handlers['BROADCAST_CUSTOM'] = function (ctx, box, data, cb) { var msg = data.msg; var uid = msg.uid; - broadcasts[uid] = { + var old = activeCustom; + activeCustom = { + uid: uid, type: box.type, hash: data.hash }; - cb(false); + cb(false, old); }; handlers['BROADCAST_DELETE'] = function (ctx, box, data, cb) { var msg = data.msg; var content = msg.content; - // If this is a "clear all" message, empty the box and update lkh - if (content.all) { - // 3rd argument of callback: clear the mailbox - return void cb(null, null, true); - } - var uid = content.uid; // uid of the message to delete - if (!broadcasts[uid]) { - // We don't have this message in memory, nothing to delete - return void cb(true); + if (activeCustom && activeCustom.uid === uid) { + // We have the message in memory, remove it and don't keep the DELETE msg + cb(true, activeCustom); + activeCustom = undefined; + return; } - - // We have the message in memory, remove it and don't keep the DELETE msg - cb(true, broadcasts[uid]); - delete broadcasts[uid]; + // We don't have this message in memory, nothing to delete + cb(true); }; return { diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js index 4ab2b6f72..58396b1e9 100644 --- a/www/common/sframe-common-mailbox.js +++ b/www/common/sframe-common-mailbox.js @@ -219,8 +219,8 @@ define([ var historyState = false; var onHistory = function () {}; - mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) { - if (historyState) { return void cb("ALREADY_CALLED"); } + mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) { + if (type !== "broadcast" && historyState) { return void cb("ALREADY_CALLED"); } historyState = true; var txid = Util.uid(); execCommand('LOAD_HISTORY', { diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 172568f44..d59c9afa6 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -2,6 +2,7 @@ define([ 'jquery', '/customize/application_config.js', '/api/config', + '/api/broadcast', '/common/common-ui-elements.js', '/common/common-interface.js', '/common/common-hash.js', @@ -11,7 +12,7 @@ define([ '/common/hyperscript.js', '/common/messenger-ui.js', '/customize/messages.js', -], function ($, Config, ApiConfig, UIElements, UI, Hash, Util, Feedback, MT, h, +], function ($, Config, ApiConfig, Broadcast, UIElements, UI, Hash, Util, Feedback, MT, h, MessengerUI, Messages) { var Common; @@ -45,6 +46,7 @@ MessengerUI, Messages) { var TITLE_CLS = Bar.constants.title = "cp-toolbar-title"; var LINK_CLS = Bar.constants.link = "cp-toolbar-link"; var NOTIFICATIONS_CLS = Bar.constants.user = 'cp-toolbar-notifications'; + var MAINTENANCE_CLS = Bar.constants.user = 'cp-toolbar-maintenance'; // User admin menu var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; @@ -78,6 +80,7 @@ MessengerUI, Messages) { 'class': USER_CLS }).appendTo($topContainer); $('', {'class': LIMIT_CLS}).hide().appendTo($userContainer); + $('', {'class': MAINTENANCE_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer); $('', {'class': NOTIFICATIONS_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer); $('', {'class': USERADMIN_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer); @@ -1026,6 +1029,42 @@ MessengerUI, Messages) { return $userAdmin; }; + Messages.broadcast_maintenance = "A maintenance is planned between {0} and {1}"; // XXX + var createMaintenance = function (toolbar, config) { + var $notif = toolbar.$top.find('.'+MAINTENANCE_CLS); + var button = h('button.cp-maintenance-wrench.fa.fa-wrench'); + $notif.append(button); + + + var m = Broadcast.maintenance; + $(button).click(function () { + if (!m || !m.start || !m.end) { return; } + UI.alert(Messages._getKey('broadcast_maintenance', [ + new Date(m.start).toLocaleString(), + new Date(m.end).toLocaleString(), + ]), null, true); + }); + + Common.makeUniversal('broadcast', { + onEvent: function (obj) { + var cmd = obj.ev; + if (cmd !== "MAINTENANCE") { return; } + var data = obj.data; + if (!data) { + return void $notif.hide(); + } + m = data; + $notif.css('display', ''); + } + }); + + if (m && m.start && m.end) { + $notif.css('display', ''); + } else { + $notif.hide(); + } + }; + var createNotifications = function (toolbar, config) { var $notif = toolbar.$top.find('.'+NOTIFICATIONS_CLS).show(); var openNotifsApp = h('div.cp-notifications-gotoapp', h('p', Messages.openNotificationsApp || "Open notifications App")); @@ -1287,6 +1326,7 @@ MessengerUI, Messages) { tb['useradmin'] = createUserAdmin; tb['unpinnedWarning'] = createUnpinnedWarning; tb['notifications'] = createNotifications; + tb['maintenance'] = createMaintenance; tb['pad'] = function () { toolbar.$file.show(); @@ -1323,6 +1363,7 @@ MessengerUI, Messages) { }; addElement(config.displayed, {}, true); + addElement(['maintenance'], {}, true); toolbar['linkToMain'] = createLinkToMain(toolbar, config);