Merge branch 'support' of github.com:xwiki-labs/cryptpad into notifications

This commit is contained in:
ClemDee
2019-06-27 17:14:55 +02:00
16 changed files with 507 additions and 160 deletions

View File

@@ -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;

View File

@@ -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 = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories);
if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-user-o'})); }
if (key === 'stats') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
if (key === 'support') { $category.append($('<span>', {'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;

View File

@@ -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);
});

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 //////////////////////////////////////

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -123,6 +123,7 @@ define([
var onMessage = function (data) {
// data = { type: 'type', content: {msg: 'msg', hash: 'hash'} }
console.log(data.type, data.content);
pushMessage(data);
if (!history[data.type]) { history[data.type] = []; }
history[data.type].push(data.content);

View File

@@ -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;

View File

@@ -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;
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);
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
$ticket.append(makeMessage(content, hash));
},
onViewed: function (data) {
// Remove the ticket with this hash
// If the ticket div is empty, remove the ticket div
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, 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 = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
sFrameChan = common.getSframeChannel();
var sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();

View File

@@ -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
});
});

203
www/support/ui.js Normal file
View File

@@ -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
};
});