mirror of https://github.com/xwiki-labs/cryptpad
1004 lines
40 KiB
JavaScript
1004 lines
40 KiB
JavaScript
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||
//
|
||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
define([
|
||
'jquery',
|
||
'/api/config',
|
||
'/customize/application_config.js',
|
||
'/common/toolbar.js',
|
||
'/components/nthen/index.js',
|
||
'/common/sframe-common.js',
|
||
'/common/hyperscript.js',
|
||
'/customize/messages.js',
|
||
'/common/common-interface.js',
|
||
'/common/common-ui-elements.js',
|
||
'/common/common-util.js',
|
||
'/common/common-hash.js',
|
||
'/common/inner/sidebar-layout.js',
|
||
'/support/ui.js',
|
||
|
||
'/components/file-saver/FileSaver.min.js',
|
||
|
||
'css!/components/components-font-awesome/css/font-awesome.min.css',
|
||
'less!/moderation/app-moderation.less',
|
||
], function (
|
||
$,
|
||
ApiConfig,
|
||
AppConfig,
|
||
Toolbar,
|
||
nThen,
|
||
SFCommon,
|
||
h,
|
||
Messages,
|
||
UI,
|
||
UIElements,
|
||
Util,
|
||
Hash,
|
||
Sidebar,
|
||
Support
|
||
)
|
||
{
|
||
var APP = {};
|
||
var saveAs = window.saveAs;
|
||
|
||
var common;
|
||
var sframeChan;
|
||
var events = {
|
||
NEW_TICKET: Util.mkEvent(),
|
||
UPDATE_TICKET: Util.mkEvent(),
|
||
UPDATE_RIGHTS: Util.mkEvent(),
|
||
RECORDED_CHANGE: Util.mkEvent(),
|
||
REFRESH_FILTER: Util.mkEvent(),
|
||
REFRESH_TAGS: Util.mkEvent()
|
||
};
|
||
|
||
|
||
var andThen = function (common, $container, linkedTicket) {
|
||
const sidebar = Sidebar.create(common, 'support', $container);
|
||
const blocks = sidebar.blocks;
|
||
APP.recorded = {};
|
||
APP.allTags = [];
|
||
APP.openTicketCategory = Util.mkEvent();
|
||
|
||
var sortTicket = tickets => (c1, c2) => {
|
||
return tickets[c2].time - tickets[c1].time;
|
||
};
|
||
const onShowTicket = function (ticket, channel, data, done) {
|
||
APP.module.execCommand('LOAD_TICKET_ADMIN', {
|
||
channel: channel,
|
||
curvePublic: data.authorKey,
|
||
supportKey: data.supportKey
|
||
}, function (obj) {
|
||
if (!Array.isArray(obj)) {
|
||
console.error(obj && obj.error);
|
||
done(false);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
var $ticket = $(ticket);
|
||
obj.forEach(function (msg) {
|
||
// Only add notifications channel if this is coming from the other user
|
||
if (!data.notifications && msg.sender.drive) {
|
||
data.notifications = Util.find(msg, ['sender', 'notifications']);
|
||
}
|
||
if (msg.close) {
|
||
$ticket.addClass('cp-support-list-closed');
|
||
return $ticket.append(APP.support.makeCloseMessage(msg));
|
||
}
|
||
if (msg.legacy && msg.messages) {
|
||
msg.messages.forEach(c => {
|
||
$ticket.append(APP.support.makeMessage(c));
|
||
});
|
||
return;
|
||
}
|
||
$ticket.append(APP.support.makeMessage(msg));
|
||
});
|
||
done(true);
|
||
});
|
||
};
|
||
|
||
// Support panel functions
|
||
let open = [];
|
||
let refreshAll = function () {
|
||
APP.$refreshButton.prop('disabled', false);
|
||
};
|
||
let refresh = ($container, type, _cb) => {
|
||
let cb = Util.mkAsync(_cb || function () {});
|
||
APP.module.execCommand('LIST_TICKETS_ADMIN', {
|
||
type: type
|
||
}, (tickets) => {
|
||
if (tickets.error) {
|
||
cb();
|
||
if (tickets.error === 'EFORBIDDEN') {
|
||
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
|
||
}
|
||
return void UI.errorLoadingScreen(tickets.error);
|
||
}
|
||
open = open.filter(chan => {
|
||
// Remove deleted tickets from memory
|
||
return tickets[chan];
|
||
});
|
||
UI.removeLoadingScreen();
|
||
|
||
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),
|
||
h('span.cp-support-count'),
|
||
]));
|
||
var col2 = h('div.cp-support-column', h('h1', [
|
||
h('span', Messages.admin_support_normal),
|
||
h('span.cp-support-count'),
|
||
]));
|
||
var col3 = h('div.cp-support-column', h('h1', [
|
||
h('span', Messages.admin_support_answered),
|
||
h('span.cp-support-count'),
|
||
]));
|
||
var col4 = h('div.cp-support-column', h('h1', [
|
||
h('span', Messages.admin_support_closed),
|
||
h('span.cp-support-count'),
|
||
]));
|
||
var col5 = h('div.cp-support-column', h('h1', [
|
||
h('span', Messages.support_pending),
|
||
h('span.cp-support-count'),
|
||
]));
|
||
if (type === 'closed') {
|
||
// Only one column
|
||
col1 = col2 = col3 = col4;
|
||
}
|
||
if (type === 'pending') {
|
||
// Only one column
|
||
col1 = col2 = col3 = col5;
|
||
}
|
||
$container.append([col1, col2, col3]);
|
||
|
||
const onShow = function (ticket, channel, data, done) {
|
||
onShowTicket(ticket, channel, data, (success) => {
|
||
if (success) {
|
||
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 onReply = function (ticket, channel, data, form) {
|
||
var formData = APP.support.getFormData(form);
|
||
APP.module.execCommand('REPLY_TICKET_ADMIN', {
|
||
channel: channel,
|
||
curvePublic: data.authorKey,
|
||
notifChannel: data.notifications,
|
||
supportKey: data.supportKey,
|
||
ticket: formData
|
||
}, function (obj) {
|
||
if (obj && obj.error) {
|
||
console.error(obj && obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
$(ticket).find('.cp-support-list-message').remove();
|
||
$(ticket).find('.cp-support-form-container').remove();
|
||
refresh($container, type);
|
||
});
|
||
};
|
||
const onClose = function (ticket, channel, data) {
|
||
APP.module.execCommand('CLOSE_TICKET_ADMIN', {
|
||
channel: channel,
|
||
curvePublic: data.authorKey,
|
||
notifChannel: data.notifications,
|
||
supportKey: data.supportKey,
|
||
ticket: APP.support.getDebuggingData({
|
||
close: true
|
||
})
|
||
}, function (obj) {
|
||
if (obj && obj.error) {
|
||
console.error(obj && obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
refreshAll();
|
||
});
|
||
};
|
||
const onMove = function (ticket, channel) {
|
||
APP.module.execCommand('MOVE_TICKET_ADMIN', {
|
||
channel: channel,
|
||
from: type,
|
||
to: onMove.isTicketActive ? 'pending' : 'active'
|
||
}, function (obj) {
|
||
if (obj && obj.error) {
|
||
console.error(obj && obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
refreshAll();
|
||
});
|
||
};
|
||
onMove.disableMove = type === 'closed';
|
||
onMove.isTicketActive = type === 'active';
|
||
|
||
const onTag = (channel, tags) => {
|
||
APP.module.execCommand('SET_TAGS_ADMIN', {
|
||
channel, tags
|
||
}, function (obj) {
|
||
if (obj && obj.error) {
|
||
console.error(obj && obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
if (obj.allTags) { APP.allTags = obj.allTags; }
|
||
events.REFRESH_TAGS.fire();
|
||
});
|
||
};
|
||
onTag.getAllTags = () => {
|
||
return APP.allTags || [];
|
||
};
|
||
|
||
// Show tickets, reload the previously open ones and cal back
|
||
// once everything is loaded
|
||
let n = nThen;
|
||
Object.keys(tickets).sort(sortTicket(tickets)).forEach(function (channel) {
|
||
// Update allTags
|
||
var d = tickets[channel];
|
||
(d.tags || []).forEach(tag => {
|
||
if (!APP.allTags.includes(tag)) { APP.allTags.push(tag); }
|
||
});
|
||
// Make ticket
|
||
var ticket = APP.support.makeTicket({
|
||
id: channel,
|
||
content: d,
|
||
form: activeForms[channel],
|
||
recorded: APP.recorded,
|
||
onShow, onHide, onClose, onReply, onMove, onTag
|
||
});
|
||
|
||
var container;
|
||
if (d.lastAdmin) { container = col3; }
|
||
else if (d.premium) { container = col1; }
|
||
else { container = col2; }
|
||
$(container).append(ticket);
|
||
|
||
if (open.includes(channel)) {
|
||
n = n(waitFor => {
|
||
ticket.open(true, waitFor());
|
||
}).nThen;
|
||
}
|
||
});
|
||
// Wait for all open tickets to be loaded before calling back
|
||
// otherwise we may have a wrong scroll position
|
||
n(() => {
|
||
cb();
|
||
});
|
||
});
|
||
};
|
||
let onFilter = () => {
|
||
let tags = APP.filterTags || [];
|
||
APP.module.execCommand('FILTER_TAGS_ADMIN', { tags }, function (obj) {
|
||
if (!obj || obj.error) { return; }
|
||
$container.find('.cp-support-list-ticket').toggleClass('cp-filtered', false);
|
||
if (obj.all || !obj.tickets || !obj.tickets.length) { return; }
|
||
obj.tickets.forEach(id => {
|
||
$container.find(`.cp-support-list-ticket[data-id="${id}"]`)
|
||
.toggleClass('cp-filtered', true);
|
||
});
|
||
});
|
||
};
|
||
|
||
let activeContainer, pendingContainer, closedContainer;
|
||
refreshAll = function () {
|
||
let $rightside = sidebar.$rightside;
|
||
let s = $rightside.scrollTop();
|
||
nThen(waitFor => {
|
||
APP.module.execCommand('GET_RECORDED', {}, waitFor(function (obj) {
|
||
if (obj && obj.error) {
|
||
APP.recorded = {};
|
||
return;
|
||
}
|
||
APP.recorded = {
|
||
all: obj.messages,
|
||
onClick: id => {
|
||
APP.module.execCommand('USE_RECORDED', {id}, () => {});
|
||
}
|
||
};
|
||
}));
|
||
}).nThen(waitFor => {
|
||
APP.allTags = [];
|
||
refresh($(activeContainer), 'active', waitFor());
|
||
refresh($(pendingContainer), 'pending', waitFor());
|
||
refresh($(closedContainer), 'closed', waitFor());
|
||
}).nThen(() => {
|
||
onFilter();
|
||
events.REFRESH_TAGS.fire();
|
||
}).nThen(waitFor => {
|
||
APP.$refreshButton.prop('disabled', false);
|
||
if (!linkedTicket) { return; }
|
||
let $ticket = $container.find(`[data-link-id="${linkedTicket}"]`);
|
||
linkedTicket = undefined;
|
||
if ($ticket.length) {
|
||
let ticket = $ticket[0];
|
||
if (typeof(ticket.open) === "function") {
|
||
waitFor.abort();
|
||
ticket.open(true, () => {
|
||
ticket.scrollIntoView();
|
||
});
|
||
}
|
||
}
|
||
}).nThen(() => {
|
||
$rightside.scrollTop(s);
|
||
});
|
||
};
|
||
let _refresh = Util.throttle(refreshAll, 500);
|
||
events.NEW_TICKET.reg(_refresh);
|
||
events.UPDATE_TICKET.reg(_refresh);
|
||
events.RECORDED_CHANGE.reg(_refresh);
|
||
events.UPDATE_RIGHTS.reg(_refresh);
|
||
events.REFRESH_FILTER.reg(onFilter);
|
||
|
||
// Make sidebar layout
|
||
const categories = {
|
||
'open': { // Msg.support_cat_open
|
||
icon: 'fa fa-inbox',
|
||
content: [
|
||
'refresh',
|
||
'filter',
|
||
'active-list',
|
||
'pending-list',
|
||
]
|
||
},
|
||
'closed': { // Msg.support_cat_closed
|
||
icon: 'fa fa-archive',
|
||
content: [
|
||
'refresh',
|
||
'filter',
|
||
'closed-list'
|
||
]
|
||
},
|
||
'search': { // Msg.support_cat_search
|
||
icon: 'fa fa-search',
|
||
content: [
|
||
'filter',
|
||
'search'
|
||
],
|
||
onOpen: () => {
|
||
APP.searchAutoRefresh = true;
|
||
setTimeout(() => {
|
||
$('.cp-support-search-input').focus();
|
||
});
|
||
}
|
||
},
|
||
'new': { // Msg.support_cat_new
|
||
icon: 'fa fa-envelope',
|
||
content: [
|
||
'open-ticket'
|
||
],
|
||
onOpen: () => {
|
||
APP.openTicketCategory.fire();
|
||
setTimeout(() => {
|
||
$('.cp-support-newticket-paste').focus();
|
||
});
|
||
}
|
||
},
|
||
'legacy': { // Msg.support_cat_legacy
|
||
icon: 'fa fa-server',
|
||
content: [
|
||
'legacy'
|
||
]
|
||
},
|
||
'settings': { // Msg.support_cat_settings
|
||
icon: 'fa fa-cogs',
|
||
content: [
|
||
'privacy',
|
||
'notifications',
|
||
'recorded'
|
||
],
|
||
onOpen: () => {
|
||
setTimeout(() => {
|
||
$('.cp-moderation-recorded-id').focus();
|
||
});
|
||
}
|
||
},
|
||
};
|
||
|
||
if (!APP.privateKey) { delete categories.legacy; }
|
||
|
||
sidebar.addItem('refresh', cb => {
|
||
let button = blocks.button('secondary', 'fa-refresh', Messages.oo_refresh);
|
||
APP.$refreshButton = $(button);
|
||
Util.onClickEnter($(button), () => {
|
||
APP.$refreshButton.prop('disabled', 'disabled');
|
||
refreshAll();
|
||
});
|
||
let content = blocks.block([button]);
|
||
cb(content);
|
||
}, { noTitle: true, noHint: true });
|
||
|
||
// Msg.support_privacyHint.support_privacyTitle
|
||
sidebar.addCheckboxItem({
|
||
key: 'privacy',
|
||
getState: () => false,
|
||
query: (val, setState) => {
|
||
APP.support.setAnonymous(val);
|
||
setState(val);
|
||
}
|
||
});
|
||
sidebar.addItem('active-list', cb => {
|
||
activeContainer = h('div.cp-support-container'); // XXX block
|
||
cb(activeContainer);
|
||
}, { noTitle: true, noHint: true });
|
||
sidebar.addItem('pending-list', cb => {
|
||
pendingContainer = h('div.cp-support-container');
|
||
cb(pendingContainer);
|
||
}, { noTitle: true, noHint: true });
|
||
sidebar.addItem('closed-list', cb => {
|
||
closedContainer = h('div.cp-support-container');
|
||
cb(closedContainer);
|
||
}, { noTitle: true, noHint: true });
|
||
refreshAll();
|
||
|
||
// Msg.support_notificationsHint.support_notificationsTitle.support_notificationsLabel
|
||
sidebar.addCheckboxItem({
|
||
key: 'notifications',
|
||
getState: () => APP.disableSupportNotif,
|
||
query: (val, setState) => {
|
||
common.setAttribute(['general', 'disableSupportNotif'], val, function (err) {
|
||
if (err) { val = APP.disableSupportNotif; }
|
||
APP.disableSupportNotif = val;
|
||
setState(val);
|
||
});
|
||
}
|
||
});
|
||
|
||
sidebar.addItem('search', cb => {
|
||
|
||
let inputSearch = blocks.input({type:'text', class: 'cp-support-search-input'});
|
||
let button = blocks.button('primary', 'fa-search');
|
||
let inputBlock = blocks.inputButton(inputSearch, button, { onEnterDelegate: true });
|
||
let searchBlock = blocks.labelledInput(Messages.support_searchLabel,
|
||
inputSearch, inputBlock);
|
||
|
||
let list = blocks.block([], 'cp-support-container');
|
||
let container = blocks.block([searchBlock, list], 'cp-support-search-container');
|
||
let $list = $(list);
|
||
let searchText = '';
|
||
APP.searchAutoRefresh = false;
|
||
|
||
let redraw = (_cb) => {
|
||
let cb = _cb || function () {};
|
||
$list.empty();
|
||
let tags = APP.filterTags || [];
|
||
let text = searchText;
|
||
if (!text.length && !tags.length) { return void cb(); }
|
||
APP.module.execCommand('SEARCH_ADMIN', { text, tags }, function (obj) {
|
||
cb();
|
||
if (obj && obj.error) {
|
||
console.error(obj && obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
$list.empty();
|
||
let tickets = obj.tickets || {};
|
||
|
||
const onShow = onShowTicket;
|
||
const onHide = function (ticket, channel, data, done) {
|
||
$(ticket).find('.cp-support-list-message').remove();
|
||
done();
|
||
};
|
||
const onTag = () => {};
|
||
onTag.readOnly = true;
|
||
onTag.getAllTags = () => [];
|
||
Object.keys(tickets).sort(sortTicket(tickets)).forEach(id => {
|
||
let content = tickets[id];
|
||
content.tags = content.tags || [];
|
||
|
||
let catTag = Messages[`support_${content.category}_tag`];
|
||
if (catTag) {
|
||
// Msg.support_active_tag.support_pending_tag.support_closed_tag
|
||
content.tags.unshift(catTag.toUpperCase());
|
||
}
|
||
|
||
var ticket = APP.support.makeTicket({
|
||
id,
|
||
content,
|
||
onTag, onShow, onHide
|
||
});
|
||
$list.append(ticket);
|
||
});
|
||
});
|
||
};
|
||
|
||
let $input = $(inputSearch);
|
||
let $button = $(button);
|
||
Util.onClickEnter($button, function () {
|
||
$button.prop('disabled', 'disabled');
|
||
searchText = $input.val().trim();
|
||
redraw(() => {
|
||
APP.searchAutoRefresh = true;
|
||
$button.prop('disabled', false);
|
||
});
|
||
});
|
||
|
||
events.REFRESH_FILTER.reg(() => {
|
||
if (!APP.searchAutoRefresh) { return; }
|
||
redraw();
|
||
});
|
||
cb(container);
|
||
}, { noTitle: true, noHint: true });
|
||
|
||
sidebar.addItem('filter', cb => {
|
||
let container = blocks.block([], 'cp-support-filter-container');
|
||
let $container = $(container);
|
||
let redrawTags = () => {
|
||
$container.empty();
|
||
var existing = APP.allTags;
|
||
var list = h('div.cp-tags-list');
|
||
var reset = h('button.btn.btn-cancel.cp-tags-filter-reset', [
|
||
h('i.fa.fa-times'),
|
||
Messages.kanban_clearFilter
|
||
]);
|
||
var hint = h('span', Messages.kanban_tags);
|
||
var tags = h('div.cp-tags-filter', [
|
||
h('span.cp-tags-filter-toggle', [
|
||
hint,
|
||
reset,
|
||
]),
|
||
list,
|
||
]);
|
||
var $reset = $(reset);
|
||
var $list = $(list);
|
||
var $hint = $(hint);
|
||
var setTagFilterState = function (bool) {
|
||
$hint.css('visibility', bool? 'hidden': 'visible');
|
||
$reset.css('visibility', bool? 'visible': 'hidden');
|
||
};
|
||
|
||
var getTags = function () {
|
||
return $list.find('span.active').map(function () {
|
||
return String($(this).data('tag'));
|
||
}).get();
|
||
};
|
||
var commitTags = function () {
|
||
var t = getTags();
|
||
setTagFilterState(t.length);
|
||
APP.filterTags = t;
|
||
events.REFRESH_FILTER.fire();
|
||
};
|
||
APP.filterTags = (APP.filterTags || []).filter(tag => {
|
||
return existing.includes(tag);
|
||
});
|
||
|
||
var redrawList = function (allTags) {
|
||
if (!Array.isArray(allTags) || !allTags.length) {
|
||
setTimeout(() => {
|
||
$list.closest('.cp-sidebarlayout-element')
|
||
.toggleClass('cp-sidebar-force-hide', true);
|
||
});
|
||
return;
|
||
}
|
||
setTimeout(() => {
|
||
$list.closest('.cp-sidebarlayout-element')
|
||
.toggleClass('cp-sidebar-force-hide', false);
|
||
});
|
||
$list.empty();
|
||
$list.removeClass('cp-empty');
|
||
if (!allTags.length) {
|
||
$list.addClass('cp-empty');
|
||
$list.append(h('em', Messages.kanban_noTags));
|
||
return;
|
||
}
|
||
allTags.forEach(function (t) {
|
||
let active = APP.filterTags.includes(t) ? '.active' : '';
|
||
var $tag = $(h('span'+active, {'data-tag':t}, t)).appendTo($list);
|
||
Util.onClickEnter($tag, function () {
|
||
$tag.toggleClass('active');
|
||
commitTags();
|
||
});
|
||
});
|
||
};
|
||
redrawList(existing);
|
||
commitTags();
|
||
|
||
Util.onClickEnter($reset, function () {
|
||
$list.find('span').removeClass('active');
|
||
commitTags();
|
||
});
|
||
|
||
$container.append(tags);
|
||
};
|
||
events.REFRESH_TAGS.reg(redrawTags);
|
||
cb(container);
|
||
}, { noTitle: true, noHint: true });
|
||
|
||
// Msg.support_recordedHint.support_recordedTitle
|
||
sidebar.addItem('recorded', cb => {
|
||
let empty = blocks.inline(Messages.support_recordedEmpty);
|
||
let list = blocks.block([], 'cp-moderation-recorded-list');
|
||
let inputId = blocks.input({type:'text', class: 'cp-moderation-recorded-id',
|
||
maxlength: 20 });
|
||
let inputContent = blocks.textarea();
|
||
let labelId = blocks.labelledInput(Messages.support_recordedId, inputId);
|
||
let labelContent = blocks.labelledInput(Messages.support_recordedContent, inputContent);
|
||
|
||
let create = blocks.button('primary', 'fa-plus', Messages.tag_add);
|
||
let nav = blocks.nav([create]);
|
||
|
||
let form = blocks.form([
|
||
empty,
|
||
list,
|
||
labelId,
|
||
labelContent,
|
||
], nav);
|
||
|
||
let $empty = $(empty);
|
||
let $list = $(list).hide();
|
||
let $create = $(create);
|
||
let $inputId = $(inputId).on('input', () => {
|
||
let val = $inputId.val().toLowerCase().replace(/ /g, '-').replace(/[^a-z-_]/g, '');
|
||
$inputId.val(val);
|
||
});
|
||
|
||
let refresh = function () {};
|
||
let edit = (id, content, remove) => {
|
||
APP.module.execCommand('SET_RECORDED', {id, content, remove}, function (obj) {
|
||
$create.removeAttr('disabled');
|
||
if (obj && obj.error) {
|
||
console.error(obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
$(inputId).val('');
|
||
$(inputContent).val('');
|
||
events.RECORDED_CHANGE.fire();
|
||
});
|
||
};
|
||
refresh = () => {
|
||
APP.module.execCommand('GET_RECORDED', {}, function (obj) {
|
||
if (obj && obj.error) {
|
||
console.error(obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
let messages = obj.messages;
|
||
$list.empty();
|
||
Object.keys(messages).forEach(id => {
|
||
let del = blocks.button('danger-alt', 'fa-trash-o', Messages.kanban_delete);
|
||
Util.onClickEnter($(del), () => {
|
||
edit(id, '', true);
|
||
});
|
||
$list.append(h('div.cp-moderation-recorded', [
|
||
h('span.cp-moderation-recorded-header', id),
|
||
h('div.cp-moderation-recorded-body', [
|
||
h('div.cp-moderation-recorded-content', messages[id].content),
|
||
h('nav', del)
|
||
])
|
||
]));
|
||
});
|
||
if (!Object.keys(messages).length) {
|
||
$list.hide();
|
||
$empty.show();
|
||
return;
|
||
}
|
||
$list.show();
|
||
$empty.hide();
|
||
});
|
||
};
|
||
|
||
Util.onClickEnter($create, function () {
|
||
$create.attr('disabled', 'disabled');
|
||
let id = $(inputId).val().trim();
|
||
let content = $(inputContent).val().trim();
|
||
edit(id, content, false);
|
||
});
|
||
|
||
events.RECORDED_CHANGE.reg(refresh);
|
||
|
||
refresh();
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.support_openTicketHint.support_openTicketTitle
|
||
sidebar.addItem('open-ticket', cb => {
|
||
let form = APP.support.makeForm({});
|
||
|
||
let updateRecorded = () => {
|
||
APP.module.execCommand('GET_RECORDED', {}, function (obj) {
|
||
if (obj && obj.error) { return; }
|
||
form.updateRecorded({
|
||
all: obj.messages,
|
||
onClick: id => {
|
||
APP.module.execCommand('USE_RECORDED', {id}, () => {});
|
||
}
|
||
});
|
||
});
|
||
};
|
||
events.RECORDED_CHANGE.reg(updateRecorded);
|
||
APP.openTicketCategory.reg(updateRecorded);
|
||
|
||
let inputName = blocks.input({type: 'text', readonly: true});
|
||
let inputChan = blocks.input({type: 'text', readonly: true});
|
||
let inputKey = blocks.input({type: 'text', readonly: true});
|
||
let labelName = blocks.labelledInput(Messages.login_username, inputName);
|
||
let labelChan = blocks.labelledInput(Messages.support_userChannel, inputChan);
|
||
let labelKey = blocks.labelledInput(Messages.support_userKey, inputKey);
|
||
|
||
let send = blocks.button('primary', 'fa-paper-plane', Messages.support_formButton);
|
||
let nav = blocks.nav([send]);
|
||
|
||
let reset = blocks.button('danger-alt', 'fa-times', Messages.form_reset);
|
||
|
||
let paste = blocks.textarea({
|
||
class: 'cp-support-newticket-paste',
|
||
placeholder: Messages.support_pasteUserData
|
||
});
|
||
let inputs = h('div.cp-moderation-userdata-inputs', [ labelName, labelChan, labelKey ]);
|
||
let userData = h('div.cp-moderation-userdata', [inputs , paste, reset]);
|
||
|
||
let $reset = $(reset).hide();
|
||
let $paste = $(paste).on('input', () => {
|
||
let text = $paste.val().trim();
|
||
let parsed = Util.tryParse(text);
|
||
$paste.val('');
|
||
if (!parsed || !parsed.name || !parsed.notifications || !parsed.curvePublic) {
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
$(inputName).val(parsed.name);
|
||
$(inputChan).val(parsed.notifications);
|
||
$(inputKey).val(parsed.curvePublic);
|
||
$paste.hide();
|
||
$reset.show();
|
||
});
|
||
Util.onClickEnter($reset, () => {
|
||
$(inputName).val('');
|
||
$(inputChan).val('');
|
||
$(inputKey).val('');
|
||
$reset.hide();
|
||
$paste.show();
|
||
setTimeout(() => { $paste.focus(); });
|
||
});
|
||
[inputName, inputChan, inputKey].forEach(input => {
|
||
$(input).on('input', () => { $paste.show(); });
|
||
});
|
||
|
||
let $send = $(send);
|
||
Util.onClickEnter($send, function () {
|
||
let name = $(inputName).val().trim();
|
||
let chan = $(inputChan).val().trim();
|
||
let key = $(inputKey).val().trim();
|
||
let data = APP.support.getFormData(form);
|
||
|
||
if (!name) { return void UI.warn(Messages.login_invalUser); }
|
||
if (!Hash.isValidChannel(chan)) { return void UI.warn(Messages.support_invalChan); }
|
||
if (key.length !== 44) { return void UI.warn(Messages.admin_invalKey); }
|
||
|
||
$send.attr('disabled', 'disabled');
|
||
APP.module.execCommand('MAKE_TICKET_ADMIN', {
|
||
name: name,
|
||
notifications: chan,
|
||
curvePublic: key,
|
||
channel: Hash.createChannelId(),
|
||
title: data.title,
|
||
ticket: data
|
||
}, function (obj) {
|
||
if (obj && obj.error) {
|
||
console.error(obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
refreshAll();
|
||
sidebar.openCategory('open');
|
||
});
|
||
});
|
||
|
||
let div = blocks.form([userData, form], nav);
|
||
cb(div);
|
||
});
|
||
|
||
// Msg.support_legacyHint.support_legacyTitle
|
||
sidebar.addItem('legacy', cb => {
|
||
if (!APP.privateKey) { return void cb(false); }
|
||
|
||
let start = blocks.button('primary', 'fa-paper-plane', Messages.support_legacyButton);
|
||
let dump = blocks.button('secondary', 'fa-database', Messages.support_legacyDump);
|
||
let clean = blocks.button('danger', 'fa-trash-o', Messages.support_legacyClear);
|
||
let content = h('div.cp-support-container');
|
||
let nav = blocks.nav([start, dump, clean]);
|
||
let spinner = UI.makeSpinner($(nav));
|
||
|
||
let sortLegacyTickets = contentByHash => {
|
||
let all = {};
|
||
Object.keys(contentByHash).forEach(key => {
|
||
let data = contentByHash[key];
|
||
let content = data.content;
|
||
let id = content.id;
|
||
content.hash = key;
|
||
if (data.ctime) { content.time = data.ctime; }
|
||
if (content.sender && content.sender.curvePublic !== data.author) { return; }
|
||
all[id] = all[id] || [];
|
||
all[id].push(content);
|
||
all[id].sort((c1, c2) => {
|
||
return c1.time - c2.time;
|
||
});
|
||
});
|
||
// sort
|
||
let sorted = Object.keys(all).sort((t1, t2) => {
|
||
let a = t1[0];
|
||
let b = t2[0];
|
||
return (a.time || 0) - (b.time || 0);
|
||
});
|
||
return sorted.map(id => {
|
||
return all[id];
|
||
});
|
||
};
|
||
let $dumpBtn = $(dump);
|
||
UI.confirmButton(dump, { classes: 'btn-secondary' }, function () {
|
||
spinner.spin();
|
||
$dumpBtn.prop('disabled', 'disabled').blur();
|
||
APP.module.execCommand('DUMP_LEGACY', {}, contentByHash => {
|
||
$dumpBtn.prop('disabled', false);
|
||
spinner.done();
|
||
// group by ticket id
|
||
let sorted = sortLegacyTickets(contentByHash);
|
||
let dump = '';
|
||
sorted.forEach((t,i) => {
|
||
if (!Array.isArray(t) || !t.length) { return; }
|
||
let first = t[0];
|
||
if (i) { dump += '\n\n'; }
|
||
dump += `================================
|
||
================================
|
||
ID: #${first.id}
|
||
Title: ${first.title}
|
||
User: ${first.sender.name}
|
||
Date: ${new Date(first.time).toISOString()}`;
|
||
t.forEach(msg => {
|
||
if (!msg.message) {
|
||
dump += `
|
||
--------------------------------
|
||
CLOSED: ${new Date(msg.time).toISOString()}`;
|
||
return;
|
||
}
|
||
dump += `
|
||
--------------------------------
|
||
From: ${msg.sender.name}
|
||
Date: ${new Date(msg.time).toISOString()}
|
||
---
|
||
${msg.message}
|
||
---
|
||
Attachments:${JSON.stringify(msg.attachments, 0, 2)}`;
|
||
});
|
||
});
|
||
saveAs(new Blob([dump], {type: 'text/plain'}), "cryptpad-support-dump.txt");
|
||
});
|
||
});
|
||
UI.confirmButton(clean, { classes: 'btn-danger' }, function () {
|
||
APP.module.execCommand('CLEAR_LEGACY', {}, () => {
|
||
delete APP.privateKey;
|
||
sidebar.deleteCategory('legacy');
|
||
sidebar.openCategory('open');
|
||
});
|
||
});
|
||
let run = () => {
|
||
let $div = $(content);
|
||
$div.empty();
|
||
spinner.spin();
|
||
$(start).prop('disabled', 'disabled').blur();
|
||
APP.module.execCommand('GET_LEGACY', {}, contentByHash => {
|
||
$(start).prop('disabled', false);
|
||
spinner.done();
|
||
// group by ticket id
|
||
let sorted = sortLegacyTickets(contentByHash);
|
||
sorted.forEach(ticket => {
|
||
if (!Array.isArray(ticket) || !ticket.length) { return; }
|
||
ticket.forEach(content => {
|
||
var id = content.id;
|
||
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
|
||
|
||
if (!content.message) {
|
||
// A ticket has been closed by the admins...
|
||
if (!$ticket.length) { return; }
|
||
$ticket.hide();
|
||
$ticket.append(APP.support.makeCloseMessage(content));
|
||
return;
|
||
}
|
||
$ticket.show();
|
||
|
||
const onMove = function () {
|
||
let hashes = [];
|
||
let messages = [];
|
||
ticket.forEach(content => {
|
||
hashes.push(content.hash);
|
||
let clone = Util.clone(content);
|
||
delete clone.hash;
|
||
messages.push(clone);
|
||
});
|
||
APP.module.execCommand('RESTORE_LEGACY', {
|
||
messages, hashes
|
||
}, obj => {
|
||
if (obj && obj.error) {
|
||
console.error(obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
$ticket.remove();
|
||
});
|
||
};
|
||
if (!$ticket.length) {
|
||
content.category = 'legacy'; // Hide invalid features
|
||
$ticket = $(APP.support.makeTicket({id, content, onMove}));
|
||
$div.append($ticket);
|
||
}
|
||
$ticket.append(APP.support.makeMessage(content));
|
||
});
|
||
});
|
||
});
|
||
};
|
||
Util.onClickEnter($(start), run);
|
||
|
||
|
||
|
||
let div = blocks.form([content], nav);
|
||
cb(div);
|
||
});
|
||
|
||
sidebar.makeLeftside(categories);
|
||
};
|
||
|
||
var createToolbar = function () {
|
||
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
|
||
var configTb = {
|
||
displayed: displayed,
|
||
sfCommon: common,
|
||
$container: APP.$toolbar,
|
||
pageTitle: Messages.moderationPage,
|
||
metadataMgr: common.getMetadataMgr(),
|
||
};
|
||
APP.toolbar = Toolbar.create(configTb);
|
||
APP.toolbar.$rightside.hide();
|
||
};
|
||
|
||
nThen(function (waitFor) {
|
||
$(waitFor(UI.addLoadingScreen));
|
||
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
|
||
}).nThen(function (waitFor) {
|
||
APP.$container = $('#cp-sidebarlayout-container');
|
||
APP.$toolbar = $('#cp-toolbar');
|
||
sframeChan = common.getSframeChannel();
|
||
sframeChan.onReady(waitFor());
|
||
}).nThen(function (waitFor) {
|
||
common.getAttribute(['general', 'disableSupportNotif'], waitFor(function (err, value) {
|
||
APP.disableSupportNotif = !!value;
|
||
}));
|
||
}).nThen(function (/*waitFor*/) {
|
||
createToolbar();
|
||
var metadataMgr = common.getMetadataMgr();
|
||
var privateData = metadataMgr.getPrivateData();
|
||
common.setTabTitle(Messages.moderationPage);
|
||
|
||
if (!ApiConfig.supportMailboxKey) {
|
||
return void UI.errorLoadingScreen(Messages.support_disabledTitle);
|
||
}
|
||
|
||
APP.privateKey = privateData.supportPrivateKey;
|
||
APP.origin = privateData.origin;
|
||
APP.readOnly = privateData.readOnly;
|
||
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);
|
||
|
||
let active = privateData.category || 'active';
|
||
let linkedTicket;
|
||
if (active.indexOf('-') !== -1) {
|
||
linkedTicket = active.slice(active.indexOf('-')+1);
|
||
active = active.split('-')[0];
|
||
}
|
||
|
||
andThen(common, APP.$container, linkedTicket);
|
||
UI.removeLoadingScreen();
|
||
|
||
});
|
||
});
|