New support: realtime update of admin and user views

This commit is contained in:
yflory 2024-02-20 14:00:09 +01:00
parent 43047ab326
commit 503bba974c
7 changed files with 251 additions and 83 deletions

View File

@ -59,10 +59,29 @@
display: none;
}
}
.cp-support-ispremium {
padding: 0 5px;
background-color: @cp_admin-premium-bg;
}
.cp-support-list-message {
background-color: @msg-bg;
padding: 5px 5px;
border-radius: @variables_radius;
&.cp-support-fromadmin {
background-color: @cp_admin-isadmin-bg !important;
.cp-support-message-from, .cp-support-showdata {
color: @cryptpad_text_col;
background-color: fade(@cp_admin-isadmin-bg, 10%) !important;
}
}
&:last-child {
&.cp-support-frompremium {
background-color: @cp_admin-premium-bg;
.cp-support-showdata {
background-color: fade(@cp_admin-premium-bg, 10%);
}
}
}
.cp-support-fromme {
background-color: @fromme-bg;
}
@ -86,6 +105,9 @@
margin-bottom: 10px;
}
}
&:not(:last-child) {
margin-bottom: 3px;
}
}
.cp-support-list-actions {
display: flex;

View File

@ -861,17 +861,20 @@ define([
var content = msg.content;
content.time = data.time;
if (!content.isAdmin) { // A user replied to an admin
// Update admin chainpad
let i = 0;
let handle = function () {
var support = Util.find(ctx, ['store', 'modules', 'support']);
if (!support && i++ < 100) { setTimeout(handle, 600); }
if (!support) { return; }
let i = 0;
let handle = function () {
var support = Util.find(ctx, ['store', 'modules', 'support']);
if (!support && i++ < 100) { setTimeout(handle, 600); }
if (!support) { return; }
if (!content.isAdmin) { // A user replied to an admin
// Update admin chainpad
support.updateAdminTicket(content);
};
handle();
}
} else {
// Trigger realtime update of user support
support.updateUserTicket(content);
}
};
handle();
if (supportNotif) { return void cb(true); }
supportNotif = content.channel;

View File

@ -3,6 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/api/config',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
@ -11,7 +12,7 @@ define([
'chainpad-listmap',
'/components/chainpad/chainpad.dist.js',
'chainpad-netflux'
], function (Util, Hash, Realtime, nThen, Crypto, Listmap, ChainPad, CpNetflux) {
], function (ApiConfig, Util, Hash, Realtime, nThen, Crypto, Listmap, ChainPad, CpNetflux) {
var Support = {};
// UTILS
@ -20,6 +21,8 @@ define([
var cb = Util.mkAsync(_cb);
if (isAdmin && !ctx.adminRdyEvt) { return void cb('EFORBIDDEN'); }
require(['/api/config?' + (+new Date())], function (NewConfig) {
ctx.adminKeys = NewConfig.adminKeys; // Update admin keys // XXX MODERATOR
var supportKey = NewConfig.newSupportMailbox;
if (!supportKey) { return void cb('E_NOT_INIT'); }
@ -222,6 +225,11 @@ define([
var getMyTickets = function (ctx, data, cId, cb) {
var all = [];
var n = nThen;
if (!ctx.clients[cId]) {
ctx.clients[cId] = {
admin: false
};
}
Object.keys(ctx.supportData).forEach(function (ticket) {
n = n((waitFor) => {
var t = Util.clone(ctx.supportData[ticket]);
@ -250,6 +258,11 @@ define([
var listTicketsAdmin = function (ctx, data, cId, cb) {
if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); }
if (!ctx.clients[cId]) {
ctx.clients[cId] = {
admin: true
};
}
ctx.adminRdyEvt.reg(() => {
var doc = ctx.adminDoc.proxy;
cb(Util.clone(doc.tickets.active));
@ -264,8 +277,22 @@ define([
if (Array.isArray(res) && res.length) {
res.sort((t1, t2) => { return t1.time - t2.time; });
let last = res[res.length - 1];
let premium = res.some((msg) => {
let curve = Util.find(msg, ['sender', 'curvePublic']);
if (data.curvePublic !== curve) { return; }
return Util.find(msg, ['sender', 'quota', 'plan']);
});
var senderKey = last.sender && last.sender.edPublic;
var entry = doc.tickets.active[data.channel];
if (entry) { entry.time = last.time; }
if (entry) {
entry.time = last.time;
entry.premium = premium;
if (senderKey) {
entry.lastAdmin = ctx.adminKeys.indexOf(senderKey) !== -1
}
}
}
cb(res);
});
@ -288,6 +315,14 @@ define([
// Mailbox events
var notifyClient = function (ctx, admin, type, channel) {
let notifyList = Object.keys(ctx.clients).filter((cId) => {
return Boolean(ctx.clients[cId].admin) === admin;
});
if (!notifyList.length) { return; }
ctx.emit(type, { channel }, [notifyList]);
};
var addAdminTicket = function (ctx, data, cb) {
// Wait for the chainpad to be ready before adding the data
if (!ctx.adminRdyEvt) { return void cb(false); } // XXX not an admin, delete mailbox?
@ -310,6 +345,7 @@ define([
Realtime.whenRealtimeSyncs(ctx.adminDoc.realtime, function () {
cb(false);
});
notifyClient(ctx, true, 'NEW_TICKET', data.channel);
});
});
};
@ -325,10 +361,15 @@ define([
if (!doc.tickets.active[data.channel] && !doc.tickets.pending[data.channel]) {
return; }
let t = doc.tickets.active[data.channel] || doc.tickets.pending[data.channel];
t.time = data.time;
if (data.time > (t.time + 2000)) { t.time = data.time; }
t.lastAdmin = false;
notifyClient(ctx, true, 'UPDATE_TICKET', data.channel);
});
});
};
var updateUserTicket = function (ctx, data) {
notifyClient(ctx, false, 'UPDATE_TICKET', data.channel);
};
// INITIALIZE ADMIN
@ -354,6 +395,7 @@ define([
doc.tickets = doc.tickets || {};
doc.tickets.active = doc.tickets.active || {};
doc.tickets.closed = doc.tickets.closed || {};
doc.tickets.pending = doc.tickets.pending || {};
ctx.adminRdyEvt.fire();
cb();
});
@ -405,11 +447,11 @@ define([
var proxy = store.proxy.support = store.proxy.support || {};
var ctx = {
adminKeys: ApiConfig.adminKeys,
supportData: proxy,
store: cfg.store,
Store: cfg.Store,
emit: emit,
channels: {},
clients: {}
};
@ -419,7 +461,7 @@ define([
support.ctx = ctx;
support.removeClient = function (clientId) {
// XXX TODO
delete ctx.clients[clientId];
};
support.leavePad = function (padChan) {
// XXX TODO
@ -430,6 +472,9 @@ define([
support.updateAdminTicket = function (content) {
updateAdminTicket(ctx, content);
};
support.updateUserTicket = function (content) {
updateUserTicket(ctx, content);
};
support.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;

View File

@ -52,16 +52,27 @@ define([
var Nacl = window.nacl;
var common;
var sFrameChan;
var events = {
'NEW_TICKET': Util.mkEvent(),
'UPDATE_TICKET': Util.mkEvent()
};
var andThen = function () {
var andThen = function (linkedTicket) {
var $body = $('#cp-content-container');
var button = h('button.btn.btn-primary', 'refresh'); // XXX
$body.append(h('div', button));
var $container = $(h('div.cp-support-container')).appendTo($body);
var refresh = () => {
let open = [];
let refresh = () => {
APP.module.execCommand('LIST_TICKETS_ADMIN', {}, (tickets) => {
let activeForms = {};
$container.find('.cp-support-form-container').each((i, el) => {
let id = $(el).attr('data-id');
if (!id) { return; }
activeForms[id] = el;
});
$container.empty();
var col1 = h('div.cp-support-column', h('h1', [
h('span', Messages.admin_support_premium),
@ -80,24 +91,33 @@ define([
return tickets[c2].time - tickets[c1].time;
};
const onLoad = function (ticket, channel, data) {
const onShow = function (ticket, channel, data, done) {
APP.module.execCommand('LOAD_TICKET_ADMIN', {
channel: channel,
curvePublic: data.authorKey
}, function (obj) {
if (!Array.isArray(obj)) {
console.error(obj && obj.error);
done();
return void UI.warn(Messages.error);
}
obj.forEach(function (msg) {
console.error(msg);
if (!data.notifications) {
data.notifications = Util.find(msg, ['sender', 'notifications']);
}
$(ticket).append(APP.support.makeMessage(msg));
});
if (!open.includes(channel)) { open.push(channel); }
done();
});
};
const onHide = function (ticket, channel, data, done) {
$(ticket).find('.cp-support-list-message').remove();
open = open.filter((chan) => {
return chan !== channel;
});
done();
};
const onClose = function (ticket, channel, data) {
APP.module.execCommand('CLOSE_TICKET_ADMIN', {
channel: channel,
@ -120,26 +140,43 @@ define([
return void UI.warn(Messages.error);
}
$(ticket).find('.cp-support-list-message').remove();
refresh(); // XXX RE-open this ticket and scroll to?
$(ticket).find('.cp-support-form-container').remove();
refresh();
});
};
Object.keys(tickets).sort(sortTicket).forEach(function (channel) {
var d = tickets[channel];
var ticket = APP.support.makeTicket(channel, d, onLoad, onClose, onReply);
var ticket = APP.support.makeTicket({
id: channel,
content: d,
form: activeForms[channel],
onShow, onHide, onClose, onReply
});
var container;
if (d.lastAdmin) { container = col3; }
else if (d.premium) { container = col1; }
else { container = col2; }
$(container).append(ticket);
if (open.includes(channel)) { return void ticket.open(); }
if (linkedTicket === channel) {
linkedTicket = undefined;
ticket.open();
ticket.scrollIntoView();
}
});
open = [];
console.log(tickets);
});
};
let _refresh = Util.throttle(refresh, 500);
Util.onClickEnter($(button), function () {
refresh();
});
events.NEW_TICKET.reg(_refresh);
events.UPDATE_TICKET.reg(_refresh); // XXX dont refresh all?
refresh();
};
@ -177,10 +214,24 @@ define([
APP.privateKey = privateData.supportPrivateKey;
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.module = common.makeUniversal('support');
APP.module = common.makeUniversal('support', {
onEvent: (obj) => {
let cmd = obj.ev;
let data = obj.data;
if (!events[cmd]) { return; }
events[cmd].fire(data);
}
});
APP.support = Support.create(common, true);
andThen();
let active = privateData.category || 'active';
let linkedTicket;
if (active.indexOf('-') !== -1) {
linkedTicket = active.split('-')[1];
active = active.split('-')[0];
}
andThen(linkedTicket);
UI.removeLoadingScreen();
});

View File

@ -16,20 +16,6 @@ define([
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
}).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);
});
sframeChan.on('Q_UPDATE_LIMIT', function (data, cb) {
Cryptpad.updatePinLimit(function (e) {
cb({error: e});
});
});
};
var category;
if (window.location.hash) {
category = window.location.hash.slice(1);
@ -40,7 +26,6 @@ define([
};
SFCommonO.start({
noRealtime: true,
addRpc: addRpc,
addData: addData
});
});

View File

@ -152,6 +152,9 @@ define([
return $div;
};
var events = {
'UPDATE_TICKET': Util.mkEvent()
};
create['listnew'] = function () {
var key = 'listnew';
var $div = makeBlock(key); // Msg.support_listHint, .support_listTitle
@ -159,6 +162,8 @@ define([
var $list = $(list);
let activeForm = {}; // .channel and .form
let refresh = function () {
const onClose = function (ticket, channel, data) {
APP.supportModule.execCommand('CLOSE_TICKET', {
@ -176,7 +181,8 @@ define([
ticket: formData
}, function (obj) {
if (obj && obj.error) { return void UI.warn(Messages.error); }
refresh(); // XXX RE-open this ticket and scroll to?
$(ticket).find('.cp-support-form-container').remove();
refresh();
});
};
@ -185,6 +191,15 @@ define([
return void UI.warn(Messages.error);
}
if (!Array.isArray(obj.tickets)) { return void UI.warn(Messages.error); }
// Recover forms
let activeForms = {};
$list.find('.cp-support-form-container').each((i, el) => {
let id = $(el).attr('data-id');
if (!id) { return; }
activeForms[id] = el;
});
$list.empty();
obj.tickets.forEach((data) => {
var title = data.title;
@ -192,7 +207,12 @@ define([
var messages = data.messages;
var first = messages[0];
first.id = data.id;
var ticket = APP.support.makeTicket(data.id, data, null, onClose, onReply);
var ticket = APP.support.makeTicket({
id: data.id,
content: data,
form: activeForms[data.id],
onClose, onReply
});
$list.append(ticket);
messages.forEach(msg => {
$(ticket).append(APP.support.makeMessage(msg));
@ -205,6 +225,8 @@ define([
Util.onClickEnter($(button), function () {
refresh();
});
let _refresh = Util.throttle(refresh, 500);;
events.UPDATE_TICKET.reg(_refresh);
refresh();
$div.append([
button,
@ -409,7 +431,14 @@ define([
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, false, APP.pinUsage, APP.teamsUsage);
APP.supportModule = common.makeUniversal('support');
APP.supportModule = common.makeUniversal('support', {
onEvent: (obj) => {
let cmd = obj.ev;
let data = obj.data;
if (!events[cmd]) { return; }
events[cmd].fire(data);
}
});
// Content
var $rightside = APP.$rightside;

View File

@ -187,7 +187,7 @@ define([
abuse: Pages.customURLs.terms,
};
var makeForm = function (ctx, cb, title, hideNotice) {
var makeForm = function (ctx, oldData, cb, title, hideNotice) {
var button;
if (typeof(cb) === "function") {
@ -253,7 +253,7 @@ define([
cb ? undefined : h('br'),
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessage
}),
}, (oldData && oldData.message) || ''),
h('label', Messages.support_attachments),
attachments = h('div.cp-support-attachments'),
addAttachment = h('button.btn', Messages.support_addAttachment),
@ -262,6 +262,32 @@ define([
cancel
];
var _addAttachment = (name, href) => {
var x, a;
var span = h('span', {
'data-name': name,
'data-href': href
}, [
x = h('i.fa.fa-times'),
a = h('a', {
href: '#'
}, name)
]);
$(x).click(function () {
$(span).remove();
});
$(a).click(function (e) {
e.preventDefault();
ctx.common.openURL(href);
});
$(attachments).append(span);
};
if (oldData && Array.isArray(oldData.attachments)) {
oldData.attachments.forEach((data) => {
_addAttachment(data.name, data.href);
});
}
$(addAttachment).click(function () {
var $input = $('<input>', {
'type': 'file',
@ -273,25 +299,7 @@ define([
files.forEach(function (file) {
var ev = {};
ev.callback = function (data) {
var x, a;
var span = h('span', {
'data-name': data.name,
'data-href': data.url
}, [
x = h('i.fa.fa-times'),
a = h('a', {
href: '#'
}, data.name)
]);
$(x).click(function () {
$(span).remove();
});
$(a).click(function (e) {
e.preventDefault();
ctx.common.openURL(data.url);
});
$(attachments).append(span);
_addAttachment(data.name, data.url);
};
// The empty object allows us to bypass the file upload modal
ctx.FM.handleFile(file, ev, {});
@ -310,7 +318,8 @@ define([
return form;
};
var makeTicket = function (ctx, id, content, onShow, onClose, onReply) {
var makeTicket = function (ctx, opts) {
let { id, content, form, onShow, onHide, onClose, onReply, onForm } = opts;
var common = ctx.common;
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
@ -321,10 +330,10 @@ define([
var adminActions;
var adminClasses = '';
var adminOpen;
if (ctx.isAdmin) {
// Admin custom style
let isPremium = content.premium ? '.cp-support-premium' : '';
adminClasses = `.cp-not-loaded${isPremium}`;
adminClasses = `.cp-not-loaded`;
// Admin actions
let show = h('button.btn.btn-primary.cp-support-expand', Messages.admin_support_open);
let $show = $(show);
@ -335,31 +344,49 @@ define([
]);
$(url).click(function (e) {
e.stopPropagation();
var link = privateData.origin + privateData.pathname + '#' + 'support-' + id;
var link = privateData.origin + privateData.pathname + '#' + 'active-' + id;
Clipboard.copy(link, (err) => {
if (!err) { UI.log(Messages.shareSuccess); }
});
});
Util.onClickEnter($show, function () {
$ticket.removeClass('cp-not-loaded');
$show.remove();
onShow(ticket, id, content);
});
let visible = false;
adminOpen = function (force) {
$show.prop('disabled', 'disabled');
if (visible && !force) {
$ticket.toggleClass('cp-not-loaded', true);
return onHide(ticket, id, content, function () {
visible = false;
$show.text(Messages.admin_support_open);
$show.prop('disabled', '');
});
}
$ticket.toggleClass('cp-not-loaded', false);
onShow(ticket, id, content, function () {
visible = true;
$show.text(Messages.admin_support_collapse);
$show.prop('disabled', '');
});
};
Util.onClickEnter($show, adminOpen);
adminActions = h('span.cp-support-title-buttons', [ url, show ])
}
let isPremium = content.premium ? '.cp-support-ispremium' : '';
var name = Util.fixHTML(content.author) || Messages.anonymous;
var ticket = h(`div.cp-support-list-ticket${adminClasses}`, {
'data-id': id
}, [
h('div.cp-support-ticket-header', [
h('span', content.title),
ctx.isAdmin ? UI.setHTML(h('span'), Messages._getKey('support_from', [name])) : '',
ctx.isAdmin ? UI.setHTML(h(`span${isPremium}`), Messages._getKey('support_from', [name])) : '',
h('span', new Date(content.time).toLocaleString()),
adminActions,
]),
actions
]);
ticket.open = adminOpen;
// Add button handlers
var $ticket = $(ticket);
@ -369,17 +396,23 @@ define([
$(close).remove();
onClose(ticket, id, content);
});
$(answer).click(function () {
var addForm = function () {
$ticket.find('.cp-support-form-container').remove();
$(actions).hide();
var form = makeForm(ctx, function () {
onReply(ticket, id, content, form, function () {
var oldData = form ? getFormData(ctx, form) : {};
form = undefined;
var newForm = makeForm(ctx, oldData, function () {
onReply(ticket, id, content, newForm, function () {
$(actions).css('display', '');
$(form).remove();
});
}, content.title, true);
$ticket.append(form);
});
$(newForm).attr('data-id', id);
$ticket.append(newForm);
};
if (form) { addForm(); }
$(answer).click(addForm);
return ticket;
};
@ -446,7 +479,7 @@ define([
$pre.text(displayed);
var adminClass = (fromAdmin? '.cp-support-fromadmin': '');
var premiumClass = (fromPremium && !fromAdmin? '.cp-support-frompremium': '');
var premiumClass = (ctx.isAdmin && fromPremium && !fromAdmin? '.cp-support-frompremium': '');
var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
return h('div.cp-support-list-message' + adminClass + premiumClass, {
'data-hash': hash
@ -507,13 +540,13 @@ define([
return getFormData(ctx, form);
};
ui.makeForm = function (cb, title, hideNotice) {
return makeForm(ctx, cb, title, hideNotice);
return makeForm(ctx, {}, cb, title, hideNotice);
};
ui.makeCategoryDropdown = function (container, onChange, all) {
return makeCategoryDropdown(ctx, container, onChange, all);
};
ui.makeTicket = function (id, content, onShow, onClose, onReply) {
return makeTicket(ctx, id, content, onShow, onClose, onReply);
ui.makeTicket = function (opts) {
return makeTicket(ctx, opts);
};
ui.makeMessage = function (content, hash) {
return makeMessage(ctx, content, hash);