Merge remote-tracking branch 'origin/support' into notifications

This commit is contained in:
ClemDee
2019-06-25 16:44:55 +02:00
14 changed files with 562 additions and 11 deletions

View File

@@ -17,6 +17,6 @@ define(function () {
// Sub
plan: 'CryptPad_plan',
// Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'notifications']
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications']
};
});

View File

@@ -85,6 +85,11 @@ define([
return id;
};
Hash.getChannelIdFromKey = function (publicKey) {
if (!publicKey) { return; }
return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16));
};
Hash.createRandomHash = function (type, password) {
var cryptor;
if (type === 'file') {

View File

@@ -478,6 +478,7 @@ define([
settings: store.proxy.settings,
thumbnails: disableThumbnails === false,
isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])),
support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']),
pendingFriends: store.proxy.friends_pending || {}
}
};

View File

@@ -10,6 +10,7 @@ define([
var TYPES = [
'notifications',
'support'
];
var BLOCKING_TYPES = [
];
@@ -25,6 +26,16 @@ define([
if (res.error) { console.error(res); }
});
}
if (!mailboxes['support']) {
mailboxes.support = {
channel: Hash.createChannelId(),
lastKnownHash: '',
viewed: []
};
ctx.pinPads([mailboxes.support.channel], function (res) {
if (res.error) { console.error(res); }
});
}
};
/*
@@ -76,15 +87,30 @@ proxy.mailboxes = {
var crypto = Crypto.Mailbox.createEncryptor(keys);
var network = ctx.store.network;
var ciphertext = crypto.encrypt(JSON.stringify({
var text = JSON.stringify({
type: type,
content: msg
}), user.curvePublic);
});
var ciphertext = crypto.encrypt(text, user.curvePublic);
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)
var box;
if (Object.keys(ctx.boxes).some(function (t) {
var _box = ctx.boxes[t];
if (_box.channel === user.channel) {
box = _box;
return true;
}
})) {
var hash = ciphertext.slice(0, 64);
box.onMessage(text, null, null, null, hash, user.curvePublic);
}
});
}, function (err) {
cb({error: err});
@@ -157,6 +183,7 @@ proxy.mailboxes = {
var openChannel = function (ctx, type, m, onReady) {
var box = ctx.boxes[type] = {
channel: m.channel,
queue: [], // Store the messages to send when the channel is ready
history: [], // All the hashes loaded from the server in corretc order
content: {}, // Content of the messages that should be displayed
@@ -213,7 +240,7 @@ proxy.mailboxes = {
});
box.queue = [];
};
cfg.onMessage = function (msg, user, vKey, isCp, hash, author) {
box.onMessage = cfg.onMessage = function (msg, user, vKey, isCp, hash, author) {
if (hash === m.lastKnownHash) { return; }
try {
msg = JSON.parse(msg);

View File

@@ -90,8 +90,11 @@ define([
var pushMessage = function (data, handler) {
var todo = function (f) {
try {
var el = createElement(data);
Notifications.add(Common, data, el);
var el;
if (data.type === 'notifications') {
el = createElement(data);
Notifications.add(Common, data, el);
}
f(data, el);
} catch (e) {
console.error(e);
@@ -108,7 +111,9 @@ define([
onViewedHandlers.forEach(function (f) {
try {
f(data);
Notifications.remove(Common, data);
if (data.type === 'notifications') {
Notifications.remove(Common, data);
}
} catch (e) {
console.error(e);
}
@@ -139,18 +144,25 @@ define([
var subscribed = false;
// Get all existing notifications + the new ones when they come
mailbox.subscribe = function (cfg) {
mailbox.subscribe = function (types, cfg) {
if (!subscribed) {
execCommand('SUBSCRIBE', null, function () {});
subscribed = true;
}
if (typeof(cfg.onViewed) === "function") {
onViewedHandlers.push(cfg.onViewed);
onViewedHandlers.push(function (data) {
if (types.indexOf(data.type) === -1) { return; }
cfg.onViewed(data);
});
}
if (typeof(cfg.onMessage) === "function") {
onMessageHandlers.push(cfg.onMessage);
onMessageHandlers.push(function (data, el) {
if (types.indexOf(data.type) === -1) { return; }
cfg.onMessage(data, el);
});
}
Object.keys(history).forEach(function (type) {
if (types.indexOf(type) === -1) { return; }
history[type].forEach(function (data) {
pushMessage({
type: type,

View File

@@ -983,7 +983,7 @@ MessengerUI, Messages) {
$button.addClass('fa-bell');
};
Common.mailbox.subscribe({
Common.mailbox.subscribe(['notifications'], {
onMessage: function (data, el) {
if (el) {
div.appendChild(el);

View File

@@ -0,0 +1,19 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
&.cp-app-support {
.framework_min_main(
@bg-color: @colortheme_support-bg,
@warn-color: @colortheme_support-warn,
@color: @colortheme_support-color
);
.sidebar-layout_main();
.cp-hidden {
display: none !important;
}
display: flex;
flex-flow: column;
}

12
www/support/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

18
www/support/inner.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/support/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body class="cp-app-support">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>
</html>

370
www/support/inner.js Normal file
View File

@@ -0,0 +1,370 @@
define([
'jquery',
'/common/toolbar3.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-hash.js',
'/customize/messages.js',
'/common/hyperscript.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',
'less!/support/app-support.less',
], function (
$,
Toolbar,
nThen,
SFCommon,
UI,
UIElements,
Util,
Hash,
Messages,
h,
ApiConfig,
Feedback
)
{
var saveAs = window.saveAs;
var APP = window.APP = {};
var common;
var metadataMgr;
var privateData;
var sframeChan;
var categories = {
'tickets': [
'cp-support-list',
],
'new': [
'cp-support-form',
],
};
var supportKey = ApiConfig.supportMailbox; // XXX curvePublic
var supportChannel = Hash.getChannelIdFromKey(supportKey); // XXX
if (true || !supportKey || !supportChannel) {
categories = {
'tickets': [
'cp-support-disabled'
]
};
}
var create = {};
var makeBlock = function (key, addButton) {
// Convert to camlCase for translation keys
var safeKey = key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
var $div = $('<div>', {'class': 'cp-support-' + key + ' cp-sidebarlayout-element'});
$('<label>').text(Messages['support_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages['support_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
if (addButton) {
$('<button>', {
'class': 'btn btn-primary'
}).text(Messages['support_'+safeKey+'Button'] || safeKey).appendTo($div);
}
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)
]);
};
// Register to the "support" mailbox
common.mailbox.subscribe(['support'], {
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;
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
// TODO: add a "closed" class to the ticket in the UI
}
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.append(makeMessage(content, hash));
},
onViewed: function (data) {
// Remove the ticket with this hash
// If the ticket div is empty, remove the ticket div
}
});
return $div;
};
// Create a new tickets
create['form'] = function () {
var key = 'form';
var $div = makeBlock(key, true);
var form = makeForm();
$div.find('button').before(form);
var id = Util.uid();
$div.find('button').click(function () {
var sent = sendForm(id, form);
if (sent) {
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
}
});
return $div;
};
// Support is disabled...
create['disabled'] = function () {
var key = 'disabled';
var $div = makeBlock(key);
// XXX add text
return $div;
};
var hideCategories = function () {
APP.$rightside.find('> div').hide();
};
var showCategories = function (cat) {
hideCategories();
cat.forEach(function (c) {
APP.$rightside.find('.'+c).show();
});
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'})
.appendTo(APP.$leftside);
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var active = privateData.category || 'tickets';
common.setHash(active);
Object.keys(categories).forEach(function (key) {
var $category = $('<div>', {
'class': 'cp-sidebarlayout-category',
'data-category': key
}).appendTo($categories);
if (key === 'tickets') { $category.append($('<span>', {'class': 'fa fa-envelope-o'})); }
if (key === 'new') { $category.append($('<span>', {'class': 'fa fa-life-ring'})); }
if (key === active) {
$category.addClass('cp-leftside-active');
}
$category.click(function () {
if (!Array.isArray(categories[key]) && categories[key].onClick) {
categories[key].onClick();
return;
}
active = key;
common.setHash(key);
$categories.find('.cp-leftside-active').removeClass('cp-leftside-active');
$category.addClass('cp-leftside-active');
showCategories(categories[key]);
});
$category.append(Messages['support_cat_'+key] || key);
});
showCategories(categories[active]);
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
displayed: displayed,
sfCommon: common,
$container: APP.$toolbar,
pageTitle: Messages.supportPage || 'Support', // XXX
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');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();
metadataMgr = common.getMetadataMgr();
privateData = metadataMgr.getPrivateData();
common.setTabTitle(Messages.supportPage || 'Support');
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
// Content
var $rightside = APP.$rightside;
var addItem = function (cssClass) {
var item = cssClass.slice(11); // remove 'cp-support-'
if (typeof (create[item]) === "function") {
$rightside.append(create[item]());
}
};
for (var cat in categories) {
if (!Array.isArray(categories[cat])) { continue; }
categories[cat].forEach(addItem);
}
createLeftside();
UI.removeLoadingScreen();
});
});

55
www/support/main.js Normal file
View File

@@ -0,0 +1,55 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/support/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
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);
window.location.hash = '';
}
var addData = function (obj) {
if (category) { obj.category = category; }
};
SFCommonO.start({
noRealtime: true,
addRpc: addRpc,
addData: addData
});
});
});