diff --git a/www/admin/inner.js b/www/admin/inner.js index d5e2ec569..166f7a36c 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -2621,18 +2621,18 @@ Example }; // XXX - Messages.admin_supportNewHint = "Create or update the current support keys."; - Messages.admin_supportNewTitle = "Initialize support"; - Messages.admin_supportNewEnabled = "Modern support system is enabled."; - Messages.admin_supportNewDisabled = "Modern support system is disabled."; - Messages.admin_supportNewInit = "Initialize support page on this instance"; - Messages.admin_supportNewDelete = "Disable support"; - Messages.admin_supportNewConfirm = "Are you sure? This will remove access to all current moderators and delete all existing tickets."; + Messages.admin_supportSetupHint = "Create or update the current support keys."; + Messages.admin_supportSetupTitle = "Initialize support"; + Messages.admin_supportEnabled = "Modern support system is enabled."; + Messages.admin_supportDisabled = "Modern support system is disabled."; + Messages.admin_supportInit = "Initialize support page on this instance"; + Messages.admin_supportDelete = "Disable support"; + Messages.admin_supportConfirm = "Are you sure? This will remove access to all current moderators and delete all existing tickets."; Messages.admin_supportMembers = "Current support team"; Messages.admin_supportAdd = "Add a contact to the support team"; Messages.admin_supportRotateNotify = "Warning: new keys have been generated but an unenexpected error prevented the system to send them to the moderators. You may have to remove and re-add all the other moderators"; - create['support-new'] = function () { - const $block = makeBlock('support-new'); // Msg.admin_supportNewHint, .admin_supportNewTitle + create['support-setup'] = function () { + const $block = makeBlock('support-setup'); // Msg.admin_supportNewHint, .admin_supportNewTitle const $div = $(h('div')).appendTo($block); let supportKey = ApiConfig.supportMailboxKey; let edPublic = common.getMetadataMgr().getPrivateData().edPublic; // My edPublic @@ -2642,9 +2642,9 @@ Example const state = h('div'); const $state = $(state).appendTo($div); - const button = h('button.btn.btn-primary', Messages.admin_supportNewInit); + const button = h('button.btn.btn-primary', Messages.admin_supportInit); const $button = $(button).appendTo($div); - const delButton = h('button.btn.btn-danger', Messages.admin_supportNewDelete); + const delButton = h('button.btn.btn-danger', Messages.admin_supportDelete); const $delButton = $(delButton).appendTo($div).hide(); const spinner = UI.makeSpinner($div); @@ -2655,24 +2655,19 @@ Example $delButton.show(); return $state.append([ h('i.fa.fa-check'), - h('span', Messages.admin_supportNewEnabled) + h('span', Messages.admin_supportEnabled) ]); } $button.show(); $state.append([ h('i.fa.fa-times'), - h('span', Messages.admin_supportNewDisabled) + h('span', Messages.admin_supportDisabled) ]); }; setState(); - - // XXX TODO add/remove access - // XXX when removing access, send new private keys to everybody who should keep access AND move chainpad doc to the new one - // ==> we'll need to delete the old chainpad doc using admin commands - Util.onClickEnter($delButton, function () { - UI.confirm(Messages.admin_supportNewConfirm, function (yes) { + UI.confirm(Messages.admin_supportConfirm, function (yes) { if (!yes) { return; } // Send the decree, don't delete data locally, we just want to remove // the support UI for the clients diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 5e396fb84..d6642ecad 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -215,6 +215,7 @@ define(function() { teams: 'fa-users', admin: 'fa-gears', settings: 'fa-gear', + moderation: 'fa-comments-o', profile: 'fa-user-circle', support: 'fa-life-ring', accounts: 'fa-ticket' diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index c21821a30..a020a905b 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2037,7 +2037,7 @@ define([ options.push({ tag: 'hr' }); // Add administration panel link if the user is an admin - if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) { + if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.includes(priv.edPublic)) { options.push({ tag: 'a', attributes: {'class': 'cp-toolbar-menu-admin fa fa-cogs'}, @@ -2051,6 +2051,19 @@ define([ }, }); } + // Add moderation panel link if the user is a moderator and support is enabled + if (priv.edPublic && Config.supportMailboxKey && Array.isArray(Config.moderatorKeys) && Config.moderatorKeys.includes(priv.edPublic)) { + Messages.moderationPage = "Support panel"; // XXX already in moderation/inner.js + options.push({ + tag: 'a', + attributes: {'class': 'cp-toolbar-menu-admin fa fa-comments-o'}, + content: h('span', Messages.moderationPage || 'Support panel'), + action: function () { + Common.openURL(origin+'/moderation/'); + return true; + }, + }); + } options.push({ tag: 'a', attributes: { diff --git a/www/common/inner/sidebar-layout.js b/www/common/inner/sidebar-layout.js index 17ae67c28..25a6ccfe4 100644 --- a/www/common/inner/sidebar-layout.js +++ b/www/common/inner/sidebar-layout.js @@ -44,14 +44,15 @@ define([ ]); }; + blocks.icon = (icon) => { + let prefix = icon.slice(0, icon.indexOf('-')); + let cls = `.${prefix}.${icon}`; + return h(`i${cls}`); + }; blocks.button = (type, icon, text) => { type = type || 'primary'; - if (icon && icon.indexOf('-') !== -1) { - let prefix = icon.slice(0, icon.indexOf('-')); - icon = `${prefix} ${icon}`; - } return h(`button.btn.btn-${type}`, [ - icon ? h('i', { 'class': icon }) : undefined, + icon ? blocks.icon(icon) : undefined, h('span', text) ]); }; @@ -81,7 +82,9 @@ define([ return h('span', value); }; blocks.block = (content, className) => { - return h('div', { class: className }, content); + let attr = {}; + if (className) { attr.class = className; } + return h('div', attr, content); }; blocks.paragraph = (content) => { return h('p', content); diff --git a/www/moderation/inner.js b/www/moderation/inner.js index 201973f0b..7e352c4e7 100644 --- a/www/moderation/inner.js +++ b/www/moderation/inner.js @@ -54,6 +54,8 @@ define([ }; // XXX + Messages.moderationPage = "Support panel"; // XXX + Messages.support_pending = "Pending tickets:"; Messages.support_pending_tag = "Pending"; Messages.support_active_tag = "Active"; @@ -954,7 +956,7 @@ Attachments:${JSON.stringify(msg.attachments, 0, 2)}`; displayed: displayed, sfCommon: common, $container: APP.$toolbar, - pageTitle: Messages.supportPage, + pageTitle: Messages.moderationPage, metadataMgr: common.getMetadataMgr(), }; APP.toolbar = Toolbar.create(configTb); @@ -977,7 +979,7 @@ Attachments:${JSON.stringify(msg.attachments, 0, 2)}`; createToolbar(); var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); - common.setTabTitle(Messages.supportPage); + common.setTabTitle(Messages.moderationPage); if (!ApiConfig.supportMailboxKey) { return void UI.errorLoadingScreen(Messages.support_disabledTitle); diff --git a/www/newadmin/inner.js b/www/newadmin/inner.js index 53e0be9a0..f6f1fe68f 100644 --- a/www/newadmin/inner.js +++ b/www/newadmin/inner.js @@ -111,9 +111,7 @@ define([ 'support' : { icon : 'fa fa-life-ring', content : [ - 'support-list', - 'support-init', - 'support-priv', + 'support-setup', ] }, 'broadcast' : { @@ -2574,6 +2572,266 @@ define([ }; + // XXX DUPLICATE FROM ADMIN/INNER.JS + Messages.admin_supportSetupHint = "Create or update the current support keys."; + Messages.admin_supportSetupTitle = "Initialize support"; + Messages.admin_supportEnabled = "Modern support system is enabled."; + Messages.admin_supportDisabled = "Modern support system is disabled."; + Messages.admin_supportInit = "Initialize support page on this instance"; + Messages.admin_supportDelete = "Disable support"; + Messages.admin_supportConfirm = "Are you sure? This will remove access to all current moderators and delete all existing tickets."; + Messages.admin_supportMembers = "Current support team"; + Messages.admin_supportAdd = "Add a contact to the support team"; + Messages.admin_supportRotateNotify = "Warning: new keys have been generated but an unenexpected error prevented the system to send them to the moderators. You may have to remove and re-add all the other moderators"; + sidebar.addItem('support-setup', function (cb) { + let supportKey = ApiConfig.supportMailboxKey; + let edPublic = common.getMetadataMgr().getPrivateData().edPublic; // My edPublic + let refresh = function () {}; + let content = blocks.block(); + const $div = $(content); + const redraw = function (moderatorsData) { + $div.empty(); + + const state = blocks.block(); + const $state = $(state).appendTo($div); + const button = blocks.button('primary', '', Messages.admin_supportInit); + const $button = $(button).appendTo($div); + const delButton = blocks.button('danger', '', Messages.admin_supportDelete); + const $delButton = $(delButton).appendTo($div).hide(); + const spinner = UI.makeSpinner($div); + + const setState = function () { + $state.html(''); + if (supportKey) { + $button.hide(); + $delButton.show(); + return $state.append([ + blocks.icon('fa-check'), + blocks.inline(Messages.admin_supportEnabled) + ]); + } + $button.show(); + $state.append([ + blocks.icon('fa-times'), + blocks.inline(Messages.admin_supportDisabled) + ]); + }; + setState(); + + Util.onClickEnter($delButton, function () { + UI.confirm(Messages.admin_supportConfirm, function (yes) { + if (!yes) { return; } + // Send the decree, don't delete data locally, we just want to remove + // the support UI for the clients + spinner.spin(); + $delButton.attr('disabled', 'disabled'); + APP.supportModule.execCommand('DISABLE_SUPPORT', {}, (obj) => { + $delButton.removeAttr('disabled'); + if (obj && obj.error) { + UI.warn(Messages.error); + console.error(obj.error); + return void spinner.hide(); + } + spinner.done(); + UI.log(Messages.saved); + supportKey = undefined; + refresh(); + }); + }); + }); + + const getMyData = () => { + let metadataMgr = common.getMetadataMgr(); + let priv = metadataMgr.getPrivateData(); + let user = metadataMgr.getUserData(); + return { + name: user.name, + edPublic: priv.edPublic, + curvePublic: user.curvePublic, + mailbox: user.notifications, + profile: user.profile + }; + }; + const getContactData = (curve) => { + if (!curve) { return getMyData(); } + let friends = common.getFriends(true); + let f = friends[curve]; + return { + name: f.displayName, + edPublic: f.edPublic, + curvePublic: f.curvePublic, + mailbox: f.notifications, + profile: f.profile + }; + }; + const generateKey = function () { + if (supportKey && !moderatorsData[edPublic]) { + UI.alert("A support key already exists. You must be a moderator to generate a new one or delete the existing support data."); + return; + } + spinner.spin(); + $button.attr('disabled', 'disabled'); + const onError = (waitFor, err, res) => { + if (waitFor) { waitFor.abort(); } + console.error(err, res); + spinner.hide(); + UI.warn(Messages.error); + }; + + nThen((waitFor) => { + // Add myself to moderator role if not already there + let me = getContactData(); + if (moderatorsData[me.edPublic]) { return; } + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADD_MODERATOR', + data: me + }, waitFor((e, response) => { + console.error(e, response); + })); + }).nThen((waitFor) => { + // Copy chainpad doc and pin + APP.supportModule.execCommand('ROTATE_KEYS', {}, waitFor((obj) => { + $button.removeAttr('disabled'); + if (obj && obj.error) { return void onError(waitFor, obj.error); } + if (obj && obj.success && obj.noNotify) { + UI.alert(Messages.admin_supportRotateNotify); + onError(waitFor, 'NOTIFY_ERROR', obj); + return; + } + spinner.done(); + UI.log(Messages.saved); + })); + }).nThen(() => { + refresh(); + }); + }; + + const addModerator = (curve, _cb) => { + let cb = Util.mkAsync(_cb); + let userData = getContactData(curve); + if (!userData) { return void cb('INVALID_USER'); } + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADD_MODERATOR', + data: userData + }, (e, response) => { + if (e || (response && response.error)) { + return void cb(e || response.error); + } + + // User added to support team in database, send them the keys + APP.supportModule.execCommand('ADD_MODERATOR', userData, cb); + }); + }; + const removeModerator = (ed) => { + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'REMOVE_MODERATOR', + data: ed + }, (e, response) => { + if (e || (response && response.error)) { + console.error(e || response.error); + return void UI.warn(Messages.error); + } + generateKey(); + }); + }; + + Util.onClickEnter($button, function () { + generateKey(); + }); + + const drawModerators = () => { + if (!supportKey) { return; } + const members = {}; + const friends = Util.clone(common.getFriends(false)); + Object.keys(moderatorsData).forEach((ed) => { + let m = moderatorsData[ed]; + members[m.curvePublic] = { + displayName: m.name, + edPublic: m.edPublic, + profile: m.profile, + curvePublic: m.curvePublic, + notificatons: m.mailbox + }; + }); + Object.keys(friends).forEach((curve) => { + if (members[curve]) { delete friends[curve]; } + }); + let currentList = UIElements.getUserGrid(Messages.admin_supportMembers, { + common: common, + list: true, + large: true, + noSelect: true, + data: members, + remove: (el) => { + let ed = $(el).attr('data-ed'); + removeModerator(ed); + } + }); + let contactsGrid = UIElements.getUserGrid(Messages.admin_supportAdd, { + common: common, + list: true, + large: true, + data: friends + }, function () {}); + + let addBtn = blocks.button('primary', '', Messages.tag_add); + Util.onClickEnter($(addBtn), () => { + var $sel = $(contactsGrid.div).find('.cp-usergrid-user.cp-selected'); + nThen((waitFor) => { + $sel.each((i, el) => { + let curve = $(el).attr('data-curve'); + if (!curve) { + console.error('Missing data on selected user', el); + return void UI.warn(Messages.error); + } + addModerator(curve, waitFor()); + }); + }).nThen(() => { + refresh(); + }); + }); + // Only moderators can add new moderators + if (!moderatorsData[edPublic]) { + contactsGrid.div = undefined; + addBtn = undefined; + } + + const list = blocks.block([ + currentList.div, + contactsGrid.div, + blocks.nav([addBtn]) + ]); + $div.append(list); + }; + drawModerators(); + }; + refresh = () => { + let moderators; + nThen((waitFor) => { + APP.supportModule.execCommand('GET_PRIVATE_KEY', {}, waitFor((obj) => { + supportKey = obj && obj.curvePublic; + })); + }).nThen((waitFor) => { + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'GET_MODERATORS', + data: {} + }, waitFor((e, response) => { + if (e || response.error) { + console.error(e || response.error); + UI.warn(Messages.error); + return; + } + moderators = response[0]; + })); + }).nThen(() => { + redraw(moderators); + }); + }; + refresh(); + + cb(content); + }); + sidebar.addItem('maintenance', function(cb){ var form = blocks.form([]); var refresh = getApi(function (Broadcast) { @@ -3377,10 +3635,11 @@ define([ // Add toolbar createToolbar(); + APP.supportModule = common.makeUniversal('support'); + // Content andThen(common, APP.$container); - common.setTabTitle(Messages.settings_title); UI.removeLoadingScreen(); }); });