diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index 07471183a..b25cbf43c 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -90,12 +90,21 @@ button.btn { @button-bg: @colortheme_sidebar-button-bg; @button-red-bg: @colortheme_sidebar-button-red-bg; + @button-alt-bg: @colortheme_sidebar-button-alt-bg; background-color: @button-bg; border-color: darken(@button-bg, 10%); color: white; &:hover { background-color: darken(@button-bg, 10%); } + &.btn-secondary { + background-color: @button-alt-bg; + border-color: darken(@button-alt-bg, 10%); + color: black; + &:hover { + background-color: darken(@button-alt-bg, 10%); + } + } &.btn-danger { background-color: @button-red-bg; border-color: darken(@button-red-bg, 10%); diff --git a/customize.dist/src/less2/include/support.less b/customize.dist/src/less2/include/support.less new file mode 100644 index 000000000..6f4bf559b --- /dev/null +++ b/customize.dist/src/less2/include/support.less @@ -0,0 +1,69 @@ +@import (reference) "./colortheme-all.less"; +.support_main () { + @ticket-bg: #F7F7F7; + @msg-bg: #eee; + @fromme-bg: #ddd; + .cp-support-container { + .cp-support-list-ticket { + display: flex; + flex-flow: column; + background-color: @ticket-bg; + padding: 10px; + width: 1200px; + max-width: 90%; + margin: 5px auto; + .cp-support-list-message { + background-color: @msg-bg; + margin: 2px; + padding: 2px 5px; + .cp-support-fromme { + background-color: @fromme-bg; + } + .cp-support-showdata { + cursor: pointer; + background-color: @fromme-bg; + .cp-support-message-data { + display: none; + cursor: default; + } + } + .cp-support-message-time { + float: right; + } + pre { + margin-bottom: 0; + &.cp-support-message-content { + margin-top: 10px; + margin-bottom: 10px; + } + } + } + .cp-support-list-actions { + order: 3; + .cp-support-hide { + display: none; + } + } + .cp-support-form-container { + order: 2; + } + &.cp-support-list-closed { + .cp-support-list-actions { + display: block !important; + .cp-support-answer, .cp-support-close { + display: none; + } + .cp-support-hide { + display: inline; + } + } + .cp-support-form-container { + display: none !important; + } + } + } + } +} + + + diff --git a/scripts/generateAdminKeys.js b/scripts/generate-admin-keys.js similarity index 96% rename from scripts/generateAdminKeys.js rename to scripts/generate-admin-keys.js index 072dd8b3d..62fe3e121 100644 --- a/scripts/generateAdminKeys.js +++ b/scripts/generate-admin-keys.js @@ -1,12 +1,13 @@ +/* jshint esversion: 6, node: true */ + const Nacl = require('tweetnacl'); -const Crypto = require('crypto'); const keyPair = Nacl.box.keyPair(); console.log(keyPair); console.log("You've just generated a new key pair for your support mailbox."); -console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server.") +console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server."); console.log("Once restarted, administrators (specified with 'adminKeys' in config.js too) will be able to add the private key into their account. This can be done using the administration panel."); console.log("You will have to send the private key to each administrator manually so that they can add it to their account."); console.log(); diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index c3b8a31de..a1c1ee779 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -1,5 +1,6 @@ @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less'; +@import (reference) '../../customize/src/less2/include/support.less'; &.cp-app-admin { @@ -9,6 +10,11 @@ @color: @colortheme_admin-color ); .sidebar-layout_main(); + .support_main(); + + .cp-hidden { + display: none !important; + } display: flex; flex-flow: column; diff --git a/www/admin/inner.js b/www/admin/inner.js index 83dec1415..96b4e5bba 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -9,6 +9,8 @@ define([ '/customize/messages.js', '/common/common-interface.js', '/common/common-util.js', + '/common/common-hash.js', + '/support/ui.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', @@ -23,7 +25,9 @@ define([ h, Messages, UI, - Util + Util, + Hash, + Support ) { var APP = {}; @@ -41,6 +45,10 @@ define([ 'cp-admin-active-pads', 'cp-admin-registered', 'cp-admin-disk-usage', + ], + 'support': [ + 'cp-admin-support-list', + 'cp-admin-support-init' ] }; @@ -94,7 +102,6 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ACTIVE_SESSIONS', }, function (e, data) { - console.log(e, data); var total = data[0]; var ips = data[1]; $div.append(h('pre', total + ' (' + ips + ')')); @@ -160,6 +167,111 @@ define([ return $div; }; + var supportKey = ApiConfig.supportMailbox; + create['support-list'] = function () { + if (!supportKey || !APP.privateKey) { return; } + var $div = makeBlock('support-list'); + $div.addClass('cp-support-container'); + var hashesById = {}; + + // Register to the "support" mailbox + common.mailbox.subscribe(['supportadmin'], { + onMessage: function (data) { + /* + Get ID of the ticket + If we already have a div for this ID + Push the message to the end of the ticket + If it's a new ticket ID + Make a new div for this ID + */ + var msg = data.content.msg; + var hash = data.content.hash; + var content = msg.content; + var id = content.id; + var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]'); + + hashesById[id] = hashesById[id] || []; + if (hashesById[id].indexOf(hash) === -1) { + hashesById[id].push(data); + } + + if (msg.type === 'CLOSE') { + // A ticket has been closed by the admins... + if (!$ticket.length) { return; } + $ticket.addClass('cp-support-list-closed'); + $ticket.append(Support.makeCloseMessage(common, content, hash)); + return; + } + if (msg.type !== 'TICKET') { return; } + + if (!$ticket.length) { + $ticket = Support.makeTicket($div, common, content, function () { + var error = false; + hashesById[id].forEach(function (d) { + common.mailbox.dismiss(d, function (err) { + if (err) { + error = true; + console.error(err); + } + }); + }); + if (!error) { $ticket.remove(); } + }); + } + $ticket.append(Support.makeMessage(common, content, hash, true)); + } + }); + return $div; + }; + + var checkAdminKey = function (priv) { + if (!supportKey) { return; } + return Hash.checkBoxKeyPair(priv, supportKey); + }; + + create['support-init'] = function () { + var $div = makeBlock('support-init'); + if (!supportKey) { + $div.append(h('p', Messages.admin_supportInitHelp || "Your server is not configured to have a support mailbox. If you want a support mailbox to receive messages from your users, you should ask your server administrator to run the script located in './scripts/generate-admin-keys.js', store the public key in the 'config.js' file, and send you the private key.")); // XXX + return $div; + } + if (!APP.privateKey || !checkAdminKey(APP.privateKey)) { + $div.append(h('p', Messages.admin_supportInitPrivate || "Your CryptPad instance is configured to use a support mailbox but your account doesn't have the correct private key to access it. Please use the following form to add or update the private key to your account")); // XXX + + var error = h('div.cp-admin-support-error'); + var input = h('input.cp-admin-add-private-key'); + var button = h('button.btn.btn-primary', Messages.admin_supportAddKey || 'add key'); // XXX + + if (APP.privateKey && !checkAdminKey(APP.privateKey)) { + $(error).text(Messages.admin_supportAddError || 'invalid'); // XXX + } + + $div.append(h('div', [ + error, + input, + button + ])); + + $(button).click(function () { + var key = $(input).val(); + if (!checkAdminKey(key)) { + $(input).val(''); + return void $(error).text(Messages.admin_supportAddError || 'invalid'); // XXX + } + sFrameChan.query("Q_ADMIN_MAILBOX", key, function () { + console.log(key); + console.log(arguments); + APP.privateKey = key; + console.log('ok'); + $('.cp-admin-support-init').hide(); + APP.$rightside.append(create['support-list']()); // TODO: check? + }); + }); + return $div; + } + return; + }; + var hideCategories = function () { APP.$rightside.find('> div').hide(); }; @@ -180,6 +292,7 @@ define([ var $category = $('
', {'class': 'cp-sidebarlayout-category'}).appendTo($categories); if (key === 'general') { $category.append($('', {'class': 'fa fa-user-o'})); } if (key === 'stats') { $category.append($('', {'class': 'fa fa-hdd-o'})); } + if (key === 'support') { $category.append($('', {'class': 'fa fa-life-ring'})); } if (key === active) { $category.addClass('cp-leftside-active'); @@ -236,6 +349,7 @@ define([ return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden'); } + APP.privateKey = privateData.supportPrivateKey; APP.origin = privateData.origin; APP.readOnly = privateData.readOnly; diff --git a/www/admin/main.js b/www/admin/main.js index cdfaf115c..817d2bd2e 100644 --- a/www/admin/main.js +++ b/www/admin/main.js @@ -38,6 +38,9 @@ define([ }).nThen(function (/*waitFor*/) { var addRpc = function (sframeChan, Cryptpad/*, Utils*/) { // Adding a new avatar from the profile: pin it and store it in the object + sframeChan.on('Q_ADMIN_MAILBOX', function (data, cb) { + Cryptpad.addAdminMailbox(data, cb); + }); sframeChan.on('Q_ADMIN_RPC', function (data, cb) { Cryptpad.adminRpc(data, cb); }); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 1b588db60..3f613ae4f 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -89,6 +89,18 @@ define([ if (!publicKey) { return; } return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16)); }; + Hash.getBoxPublicFromSecret = function (priv) { + if (!priv) { return; } + var u8_priv = Hash.decodeBase64(priv); + var pair = Nacl.box.keyPair.fromSecretKey(u8_priv); + return Hash.encodeBase64(pair.publicKey); + }; + Hash.checkBoxKeyPair = function (priv, pub) { + if (!pub || !priv) { return false; } + var u8_priv = Hash.decodeBase64(priv); + var pair = Nacl.box.keyPair.fromSecretKey(u8_priv); + return pub === Hash.encodeBase64(pair.publicKey); + }; Hash.createRandomHash = function (type, password) { var cryptor; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 66ad6a442..fef46951c 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -620,6 +620,9 @@ define([ common.adminRpc = function (data, cb) { postMessage("ADMIN_RPC", data, cb); }; + common.addAdminMailbox = function (data, cb) { + postMessage("ADMIN_ADD_MAILBOX", data, cb); + }; // Network common.onNetworkDisconnect = Util.mkEvent(); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 9c0dfb864..6022c5f37 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -479,7 +479,8 @@ define([ thumbnails: disableThumbnails === false, isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])), support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']), - pendingFriends: store.proxy.friends_pending || {} + pendingFriends: store.proxy.friends_pending || {}, + supportPrivateKey: Util.find(store.proxy, ['mailboxes', 'supportadmin', 'keys', 'curvePrivate']) } }; cb(JSON.parse(JSON.stringify(metadata))); @@ -1060,6 +1061,26 @@ define([ cb(res); }); }; + Store.addAdminMailbox = function (clientId, data, cb) { + var priv = data; + var pub = Hash.getBoxPublicFromSecret(priv); + if (!priv || !pub) { return void cb({error: 'EINVAL'}); } + var channel = Hash.getChannelIdFromKey(pub); + var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {}; + var box = mailboxes.supportadmin = { + channel: channel, + viewed: [], + lastKnownHash: '', + keys: { + curvePublic: pub, + curvePrivate: priv + } + }; + store.mailbox.open('supportadmin', box, function () { + console.log('ready'); + }); + onSync(cb); + }; ////////////////////////////////////////////////////////////////// /////////////////////// PAD ////////////////////////////////////// diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index cb4a4a5fe..7d6e2eb5d 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -10,6 +10,7 @@ define([ var TYPES = [ 'notifications', + 'supportadmin', 'support' ]; var BLOCKING_TYPES = [ @@ -96,10 +97,10 @@ proxy.mailboxes = { network.join(user.channel).then(function (wc) { wc.bcast(ciphertext).then(function () { cb(); - wc.leave(); // If we've just sent a message to one of our mailboxes, we have to trigger the handler manually // (the server won't send back our message to us) + // If it isn't one of our mailboxes, we can close it now var box; if (Object.keys(ctx.boxes).some(function (t) { var _box = ctx.boxes[t]; @@ -110,6 +111,8 @@ proxy.mailboxes = { })) { var hash = ciphertext.slice(0, 64); box.onMessage(text, null, null, null, hash, user.curvePublic); + } else { + wc.leave(); } }); }, function (err) { @@ -200,7 +203,7 @@ proxy.mailboxes = { if (!Crypto.Mailbox) { return void console.error("chainpad-crypto is outdated and doesn't support mailboxes."); } - var keys = getMyKeys(ctx); + var keys = m.keys || getMyKeys(ctx); if (!keys) { return void console.error("missing asymmetric encryption keys"); } var crypto = Crypto.Mailbox.createEncryptor(keys); var cfg = { @@ -364,6 +367,7 @@ proxy.mailboxes = { Object.keys(mailboxes).forEach(function (key) { if (TYPES.indexOf(key) === -1) { return; } var m = mailboxes[key]; +console.log(key, m); if (BLOCKING_TYPES.indexOf(key) === -1) { openChannel(ctx, key, m, function () { @@ -386,6 +390,11 @@ proxy.mailboxes = { }); }; + mailbox.open = function (key, m, cb) { + if (TYPES.indexOf(key) === -1) { return; } + openChannel(ctx, key, m, cb); + }; + mailbox.dismiss = function (data, cb) { dismiss(ctx, data, '', cb); }; diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 4bc2dfbc8..6b774339c 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -84,6 +84,7 @@ define([ DELETE_ACCOUNT: Store.deleteAccount, // Admin ADMIN_RPC: Store.adminRpc, + ADMIN_ADD_MAILBOX: Store.addAdminMailbox, }; Rpc.query = function (cmd, data, cb) { diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js index 4bcee18af..86d39b415 100644 --- a/www/common/sframe-common-mailbox.js +++ b/www/common/sframe-common-mailbox.js @@ -123,7 +123,7 @@ define([ var onMessage = function (data) { // data = { type: 'type', content: {msg: 'msg', hash: 'hash'} } - console.log(data.content); + console.log(data.type, data.content); pushMessage(data); if (!history[data.type]) { history[data.type] = []; } history[data.type].push(data.content); diff --git a/www/support/app-support.less b/www/support/app-support.less index 60091a0d9..c033332eb 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -1,5 +1,6 @@ @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less'; +@import (reference) '../../customize/src/less2/include/support.less'; &.cp-app-support { .framework_min_main( @@ -8,6 +9,7 @@ @color: @colortheme_support-color ); .sidebar-layout_main(); + .support_main(); .cp-hidden { display: none !important; diff --git a/www/support/inner.js b/www/support/inner.js index e7fdf6fc1..7b4d78bc9 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -9,8 +9,8 @@ define([ '/common/common-hash.js', '/customize/messages.js', '/common/hyperscript.js', + '/support/ui.js', '/api/config', - '/common/common-feedback.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', @@ -26,17 +26,15 @@ define([ Hash, Messages, h, - ApiConfig, - Feedback + Support, + ApiConfig ) { - var saveAs = window.saveAs; var APP = window.APP = {}; var common; var metadataMgr; var privateData; - var sframeChan; var categories = { 'tickets': [ @@ -47,9 +45,9 @@ define([ ], }; - var supportKey = ApiConfig.supportMailbox; // XXX curvePublic - var supportChannel = Hash.getChannelIdFromKey(supportKey); // XXX - if (true || !supportKey || !supportChannel) { + var supportKey = ApiConfig.supportMailbox; + var supportChannel = Hash.getChannelIdFromKey(supportKey); + if (!supportKey || !supportChannel) { categories = { 'tickets': [ 'cp-support-disabled' @@ -75,138 +73,14 @@ define([ return $div; }; - var showError = function (form, msg) { - if (!msg) { - return void $(form).find('.cp-support-form-error').text('').hide(); - } - $(form).find('.cp-support-form-error').text(msg).show(); - }; - - var makeForm = function (cb, title) { - var button; - - if (typeof(cb) === "function") { - button = h('button.btn.btn-primary.cp-support-list-send', Messages.support_send || 'Send'); // XXX - $(button).click(cb); - } - - var content = [ - h('hr'), - h('div.cp-support-form-error'), - h('label' + (title ? '.cp-hidden' : ''), Messages.support_formTitle || 'title...'), // XXX - h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), { - placeholder: Messages.support_formTitlePlaceholder || 'title here...', // XXX - value: title - }), - cb ? undefined : h('br'), - h('label', Messages.support_formMessage || 'content...'), // XXX - h('textarea.cp-support-form-msg', { - placeholder: Messages.support_formMessagePlaceholder || 'describe your problem here...' // XXX - }), - h('hr'), - button - ]; - - return h('div.cp-support-form-container', content); - }; - - var sendForm = function (id, form) { - var user = metadataMgr.getUserData(); - privateData = metadataMgr.getPrivateData(); - var $title = $(form).find('.cp-support-form-title'); - var $content = $(form).find('.cp-support-form-msg'); - - var title = $title.val(); - if (!title) { - return void showError(form, Messages.support_formTitleError || 'title error'); // XXX - } - var content = $content.val(); - if (!content) { - return void showError(form, Messages.support_formContentError || 'content error'); // XXX - } - // Success: hide any error - showError(form, null); - $content.val(''); - $title.val(''); - - common.mailbox.sendTo('TICKET', { - sender: { - name: user.name, - channel: privateData.support, - curvePublic: user.curvePublic, - edPublic: privateData.edPublic - }, - title: title, - message: content, - id: id - }, { - channel: supportChannel, - curvePublic: supportKey - }); - common.mailbox.sendTo('TICKET', { - sender: { - name: user.name, - channel: privateData.support, - curvePublic: user.curvePublic, - edPublic: privateData.edPublic - }, - title: title, - message: content, - id: id - }, { - channel: privateData.support, - curvePublic: user.curvePublic - }); - - return true; - }; // List existing (open?) tickets create['list'] = function () { var key = 'list'; var $div = makeBlock(key); - - var makeTicket = function (content) { - var ticketTitle = content.id + ' - ' + content.title; - var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer || 'Answer'); // XXX - - var $ticket = $(h('div.cp-support-list-ticket', { - 'data-id': content.id - }, [ - h('h2', ticketTitle), - h('div.cp-support-list-actions', answer) - ])); - - $(answer).click(function () { - $div.find('.cp-support-form-container').remove(); - $div.find('.cp-support-answer').show(); - $(answer).hide(); - var form = makeForm(function () { - var sent = sendForm(content.id, form); - if (sent) { - $(answer).show(); - $(form).remove(); - } - }, content.title); - $ticket.append(form); - }); - - $div.append($ticket); - return $ticket; - }; - - var makeMessage = function (content, hash) { - // Check content.sender to see if it comes from us or from an admin - // XXX admins should send their personal public key? - var fromMe = content.sender && content.sender.edPublic === privateData.edPublic; - return h('div.cp-support-list-message', [ - h('p.cp-support-message-from' + fromMe ? '.cp-support-fromme' : '', - //Messages._getKey('support_from', [content.sender.name])), // XXX - [h('b', 'From: '), content.sender.name]), - h('pre.cp-support-message-content', content.message) - ]); - }; + $div.addClass('cp-support-container'); + var hashesById = {}; // Register to the "support" mailbox common.mailbox.subscribe(['support'], { @@ -220,23 +94,39 @@ define([ */ var msg = data.content.msg; var hash = data.content.hash; + var content = msg.content; + var id = content.id; + var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]'); + + hashesById[id] = hashesById[id] || []; + if (hashesById[id].indexOf(hash) === -1) { + hashesById[id].push(data); + } + if (msg.type === 'CLOSE') { // A ticket has been closed by the admins... - // TODO: add a "closed" class to the ticket in the UI + if (!$ticket.length) { return; } + $ticket.addClass('cp-support-list-closed'); + $ticket.append(Support.makeCloseMessage(common, content, hash)); + return; } if (msg.type !== 'TICKET') { return; } - var content = msg.content; - var id = content.id; - var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]'); if (!$ticket.length) { - $ticket = makeTicket(content); + $ticket = Support.makeTicket($div, common, content, function () { + var error = false; + hashesById[id].forEach(function (d) { + common.mailbox.dismiss(d, function (err) { + if (err) { + error = true; + console.error(err); + } + }); + }); + if (!error) { $ticket.remove(); } + }); } - $ticket.append(makeMessage(content, hash)); - }, - onViewed: function (data) { - // Remove the ticket with this hash - // If the ticket div is empty, remove the ticket div + $ticket.append(Support.makeMessage(common, content, hash, false)); } }); return $div; @@ -247,14 +137,20 @@ define([ var key = 'form'; var $div = makeBlock(key, true); - var form = makeForm(); + var form = Support.makeForm(); $div.find('button').before(form); var id = Util.uid(); $div.find('button').click(function () { - var sent = sendForm(id, form); + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); + var user = metadataMgr.getUserData(); + var sent = Support.sendForm(common, id, form, { + channel: privateData.support, + curvePublic: user.curvePublic + }); if (sent) { $('.cp-sidebarlayout-category[data-category="tickets"]').click(); } @@ -338,7 +234,7 @@ define([ APP.$toolbar = $('#cp-toolbar'); APP.$leftside = $('
', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container); APP.$rightside = $('
', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); - sFrameChan = common.getSframeChannel(); + var sFrameChan = common.getSframeChannel(); sFrameChan.onReady(waitFor()); }).nThen(function (/*waitFor*/) { createToolbar(); diff --git a/www/support/main.js b/www/support/main.js index d84b1f055..90aead240 100644 --- a/www/support/main.js +++ b/www/support/main.js @@ -36,8 +36,6 @@ define([ }; window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { - var addRpc = function (sframeChan, Cryptpad, Utils) { - }; var category; if (window.location.hash) { category = window.location.hash.slice(1); @@ -48,7 +46,6 @@ define([ }; SFCommonO.start({ noRealtime: true, - addRpc: addRpc, addData: addData }); }); diff --git a/www/support/ui.js b/www/support/ui.js new file mode 100644 index 000000000..590712ff7 --- /dev/null +++ b/www/support/ui.js @@ -0,0 +1,203 @@ +define([ + 'jquery', + '/api/config', + '/common/hyperscript.js', + '/common/common-hash.js', + '/common/common-util.js', + '/customize/messages.js', +], function ($, ApiConfig, h, Hash, Util, Messages) { + + var showError = function (form, msg) { + if (!msg) { + return void $(form).find('.cp-support-form-error').text('').hide(); + } + $(form).find('.cp-support-form-error').text(msg).show(); + }; + + var send = function (common, id, type, data, dest) { + var supportKey = ApiConfig.supportMailbox; + var supportChannel = Hash.getChannelIdFromKey(supportKey); + var metadataMgr = common.getMetadataMgr(); + var user = metadataMgr.getUserData(); + var privateData = metadataMgr.getPrivateData(); + + data = data || {}; + + data.sender = { + name: user.name, + channel: privateData.support, + curvePublic: user.curvePublic, + edPublic: privateData.edPublic + }; + data.id = id; + data.time = +new Date(); + + // Send the message to the admin mailbox and to the user mailbox + common.mailbox.sendTo(type, data, { + channel: supportChannel, + curvePublic: supportKey + }); + common.mailbox.sendTo(type, data, { + channel: dest.channel, + curvePublic: dest.curvePublic + }); + }; + + var sendForm = function (common, id, form, dest) { + var $title = $(form).find('.cp-support-form-title'); + var $content = $(form).find('.cp-support-form-msg'); + + var title = $title.val(); + if (!title) { + return void showError(form, Messages.support_formTitleError || 'title error'); // XXX + } + var content = $content.val(); + if (!content) { + return void showError(form, Messages.support_formContentError || 'content error'); // XXX + } + // Success: hide any error + showError(form, null); + $content.val(''); + $title.val(''); + + send(common, id, 'TICKET', { + title: title, + message: content, + }, dest); + + return true; + }; + + var makeForm = function (cb, title) { + var button; + + if (typeof(cb) === "function") { + button = h('button.btn.btn-primary.cp-support-list-send', Messages.support_send || 'Send'); // XXX + $(button).click(cb); + } + + var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined; + + var content = [ + h('hr'), + h('div.cp-support-form-error'), + h('label' + (title ? '.cp-hidden' : ''), Messages.support_formTitle || 'title...'), // XXX + h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), { + placeholder: Messages.support_formTitlePlaceholder || 'title here...', // XXX + value: title || '' + }), + cb ? undefined : h('br'), + h('label', Messages.support_formMessage || 'content...'), // XXX + h('textarea.cp-support-form-msg', { + placeholder: Messages.support_formMessagePlaceholder || 'describe your problem here...' // XXX + }), + h('hr'), + button, + cancel + ]; + + var form = h('div.cp-support-form-container', content); + + $(cancel).click(function () { + $(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').show(); + $(form).remove(); + }); + + return form; + }; + + var makeTicket = function ($div, common, content, onHide) { + var ticketTitle = content.id + ' - ' + content.title; + var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer || 'Answer'); // XXX + var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close || 'Close'); // XXX + var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove || 'Remove'); // XXX + + var actions = h('div.cp-support-list-actions', [ + answer, + close, + hide + ]); + + var $ticket = $(h('div.cp-support-list-ticket', { + 'data-id': content.id + }, [ + h('h2', ticketTitle), + actions + ])); + + $(close).click(function () { + send(common, content.id, 'CLOSE', {}, content.sender); + }); + + $(hide).click(function () { + if (typeof(onHide) !== "function") { return; } + onHide(); + }); + + $(answer).click(function () { + $ticket.find('.cp-support-form-container').remove(); + $(actions).hide(); + var form = makeForm(function () { + var sent = sendForm(common, content.id, form, content.sender); + if (sent) { + $(actions).show(); + $(form).remove(); + } + }, content.title); + $ticket.append(form); + }); + + $div.append($ticket); + return $ticket; + }; + + var makeMessage = function (common, content, hash, isAdmin) { + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); + + // Check content.sender to see if it comes from us or from an admin + // XXX admins should send their personal public key? + var fromMe = content.sender && content.sender.edPublic === privateData.edPublic; + + var userData = h('div.cp-support-showdata', [ + Messages.support_showData || 'Show/hide data', // XXX + h('pre.cp-support-message-data', JSON.stringify(content.sender, 0, 2)) + ]); + $(userData).click(function () { + $(userData).find('pre').toggle(); + }); + + return h('div.cp-support-list-message', { + 'data-hash': hash + }, [ + h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''), + //Messages._getKey('support_from', [content.sender.name, new Date(content.time)])), // XXX + [h('b', 'From: '), content.sender.name, h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')]), + h('pre.cp-support-message-content', content.message), + isAdmin ? userData : undefined, + ]); + }; + + var makeCloseMessage = function (common, content, hash) { + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); + var fromMe = content.sender && content.sender.edPublic === privateData.edPublic; + + return h('div.cp-support-list-message', { + 'data-hash': hash + }, [ + h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''), + //Messages._getKey('support_from', [content.sender.name, new Date(content.time)])), // XXX + [h('b', 'From: '), content.sender.name, h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')]), + h('pre.cp-support-message-content', Messages.support_closed || 'Ticket closed...') // XXX + ]); + }; + + return { + sendForm: sendForm, + makeForm: makeForm, + makeTicket: makeTicket, + makeMessage: makeMessage, + makeCloseMessage: makeCloseMessage + }; +});