Merge branch 'broadcast' into calendar

This commit is contained in:
yflory 2021-04-01 18:15:53 +02:00
commit d853466ad2
29 changed files with 1284 additions and 102 deletions

View file

@ -64,6 +64,10 @@
color: @cryptpad_text_col;
}
.cp-admin-message {
color: @cryptpad_text_col;
}
.cp-inline-alert-text {
flex: 1;
}

View file

@ -414,3 +414,8 @@
@cp_whiteboard-board-border: @cryptpad_color_grey_800;
@cp_whiteboard-bg: @cp_app-bg;
@cp_whiteboard-fg: @cryptpad_text_col;
// Flatpickr
@cp_flatpickr-bg: @cryptpad_color_grey_800;
@cp_flatpickr-highlight: @cryptpad_color_brand_300;
@cp_flatpickr-highlight-text: @cryptpad_color_grey_800;

View file

@ -190,7 +190,7 @@
// Dropdown
@cp_dropdown-fg: @cryptpad_text_col;
@cp_dropdown-bg: @cryptpad_color_grey_100;
@cp_dropdown-bg-hover: @cryptpad_color_grey_100;
@cp_dropdown-bg-hover: @cryptpad_color_grey_200;
@cp_dropdown-bg-active: @cryptpad_color_grey_300;
// Rendered Markdown
@ -414,3 +414,8 @@
@cp_whiteboard-board-border: @cryptpad_color_grey_600;
@cp_whiteboard-bg: @cp_app-bg;
@cp_whiteboard-fg: @cryptpad_text_col;
// Flatpickr
@cp_flatpickr-bg: @cryptpad_color_grey_50;
@cp_flatpickr-highlight: @cryptpad_color_brand_fadest;
@cp_flatpickr-highlight-text: @cryptpad_text_col;

View file

@ -8,7 +8,7 @@
& {
@alertify_padding-base: @variables_padding;
input:not(.form-control):not([type="checkbox"]), textarea, div.cp-textarea {
input:not(.numInput):not(.form-control):not([type="checkbox"]), textarea, div.cp-textarea {
// background-color: @alertify-input-fg;
color: @cp_forms-fg;
background-color: @cp_forms-bg;
@ -169,7 +169,7 @@
&:hover, &:not(:disabled):active, &:focus {
border-color: @cryptpad_text_col;
color: @cryptpad_text_col;
background-color: fade(@cryptpad_text_col, 25%);
background-color: fade(@cryptpad_text_col, 10%);
}
}
@ -280,4 +280,90 @@
color: @cp_drive-infobox-fg;
}
}
// Flatpickr
body {
.flatpickr-calendar {
background: @cp_flatpickr-bg;
color: @cryptpad_text_col;
border-radius: 0;
box-shadow: @variables_shadow;
-webkit-box-shadow: @variables_shadow;
&.arrowTop::before, &.arrowTop::after {
border-bottom: 0;
}
.flatpickr-months {
.flatpickr-month, .flatpickr-months, .flatpickr-next-month, .flatpickr-prev-month {
color: @cryptpad_text_col;
fill: @cryptpad_text_col;
&:hover {
svg {
fill: @cryptpad_text_col;
}
}
}
.flatpickr-current-month {
span.cur-month:hover {
background: fade(@cryptpad_text_col, 10%);
}
.numInputWrapper span.arrowUp:after {
border-bottom-color: @cryptpad_text_col;
}
.numInputWrapper span.arrowDown:after {
border-top-color: @cryptpad_text_col;
}
}
}
.flatpickr-innerContainer {
border-bottom: 0;
.flatpickr-weekdays {
span.flatpickr-weekday {
color: @cryptpad_text_col;
}
}
.flatpickr-days {
border-left: 0;
border-right: 0;
.flatpickr-day {
color: @cryptpad_text_col;
&:hover {
background-color: fade(@cryptpad_text_col, 10%);
border: 0;
}
&.selected {
background: @cp_flatpickr-highlight;
color: @cp_flatpickr-highlight-text;
border: 0;
}
}
.flatpickr-disabled {
color: fade(@cryptpad_text_col, 20%);
}
}
}
.flatpickr-time {
border-top: none;
.flatpickr-time-separator, .flatpickr-am-pm {
color: @cryptpad_text_col;
}
.flatpickr-am-pm {
&:hover {
background-color: fade(@cryptpad_text_col, 10%);
}
}
.numInputWrapper {
.numInput, .arrowUp, .arrowDown {
color: @cryptpad_text_col;
&:hover, &:focus {
background-color: fade(@cryptpad_text_col, 10%);
}
}
span.arrowDown::after, span.arrowUp::after {
border-top-color: @cryptpad_text_col;
border-bottom-color: @cryptpad_text_col;
}
}
}
}
}
}

View file

@ -17,6 +17,16 @@
.cp-notification {
min-height: @notif-height;
display: flex;
.cp-broadcast {
display: flex;
font-size: 30px;
align-items: center;
padding: 0 5px;
color: @cp_dropdown-fg;
&.preview {
color: @cryptpad_color_red;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;

View file

@ -400,7 +400,7 @@
button {
.toolbar_button;
&.cp-notifications-bell {
&.cp-notifications-bell, &.cp-maintenance-wrench {
color: @cryptpad_text_col;
}
}
@ -506,7 +506,7 @@
}
.cp-toolbar-user {
height: @toolbar_line-height;
.cp-toolbar-notifications {
.cp-toolbar-notifications, .cp-toolbar-maintenance {
height: @toolbar_line-height;
width: @toolbar_line-height;
margin-left: 0;
@ -709,7 +709,7 @@
height: 43px;
}
}
.cp-toolbar-link, .cp-toolbar-notifications {
.cp-toolbar-link, .cp-toolbar-notifications, .cp-toolbar-maintenance {
line-height: @toolbar_top-height;
width: @toolbar_top-height;
height: @toolbar_top-height;
@ -717,7 +717,7 @@
box-sizing: border-box;
display: inline-block;
}
.cp-toolbar-notifications {
.cp-toolbar-notifications, .cp-toolbar-maintenance {
text-align: center;
font-size: 32px;
margin-left: 10px;

View file

@ -191,7 +191,8 @@ var ARRAY_LINE = /^\[/;
*/
Channel.isNewChannel = function (Env, channel, cb) {
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== 32) { return void cb('INVALID_CHAN'); }
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH &&
channel.length !== HK.ADMIN_CHANNEL_LENGTH) { return void cb('INVALID_CHAN'); }
// TODO replace with readMessagesBin
var done = false;
@ -229,7 +230,8 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
if (!msg) { return void cb("INVALID_MESSAGE"); }
// don't support anything except regular channels
if (!Core.isValidId(channelId) || channelId.length !== 32) {
if (!Core.isValidId(channelId) || (channelId.length !== HK.STANDARD_CHANNEL_LENGTH
&& channelId.length !== HK.ADMIN_CHANNEL_LENGTH)) {
return void cb("INVALID_CHAN");
}
@ -254,6 +256,11 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
var session = HK.getNetfluxSession(Env, netfluxId);
var allowed = HK.listAllowedUsers(metadata);
// Special broadcast channel
if (channelId.length === HK.ADMIN_CHANNEL_LENGTH) {
allowed = Env.admins;
}
if (HK.isUserSessionAllowed(allowed, session)) { return; }
w.abort();
@ -278,12 +285,19 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
// historyKeeper already knows how to handle metadata and message validation, so we just pass it off here
// if the message isn't valid it won't be stored.
Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage);
Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage, function (err) {
if (err) {
// Message not stored...
return void cb(err);
}
Server.getChannelUserList(channelId).forEach(function (userId) {
Server.send(userId, fullMessage);
// Broadcast the message
Server.getChannelUserList(channelId).forEach(function (userId) {
Server.send(userId, fullMessage);
});
});
cb();
});
};

View file

@ -10,7 +10,7 @@ Core.SESSION_EXPIRATION_TIME = 60 * 1000;
Core.isValidId = function (chan) {
return chan && chan.length && /^[a-zA-Z0-9=+-]*$/.test(chan) &&
[32, 48].indexOf(chan.length) > -1;
[32, 33, 48].indexOf(chan.length) > -1;
};
var makeToken = Core.makeToken = function () {

View file

@ -9,7 +9,8 @@ const HK = require("../hk-util");
Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
const cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH) { return cb("INVALID_CHAN_LENGTH"); }
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH &&
channel.length !== HK.ADMIN_CHANNEL_LENGTH) { return cb("INVALID_CHAN_LENGTH"); }
var cached = Env.metadata_cache[channel];
if (HK.isMetadataMessage(cached)) {

View file

@ -24,6 +24,10 @@ SET_PREMIUM_UPLOAD_SIZE
DISABLE_INTEGRATED_TASKS
DISABLE_INTEGRATED_EVICTION
// BROADCAST
SET_LAST_BROADCAST_HASH
SET_SURVEY_URL
NOT IMPLEMENTED:
// RESTRICTED REGISTRATION
@ -121,6 +125,36 @@ commands.SET_ARCHIVE_RETENTION_TIME = makeIntegerSetter('archiveRetentionTime');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_ACCOUNT_RETENTION_TIME', [365]]], console.log)
commands.SET_ACCOUNT_RETENTION_TIME = makeIntegerSetter('accountRetentionTime');
var args_isString = function (args) {
return Array.isArray(args) && typeof(args[0]) === "string";
};
var args_isMaintenance = function (args) {
return Array.isArray(args) && args[0] && args[0].end && args[0].start;
};
var makeBroadcastSetter = function (attr) {
return function (Env, args) {
if (!args_isString(args) && !args_isMaintenance(args)) {
throw new Error('INVALID_ARGS');
}
var str = args[0];
if (str === Env[attr]) { return false; }
Env[attr] = str;
Env.broadcastCache = {};
return true;
};
};
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_LAST_BROADCAST_HASH', [hash]]], console.log)
commands.SET_LAST_BROADCAST_HASH = makeBroadcastSetter('lastBroadcastHash');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SURVEY_URL', [url]]], console.log)
commands.SET_SURVEY_URL = makeBroadcastSetter('surveyURL');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [{start: +Date, end: +Date}]]], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [""]]], console.log)
commands.SET_MAINTENANCE = makeBroadcastSetter('maintenance');
var Quota = require("./commands/quota");
var Keys = require("./keys");
var Util = require("./common-util");

View file

@ -19,8 +19,10 @@ module.exports.create = function (config) {
FRESH_MODE: true,
DEV_MODE: false,
configCache: {},
broadcastCache: {},
flushCache: function () {
Env.configCache = {};
Env.broadcastCache = {};
Env.FRESH_KEY = +new Date();
if (!(Env.DEV_MODE || Env.FRESH_MODE)) { Env.FRESH_MODE = true; }
if (!Env.Log) { return; }
@ -65,6 +67,11 @@ module.exports.create = function (config) {
paths: {},
//msgStore: config.store,
// /api/broadcast
lastBroadcastHash: '',
surveyURL: undefined,
maintenance: undefined,
netfluxUsers: {},
pinStore: undefined,

View file

@ -34,6 +34,7 @@ const getHash = HK.getHash = function (msg, Log) {
// historyKeeper should explicitly store any channel
// with a 32 character id
const STANDARD_CHANNEL_LENGTH = HK.STANDARD_CHANNEL_LENGTH = 32;
const ADMIN_CHANNEL_LENGTH = HK.ADMIN_CHANNEL_LENGTH = 33;
// historyKeeper should not store messages sent to any channel
// with a 34 character id
@ -902,6 +903,11 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
// don't store messages if the channel id indicates that it's an ephemeral message
if (!channel.id || channel.id.length === EPHEMERAL_CHANNEL_LENGTH) { return void cb(); }
// Admin channel. We can only write to this one from private message (RPC)
if (channel.id.length === ADMIN_CHANNEL_LENGTH && msgStruct[1] !== null) {
return void cb('ERESTRICTED_ADMIN');
}
const isCp = /^cp\|/.test(msgStruct[4]);
let id;
if (isCp) {

View file

@ -567,7 +567,7 @@ var listChannels = function (root, handler, cb, fast) {
var metadataName;
// if the current file is not the channel data, then it must be metadata
if (!/^[0-9a-fA-F]{32}\.ndjson$/.test(item)) {
if (!/^[0-9a-fA-F]{32, 33}\.ndjson$/.test(item)) {
metadataName = item;
channelName = item.replace(/\.metadata/, '');
@ -584,7 +584,7 @@ var listChannels = function (root, handler, cb, fast) {
}
var channel = metadataName.replace(/\.metadata.ndjson$/, '');
if ([32, 34, 44].indexOf(channel.length) === -1) { return; }
if ([32, 33, 34, 44].indexOf(channel.length) === -1) { return; }
// otherwise throw it on the pile
sema.take(function (give) {

View file

@ -391,7 +391,8 @@ const getPinState = function (data, cb) {
const _getFileSize = function (channel, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === 32) {
if (channel.length === HK.STANDARD_CHANNEL_LENGTH ||
channel.length === HK.ADMIN_CHANNEL_LENGTH) {
return void store.getChannelSize(channel, function (e, size) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, 0); }

View file

@ -113,6 +113,7 @@ var setHeaders = (function () {
// Don't set CSP headers on /api/config because they aren't necessary and they cause problems
// when duplicated by NGINX in production environments
// XXX /api/broadcast too?
if (/^\/api\/config/.test(req.url)) { return; }
// targeted CSP, generic policies, maybe custom headers
const h = [
@ -272,7 +273,59 @@ var serveConfig = (function () {
};
}());
var serveBroadcast = (function () {
var cacheString = function () {
return (Env.FRESH_KEY? '-' + Env.FRESH_KEY: '') + (Env.DEV_MODE? '-' + (+new Date()): '');
};
var template = function (host) {
var maintenance = Env.maintenance;
if (maintenance && maintenance.end && maintenance.end < (+new Date())) {
maintenance = undefined;
}
return [
'define(function(){',
'return ' + JSON.stringify({
lastBroadcastHash: Env.lastBroadcastHash,
surveyURL: Env.surveyURL,
maintenance: maintenance
}, null, '\t'),
'});'
].join(';\n')
};
var cleanUp = {};
return function (req, res) {
var host = req.headers.host.replace(/\:[0-9]+/, '');
res.setHeader('Content-Type', 'text/javascript');
// don't cache anything if you're in dev mode
if (Env.DEV_MODE) {
return void res.send(template(host));
}
// generate a lookup key for the cache
var cacheKey = host + ':' + cacheString();
// XXX do we need a cache for /api/broadcast?
if (!Env.broadcastCache[cacheKey]) {
// generate the response and cache it in memory
Env.broadcastCache[cacheKey] = template(host);
// and create a function to conditionally evict cache entries
// which have not been accessed in the last 20 seconds
cleanUp[cacheKey] = Util.throttle(function () {
delete cleanUp[cacheKey];
delete Env.broadcastCache[cacheKey];
}, 20000);
}
// successive calls to this function
cleanUp[cacheKey]();
return void res.send(Env.broadcastCache[cacheKey]);
};
}());
app.get('/api/config', serveConfig);
app.get('/api/broadcast', serveBroadcast);
var four04_path = Path.resolve(__dirname + '/customize.dist/404.html');
var custom_four04_path = Path.resolve(__dirname + '/customize/404.html');

View file

@ -15,7 +15,7 @@
display: flex;
flex-flow: column;
.cp-admin-setlimit-form {
.cp-admin-setlimit-form, .cp-admin-broadcast-form {
label {
font-weight: normal !important;
}
@ -199,5 +199,85 @@
}
}
.cp-admin-broadcast-form {
input.flatpickr-input {
width: 307.875px !important; // same width as flatpickr calendar
}
.cp-broadcast-active {
display: flex;
flex-flow: column;
align-items: start;
padding: 10px;
background-color: @cp_sidebar-left-bg;
color: @cp_sidebar-left-fg;
p {
margin: 0;
}
}
.cp-broadcast-form-submit {
margin-top: 30px;
button {
margin-bottom: 10px !important;
}
}
.cp-broadcast-container {
display: flex;
flex-flow: column;
}
.cp-broadcast-lang {
margin: 30px;
margin-bottom: 0;
display: flex;
flex-flow: column;
align-items: baseline;
.cp-checkmark {
margin: 5px 0;
}
}
div.cp-broadcast-languages {
& > label.cp-checkmark:not(:last-child) {
margin-right: 20px;
}
}
.cp-broadcast-preview {
vertical-align: bottom !important;
}
.cp-broadcast-delete {
width: 100%;
min-width: 600px;
tbody {
tr {
background-color: @cp_support-msg-bg;
padding: 5px;
td {
padding: 5px;
button {
margin: 0 !important;
}
}
}
}
.cp-notification {
display: flex;
align-items: center;
.cp-avatar, .cp-broadcast, .cp-notification-dismiss {
display: none;
}
p {
margin: 0 !important;
}
.cp-notification-content {
width: 100%;
padding: 10px;
}
.cp-clickable {
cursor: pointer;
&:hover {
background-color: @cp_dropdown-bg-hover;
}
}
}
}
}
}

View file

@ -1,6 +1,7 @@
define([
'jquery',
'/api/config',
'/customize/application_config.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar.js',
'/bower_components/nthen/index.js',
@ -14,12 +15,16 @@ define([
'/common/common-signing-keys.js',
'/support/ui.js',
'/lib/datepicker/flatpickr.js',
'css!/lib/datepicker/flatpickr.min.css',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/admin/app-admin.less',
], function (
$,
ApiConfig,
AppConfig,
Crypto,
Toolbar,
nThen,
@ -31,7 +36,8 @@ define([
Util,
Hash,
Keys,
Support
Support,
Flatpickr
)
{
var APP = {
@ -67,6 +73,11 @@ define([
'cp-admin-support-list',
'cp-admin-support-init'
],
'broadcast': [ // Msg.admin_cat_broadcast
'cp-admin-maintenance',
'cp-admin-survey',
'cp-admin-broadcast',
],
'performance': [ // Msg.admin_cat_performance
'cp-admin-refresh-performance',
'cp-admin-performance-profiling',
@ -930,6 +941,551 @@ define([
return;
};
Messages.admin_cat_broadcast = "Broadcast"; // XXX
Messages.admin_maintenanceTitle = "Maintenance"; // XXX
Messages.admin_maintenanceHint = "Plan, remove or update a maintenance. You can only have one active maintenance at a time."; // XXX
Messages.admin_maintenanceButton = "Plan maintenance"; // XXX
Messages.admin_maintenanceCancel = "Cancel planned maintenance"; // XXX
Messages.broadcast_start = 'Start time';
Messages.broadcast_end = 'End time';
Messages.admin_surveyTitle = "Survey"; // XXX
Messages.admin_surveyHint = "Add, update or remove the active survey accessible from the user menu"; // XXX
Messages.admin_surveyButton = "Apply survey"; // XXX
Messages.admin_surveyCancel = "Cancel active survey"; // XXX
Messages.admin_surveyActive = "View the active survey"; // XXX
Messages.broadcast_surveyURL = 'Survey URL';
Messages.admin_broadcastTitle = "Broadcast a message"; // XXX
Messages.admin_broadcastHint = "Send a message to all the existing and future users as a notification"; // XXX
Messages.admin_broadcastButton = "Send"; // XXX
Messages.admin_broadcastActive = "Active message"; // XXX
Messages.admin_broadcastCancel = "Delete active message"; // XXX
Messages.broadcast_translations = 'Translations';
Messages.broadcast_defaultLanguage = 'Fallback to this language';
Messages.broadcast_preview = "Preview in a fake notification";
var getApi = function (cb) {
return function () {
require(['/api/broadcast?'+ (+new Date())], function (Broadcast) {
cb(Broadcast);
});
};
};
// Update the lastBroadcastHash in /api/broadcast if we can do it.
// To do so, find the last "BROADCAST_CUSTOM" in the current history and use the previous
// message's hash.
// If the last BROADCAST_CUSTOM has been deleted by an admin, we can use the most recent
// message's hash.
var checkLastBroadcastHash = function () {
var deleted = [];
require(['/api/broadcast?'+ (+new Date())], function (BCast) {
var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
if (e) { return void console.error(e); }
// No history, nothing to change
if (!Array.isArray(msgs)) { return; }
if (!msgs.length) { return; }
var lastHash;
var next = false;
// Start from the most recent messages until you find a CUSTOM message and
// check if it has been deleted
msgs.reverse().some(function (data) {
var c = data.content;
// This is the hash we want to keep
if (next) {
if (!c || !c.hash) { return; }
lastHash = c.hash;
next = false;
return true;
}
// initialize with the most recent hash
if (!lastHash && c && c.hash) { lastHash = c.hash; }
var msg = c && c.msg;
if (!msg) { return; }
// Remember all deleted messages
if (msg.type === "BROADCAST_DELETE") {
deleted.push(Util.find(msg, ['content', 'uid']));
}
// Only check custom messages
if (msg.type !== "BROADCAST_CUSTOM") { return; }
// If the most recent CUSTOM message has been deleted, it means we don't
// need to keep any message and we can continue with lastHash as the most
// recent broadcast message.
if (deleted.indexOf(msg.uid) !== -1) { return true; }
// We just found the oldest message we want to keep, move one iteration
// further into the loop to get the next message's hash.
// If this is the end of the loop, don't bump lastBroadcastHash at all.
next = true;
});
// If we don't have to bump our lastBroadcastHash, abort
if (next) { return; }
// Otherwise, bump to lastHash
console.warn('Updating last broadcast hash to', lastHash);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['SET_LAST_BROADCAST_HASH', [lastHash]]
}, function (e) {
if (e) {
console.error(e);
return;
}
console.log('lastBroadcastHash updated');
});
});
});
};
create['broadcast'] = function () {
var key = 'broadcast';
var $div = makeBlock(key);
var form = h('div.cp-admin-broadcast-form');
var $form = $(form).appendTo($div);
var refresh = getApi(function (Broadcast) {
var button = h('button.btn.btn-primary', Messages.admin_broadcastButton);
var $button = $(button);
var removeButton = h('button.btn.btn-danger', Messages.admin_broadcastCancel);
var active = h('div.cp-broadcast-active', h('p', Messages.admin_broadcastActive));
var $active = $(active);
var activeUid;
var deleted = [];
// Render active message (if there is one)
require(['/api/broadcast?'+ (+new Date())], function (BCast) {
var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
if (e) { return void console.error(e); }
if (!Array.isArray(msgs)) { return; }
if (!msgs.length) {
$active.hide();
}
msgs.reverse().some(function (data) {
var c = data.content;
var msg = c && c.msg;
if (!msg) { return; }
if (msg.type === "BROADCAST_DELETE") {
deleted.push(Util.find(msg, ['content', 'uid']));
}
if (msg.type !== "BROADCAST_CUSTOM") { return; }
if (deleted.indexOf(msg.uid) !== -1) { return true; }
// We found an active custom message, show it
var el = common.mailbox.createElement(data);
var table = h('table.cp-broadcast-delete');
var $table = $(table);
var uid = Util.find(data, ['content', 'msg', 'uid']);
var time = Util.find(data, ['content', 'msg', 'content', 'time']);
var tr = h('tr', { 'data-uid': uid }, [
h('td', 'ID: '+uid),
h('td', new Date(time || 0).toLocaleString()),
h('td', el),
h('td.delete', removeButton),
]);
$table.append(tr);
$active.append(table);
activeUid = uid;
return true;
});
if (!activeUid) { $active.hide(); }
});
});
// Custom message
var container = h('div.cp-broadcast-container');
var $container = $(container);
var languages = Messages._languages;
var keys = Object.keys(languages).sort();
// Always keep the textarea ordered by language code
var reorder = function () {
$container.find('.cp-broadcast-lang').each(function (i, el) {
var $el = $(el);
var l = $el.attr('data-lang');
$el.css('order', keys.indexOf(l));
});
};
// Remove a textarea
var removeLang = function (l) {
$container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove();
var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length;
if (!hasDefault) {
$container.find('.cp-broadcast-lang').first().find('.cp-checkmark input').prop('checked', 'checked');
}
};
var getData = function () { return false; };
var onPreview = function (l) {
var data = getData();
if (data === false) { return void UI.warn(Messages.error); }
var msg = {
uid: Util.uid(),
type: 'BROADCAST_CUSTOM',
content: data
};
common.mailbox.onMessage({
lang: l,
type: 'broadcast',
content: {
msg: msg,
hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58)
}
}, function () {
UI.log(Messages.saved);
});
};
// Add a textarea
var addLang = function (l) {
if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; }
var preview = h('button.btn.btn-secondary', Messages.broadcast_preview);
$(preview).click(function () {
onPreview(l);
});
var bcastDefault = Messages.broadcast_defaultLanguage;
var first = !$container.find('.cp-broadcast-lang').length;
var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, {
'data-lang': l,
label: {class: 'noTitle'}
});
$container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [
h('h4', languages[l]),
h('label', Messages.kanban_body),
h('textarea'),
radio,
preview
]));
reorder();
};
// Checkboxes to select translations
var boxes = keys.map(function (l) {
var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l,
languages[l], false, { label: { class: 'noTitle' } }));
var $check = $cbox.find('input').on('change', function () {
var c = $check.is(':checked');
if (c) { return void addLang(l); }
removeLang(l);
});
if (l === 'en') {
setTimeout(function () {
$check.click();
});
}
return $cbox[0];
});
// Extract form data
getData = function () {
var map = {};
var defaultLanguage;
var error = false;
$container.find('.cp-broadcast-lang').each(function (i, el) {
var $el = $(el);
var l = $el.attr('data-lang');
if (!l) { error = true; return; }
var text = $el.find('textarea').val();
if (!text.trim()) { error = true; return; }
if ($el.find('.cp-checkmark input').is(':checked')) {
defaultLanguage = l;
}
map[l] = text;
});
if (!Object.keys(map).length) {
console.error('You must select at least one language');
return false;
}
if (error) {
console.error('One of the selected languages has no data');
return false;
}
return {
defaultLanguage: defaultLanguage,
content: map
};
};
var send = function (data) {
$button.prop('disabled', 'disabled');
data.time = +new Date();
common.mailbox.sendTo('BROADCAST_CUSTOM', data, {}, function (err, data) {
if (err) {
$button.prop('disabled', '');
console.error(err);
return UI.warn(Messages.error);
}
UI.log(Messages.saved);
refresh();
checkLastBroadcastHash();
});
};
$button.click(function () {
var data = getData();
if (data === false) { return void UI.warn(Messages.error); }
send(data);
});
UI.confirmButton(removeButton, {
classes: 'btn-danger',
}, function () {
if (!activeUid) { return; }
common.mailbox.sendTo('BROADCAST_DELETE', {
uid: activeUid
}, {}, function (err, data) {
if (err) { return UI.warn(Messages.error); }
UI.log(Messages.saved);
refresh();
checkLastBroadcastHash();
});
});
// Make the form
$form.empty().append([
active,
h('label', Messages.broadcast_translations),
h('div.cp-broadcast-languages', boxes),
container,
h('div.cp-broadcast-form-submit', [
h('br'),
button
])
]);
});
refresh();
return $div;
};
create['maintenance'] = function () {
var key = 'maintenance';
var $div = makeBlock(key);
var form = h('div.cp-admin-broadcast-form');
var $form = $(form).appendTo($div);
var refresh = getApi(function (Broadcast) {
var button = h('button.btn.btn-primary', Messages.admin_maintenanceButton);
var $button = $(button);
var removeButton = h('button.btn.btn-danger', Messages.admin_maintenanceCancel);
var active;
if (Broadcast && Broadcast.maintenance) {
var m = Broadcast.maintenance;
if (m.start && m.end && m.end >= (+new Date())) {
active = h('div.cp-broadcast-active', [
UI.setHTML(h('p'), Messages._getKey('broadcast_maintenance', [
new Date(m.start).toLocaleString(),
new Date(m.end).toLocaleString(),
])),
removeButton
]);
}
}
// Start and end date pickers
var start = h('input');
var end = h('input');
var $start = $(start);
var $end = $(end);
var endPickr = Flatpickr(end, {
enableTime: true,
minDate: new Date()
});
Flatpickr(start, {
enableTime: true,
minDate: new Date(),
onChange: function () {
endPickr.set('minDate', new Date($start.val()));
}
});
// Extract form data
var getData = function () {
var start = +new Date($start.val());
var end = +new Date($end.val());
if (isNaN(start) || isNaN(end)) {
console.error('Invalid dates');
return false;
}
return {
start: start,
end: end
};
};
var send = function (data) {
$button.prop('disabled', 'disabled');
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['SET_MAINTENANCE', [data]]
}, function (e) {
if (e) {
UI.warn(Messages.error); console.error(e);
$button.prop('disabled', '');
return;
}
// Maintenance applied, send notification
common.mailbox.sendTo('BROADCAST_MAINTENANCE', {}, {}, function (err, data) {
refresh();
checkLastBroadcastHash();
});
});
};
$button.click(function () {
var data = getData();
if (data === false) { return void UI.warn(Messages.error); }
send(data);
});
UI.confirmButton(removeButton, {
classes: 'btn-danger',
}, function () {
send("");
});
$form.empty().append([
active,
h('label', Messages.broadcast_start),
start,
h('label', Messages.broadcast_end),
end,
h('br'),
h('div.cp-broadcast-form-submit', [
button
])
]);
});
refresh();
common.makeUniversal('broadcast', {
onEvent: function (obj) {
var cmd = obj.ev;
if (cmd !== "MAINTENANCE") { return; }
refresh();
}
});
return $div;
};
create['survey'] = function () {
var key = 'survey';
var $div = makeBlock(key);
var form = h('div.cp-admin-broadcast-form');
var $form = $(form).appendTo($div);
var refresh = getApi(function (Broadcast) {
var button = h('button.btn.btn-primary', Messages.admin_surveyButton);
var $button = $(button);
var removeButton = h('button.btn.btn-danger', Messages.admin_surveyCancel);
var active;
if (Broadcast && Broadcast.surveyURL) {
var a = h('a', {href: Broadcast.surveyURL}, Messages.admin_surveyActive);
$(a).click(function (e) {
e.preventDefault();
common.openUnsafeURL(Broadcast.surveyURL);
});
active = h('div.cp-broadcast-active', [
h('p', a),
removeButton
]);
}
// Survey form
var label = h('label', Messages.broadcast_surveyURL);
var input = h('input');
var $input = $(input);
// Extract form data
var getData = function () {
var url = $input.val();
if (!Util.isValidURL(url)) {
console.error('Invalid URL');
return false;
}
return url;
};
var send = function (data) {
$button.prop('disabled', 'disabled');
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['SET_SURVEY_URL', [data]]
}, function (e) {
if (e) {
$button.prop('disabled', '');
UI.warn(Messages.error); console.error(e);
return;
}
// Maintenance applied, send notification
common.mailbox.sendTo('BROADCAST_SURVEY', {
url: data
}, {}, function (err, data) {
refresh();
checkLastBroadcastHash();
});
});
};
$button.click(function () {
var data = getData();
if (data === false) { return void UI.warn(Messages.error); }
send(data);
});
UI.confirmButton(removeButton, {
classes: 'btn-danger',
}, function () {
send("");
});
$form.empty().append([
active,
label,
input,
h('br'),
h('div.cp-broadcast-form-submit', [
button
])
]);
});
refresh();
common.makeUniversal('broadcast', {
onEvent: function (obj) {
var cmd = obj.ev;
if (cmd !== "SURVEY") { return; }
refresh();
}
});
return $div;
};
var onRefreshPerformance = Util.mkEvent();
create['refresh-performance'] = function () {
@ -1010,6 +1566,7 @@ define([
stats: 'fa fa-line-chart',
quota: 'fa fa-hdd-o',
support: 'fa fa-life-ring',
broadcast: 'fa fa-bullhorn',
performance: 'fa fa-heartbeat',
};
@ -1094,8 +1651,7 @@ define([
var privateData = metadataMgr.getPrivateData();
common.setTabTitle(Messages.adminPage || 'Administration');
if (!privateData.edPublic || !ApiConfig.adminKeys || !Array.isArray(ApiConfig.adminKeys)
|| ApiConfig.adminKeys.indexOf(privateData.edPublic) === -1) {
if (!common.isAdmin()) {
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
}

View file

@ -1,6 +1,7 @@
define([
'jquery',
'/api/config',
'/api/broadcast',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-language.js',
@ -17,7 +18,7 @@ define([
'/common/visible.js',
'css!/customize/fonts/cptools/style.css',
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard,
], function ($, Config, Broadcast, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard,
Messages, AppConfig, Pages, NThen, InviteInner, Visible) {
var UIElements = {};
var urlArgs = Config.requireConf.urlArgs;
@ -1332,7 +1333,7 @@ define([
// Button
var $button = $('<button>', {
'class': ''
'class': config.buttonCls || ''
}).append($('<span>', {'class': 'cp-dropdown-button-title'}).html(config.text || ""));
if (config.caretDown) {
$('<span>', {
@ -1762,19 +1763,20 @@ define([
});
}
if (AppConfig.surveyURL) {
options.push({
tag: 'a',
attributes: {
'class': 'cp-toolbar-survey fa fa-graduation-cap'
},
content: h('span', Messages.survey),
action: function () {
Common.openUnsafeURL(AppConfig.surveyURL);
Feedback.send('SURVEY_CLICKED');
},
});
}
// If you set "" in the admin panel, it will remove the AppConfig survey
var surveyURL = typeof(Broadcast.surveyURL) !== "undefined" ? Broadcast.surveyURL
: AppConfig.surveyURL;
options.push({
tag: 'a',
attributes: {
'class': 'cp-toolbar-survey fa fa-graduation-cap'
},
content: h('span', Messages.survey),
action: function () {
Common.openUnsafeURL(surveyURL);
Feedback.send('SURVEY_CLICKED');
},
});
options.push({ tag: 'hr' });
// Add login or logout button depending on the current status
@ -1841,6 +1843,24 @@ define([
};
var $userAdmin = UIElements.createDropdown(dropdownConfigUser);
var $survey = $userAdmin.find('.cp-toolbar-survey');
if (!surveyURL) { $survey.hide(); }
Common.makeUniversal('broadcast', {
onEvent: function (obj) {
var cmd = obj.ev;
if (cmd !== "SURVEY") { return; }
var url = obj.data;
if (url === surveyURL) { return; }
if (url && !Util.isValidURL(url)) { return; }
surveyURL = url;
if (!url) {
$survey.hide();
return;
}
$survey.show();
}
});
/*
// Uncomment these lines to have a language selector in the admin menu
// FIXME clicking on the inner menu hides the outer one

View file

@ -554,6 +554,16 @@
return false;
};
Util.isValidURL = function (str) {
var pattern = new RegExp('^(https?:\\/\\/)'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
return !!pattern.test(str);
};
var emoji_patt = /([\uD800-\uDBFF][\uDC00-\uDFFF])/;
var isEmoji = function (str) {
return emoji_patt.test(str);

View file

@ -406,10 +406,67 @@ define([
}
};
Messages.broadcast_newSurvey = "A new survey is available."; // XXX
handlers['BROADCAST_SURVEY'] = function (common, data) {
var content = data.content;
var msg = content.msg.content;
content.getFormatText = function () {
return Messages.broadcast_newSurvey;
};
content.handler = function () {
common.openUnsafeURL(msg.url);
// XXX dismiss on click?
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
Messages.broadcast_newCustom = "Message from the administrators"; // XXX
handlers['BROADCAST_CUSTOM'] = function (common, data) {
var content = data.content;
var msg = content.msg.content;
var text = msg.content;
var defaultL = msg.defaultLanguage;
var myLang = data.lang || Messages._languageUsed;
// Check if our language is available
var toShow = text[myLang];
// Otherwise, fallback to the default language if it exists
if (!toShow && defaultL) { toShow = text[defaultL]; }
// No translation available, dismiss
if (!toShow) { return defaultDismiss(common, data)(); }
var slice = toShow.length > 200;
toShow = Util.fixHTML(toShow);
content.getFormatText = function () {
// XXX Add a title to custom messages? Or use a generic key in the notification and only display the text in the alert?
if (slice) {
return toShow.slice(0, 200) + '...';
}
return toShow;
};
if (slice) {
content.handler = function () {
// XXX Allow markdown (sanitized)?
var content = h('div', [
h('h4', Messages.broadcast_newCustom),
h('div.cp-admin-message', toShow)
]);
UI.alert(content);
// XXX Dismiss on click?
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText"
return {
add: function(common, data) {
console.log(data);
var type = data.content.msg.type;
if (handlers[type]) {

View file

@ -626,6 +626,33 @@ define([
cb(JSON.parse(JSON.stringify(metadata)));
};
Store.onMaintenanceUpdate = function (uid) {
// use uid in /api/broadcast so that all connected users will use the same cached
// version on the server
require(['/api/broadcast?'+uid], function (Broadcast) {
broadcast([], 'UNIVERSAL_EVENT', {
type: 'broadcast',
data: {
ev: 'MAINTENANCE',
data: Broadcast.maintenance
}
});
});
};
Store.onSurveyUpdate = function (uid) {
// use uid in /api/broadcast so that all connected users will use the same cached
// version on the server
require(['/api/broadcast?'+uid], function (Broadcast) {
broadcast([], 'UNIVERSAL_EVENT', {
type: 'broadcast',
data: {
ev: 'SURVEY',
data: Broadcast.surveyURL
}
});
});
};
var makePad = function (href, roHref, title) {
var now = +new Date();
return {
@ -1513,7 +1540,6 @@ define([
execCommand: function (clientId, data, cb) {
// The mailbox can only be used when the store is ready
onReadyEvt.reg(function () {
if (!store.loggedIn) { return void cb(); }
if (!store.mailbox) { return void cb ({error: 'Mailbox is disabled'}); }
store.mailbox.execCommand(clientId, data, cb);
});
@ -2544,9 +2570,6 @@ define([
};
var loadMailbox = function (waitFor) {
if (!store.loggedIn || !store.proxy.edPublic) {
return;
}
store.mailbox = Mailbox.init({
Store: Store,
store: store,
@ -3133,6 +3156,9 @@ define([
});
};
Store.newVersionReload = function () {
broadcast([], "NETWORK_RECONNECT");
};
Store.disconnect = function () {
if (self.accountDeletion) { return; }
if (!store.network) { return; }

View file

@ -686,6 +686,55 @@ define([
};
// Broadcast
var broadcasts = {};
handlers['BROADCAST_MAINTENANCE'] = function (ctx, box, data, cb) {
var msg = data.msg;
var uid = msg.uid;
ctx.Store.onMaintenanceUpdate(uid);
cb(true);
};
var activeSurvey;
handlers['BROADCAST_SURVEY'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
var uid = msg.uid;
var old = activeSurvey;
activeSurvey = {
type: box.type,
hash: data.hash
};
ctx.Store.onSurveyUpdate(uid);
var dismiss = !content.url;
cb(dismiss, old);
};
var activeCustom
handlers['BROADCAST_CUSTOM'] = function (ctx, box, data, cb) {
var msg = data.msg;
var uid = msg.uid;
var old = activeCustom;
activeCustom = {
uid: uid,
type: box.type,
hash: data.hash
};
cb(false, old);
};
handlers['BROADCAST_DELETE'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
var uid = content.uid; // uid of the message to delete
if (activeCustom && activeCustom.uid === uid) {
// We have the message in memory, remove it and don't keep the DELETE msg
cb(true, activeCustom);
activeCustom = undefined;
return;
}
// We don't have this message in memory, nothing to delete
cb(true);
};
return {
add: function (ctx, box, data, cb) {
/**

View file

@ -1,4 +1,6 @@
define([
'/api/config',
'/api/broadcast',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
@ -7,19 +9,22 @@ define([
'/common/outer/mailbox-handlers.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js',
], function (Util, Hash, Realtime, Messaging, Notify, Handlers, CpNetflux, Crypto) {
], function (Config, BCast, Util, Hash, Realtime, Messaging, Notify, Handlers, CpNetflux, Crypto) {
var Mailbox = {};
var TYPES = [
'notifications',
'supportadmin',
'support'
'support',
'broadcast'
];
var BLOCKING_TYPES = [
];
var BROADCAST_CHAN = '000000000000000000000000000000000'; // Admin channel, 33 characters
var initializeMailboxes = function (ctx, mailboxes) {
if (!mailboxes['notifications']) {
if (!mailboxes['notifications'] && ctx.loggedIn) {
mailboxes.notifications = {
channel: Hash.createChannelId(),
lastKnownHash: '',
@ -29,7 +34,7 @@ define([
if (res.error) { console.error(res); }
});
}
if (!mailboxes['support']) {
if (!mailboxes['support'] && ctx.loggedIn) {
mailboxes.support = {
channel: Hash.createChannelId(),
lastKnownHash: '',
@ -39,6 +44,20 @@ define([
if (res.error) { console.error(res); }
});
}
// XXX Debugging code to remove deprecated dev data
if (mailboxes.broadcast && mailboxes.broadcast.channel && mailboxes.broadcast.channel.length === 32) {
delete mailboxes['broadcast'];
}
if (!mailboxes['broadcast']) {
mailboxes.broadcast = {
channel: BROADCAST_CHAN,
lastKnownHash: BCast.lastBroadcastHash,
decrypted: true,
viewed: []
};
}
};
/*
@ -80,33 +99,49 @@ proxy.mailboxes = {
// Send a message to someone else
var sendTo = Mailbox.sendTo = function (ctx, type, msg, user, _cb) {
user = user || {};
var cb = _cb || function (obj) {
if (obj && obj.error) {
console.error(obj.error);
}
};
if (!Crypto.Mailbox) {
return void cb({error: "chainpad-crypto is outdated and doesn't support mailboxes."});
}
var keys = getMyKeys(ctx);
if (!keys) { return void cb({error: "missing asymmetric encryption keys"}); }
if (!user || !user.channel || !user.curvePublic) { return void cb({error: "no notification channel"}); }
var anonRpc = Util.find(ctx, [ 'store', 'anon_rpc', ]);
if (!anonRpc) { return void cb({error: "anonymous rpc session not ready"}); }
var crypto = Crypto.Mailbox.createEncryptor(keys);
// Always send your data
if (typeof(msg) === "object" && !msg.user) {
var myData = Messaging.createData(ctx.store.proxy, false);
msg.user = myData;
}
var text = JSON.stringify({
// Broadcast mailbox doesn't use encryption. Sending messages there is restricted
// to admins in the server directly
var crypto = { encrypt: function (x) { return x; } };
var channel = BROADCAST_CHAN;
var obj = {
uid: Util.uid(), // add uid at the beginning to have a unique server hash
type: type,
content: msg
});
};
if (!/^BROADCAST/.test(type)) {
var keys = getMyKeys(ctx);
if (!keys) { return void cb({error: "missing asymmetric encryption keys"}); }
if (!user || !user.channel || !user.curvePublic) { return void cb({error: "no notification channel"}); }
channel = user.channel;
crypto = Crypto.Mailbox.createEncryptor(keys);
// Always send your data
if (typeof(msg) === "object" && !msg.user) {
var myData = Messaging.createData(ctx.store.proxy, false);
msg.user = myData;
}
obj = {
type: type,
content: msg
};
}
var text = JSON.stringify(obj);
var ciphertext = crypto.encrypt(text, user.curvePublic);
// If we've sent this message to one of our teams' mailbox, we may want to "dismiss" it
@ -121,7 +156,7 @@ proxy.mailboxes = {
}
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
user.channel,
channel,
ciphertext
], function (err /*, response */) {
if (err) {
@ -129,7 +164,9 @@ proxy.mailboxes = {
error: err,
});
}
return void cb();
return void cb({
hash: ciphertext.slice(0,64)
});
});
};
@ -239,15 +276,18 @@ proxy.mailboxes = {
return void console.error("chainpad-crypto is outdated and doesn't support mailboxes.");
}
var keys = m.keys || getMyKeys(ctx);
if (!keys) { return void console.error("missing asymmetric encryption keys"); }
var crypto = Crypto.Mailbox.createEncryptor(keys);
if (!keys && !m.decrypted) { return void console.error("missing asymmetric encryption keys"); }
var crypto = m.decrypted ? {
encrypt: function (x) { return x; },
decrypt: function (x) { return x; }
} : Crypto.Mailbox.createEncryptor(keys);
box.encryptor = crypto;
var cfg = {
network: ctx.store.network,
channel: m.channel,
noChainPad: true,
crypto: crypto,
owners: opts.owners || [ctx.store.proxy.edPublic],
owners: type === 'broadcast' ? [] : (opts.owners || [ctx.store.proxy.edPublic]),
lastKnownHash: m.lastKnownHash
};
cfg.onConnectionChange = function () {}; // Allow reconnections in chainpad-netflux
@ -265,6 +305,7 @@ proxy.mailboxes = {
sendMessage(msg, function (err, hash) {
if (err) { return void console.error(err); }
box.history.push(hash);
_msg.ctime = +new Date();
box.content[hash] = _msg;
var message = {
msg: _msg,
@ -280,9 +321,10 @@ proxy.mailboxes = {
box.queue = [];
};
var lastReceivedHash; // Don't send a duplicate of the last known hash on reconnect
box.onMessage = cfg.onMessage = function (msg, user, vKey, isCp, hash, author) {
box.onMessage = cfg.onMessage = function (msg, user, vKey, isCp, hash, author, data) {
if (hash === m.lastKnownHash) { return; }
if (hash === lastReceivedHash) { return; }
var time = data && data.time;
lastReceivedHash = hash;
try {
msg = JSON.parse(msg);
@ -298,7 +340,22 @@ proxy.mailboxes = {
hash: hash
};
var notify = box.ready;
Handlers.add(ctx, box, message, function (dismissed, toDismiss) {
Handlers.add(ctx, box, message, function (dismissed, toDismiss, setAsLKH) {
if (setAsLKH) {
// Update LKH
box.data.lastKnownHash = hash;
box.data.viewed = [];
// Make sure we remove data about dismissed messages
Realtime.whenRealtimeSyncs(ctx.store.realtime, function () {
Object.keys(box.content).forEach(function (h) {
Handlers.remove(ctx, box, box.content[h], h);
delete box.content[h];
hideMessage(ctx, type, h, ctx.clients);
});
});
return;
}
if (toDismiss) { // List of other messages to remove
dismiss(ctx, toDismiss, '', function () {
console.log('Notification handled automatically');
@ -313,6 +370,7 @@ proxy.mailboxes = {
});
return;
}
msg.ctime = time || 0;
box.content[hash] = msg;
showMessage(ctx, type, message, null, function (obj) {
if (!obj || !obj.msg || !notify) { return; }
@ -389,12 +447,16 @@ proxy.mailboxes = {
if (type === 'HISTORY_RANGE') {
if (!Array.isArray(_msg)) { return; }
var message;
try {
var decrypted = box.encryptor.decrypt(_msg[4]);
message = JSON.parse(decrypted.content);
message.author = decrypted.author;
} catch (e) {
console.log(e);
if (req.box.type === 'broadcast') {
message = Util.tryParse(_msg[4]);
} else {
try {
var decrypted = box.encryptor.decrypt(_msg[4]);
message = JSON.parse(decrypted.content);
message.author = decrypted.author;
} catch (e) {
console.log(e);
}
}
ctx.emit('HISTORY', {
txid: txid,
@ -420,6 +482,13 @@ proxy.mailboxes = {
txid: data.txid
}
];
if (data.type === 'broadcast') {
msg = [ 'GET_HISTORY_RANGE', box.channel, {
to: data.lastKnownHash,
txid: data.txid
}
];
}
ctx.req[data.txid] = {
cId: clientId,
box: box
@ -458,22 +527,26 @@ proxy.mailboxes = {
Mailbox.init = function (cfg, waitFor, emit) {
var mailbox = {};
var store = cfg.store;
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
var ctx = {
Store: cfg.Store,
store: store,
pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata,
updateDrive: cfg.updateDrive,
mailboxes: mailboxes,
emit: emit,
clients: [],
boxes: {},
req: {}
req: {},
loggedIn: store.loggedIn && store.proxy.edPublic
};
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
initializeMailboxes(ctx, mailboxes);
initializeHistory(ctx);
if (ctx.loggedIn) {
initializeHistory(ctx);
}
Object.keys(mailboxes).forEach(function (key) {
if (TYPES.indexOf(key) === -1) { return; }
@ -490,18 +563,20 @@ proxy.mailboxes = {
}
});
Object.keys(store.proxy.teams || {}).forEach(function (teamId) {
var team = store.proxy.teams[teamId];
if (!team) { return; }
var teamMailbox = team.keys.mailbox || {};
if (!teamMailbox.channel) { return; }
var opts = {
owners: [Util.find(team, ['keys', 'drive', 'edPublic'])]
};
openChannel(ctx, 'team-'+teamId, teamMailbox, function () {
//console.log('Mailbox team', teamId);
}, opts);
});
if (ctx.loggedIn) {
Object.keys(store.proxy.teams || {}).forEach(function (teamId) {
var team = store.proxy.teams[teamId];
if (!team) { return; }
var teamMailbox = team.keys.mailbox || {};
if (!teamMailbox.channel) { return; }
var opts = {
owners: [Util.find(team, ['keys', 'drive', 'edPublic'])]
};
openChannel(ctx, 'team-'+teamId, teamMailbox, function () {
//console.log('Mailbox team', teamId);
}, opts);
});
}
mailbox.post = function (box, type, content) {
var b = ctx.boxes[box];
@ -526,6 +601,7 @@ proxy.mailboxes = {
};
mailbox.sendTo = function (type, msg, user, cb) {
if (!ctx.loggedIn) { return void cb({error:'NOT_LOGGED_IN'}); }
sendTo(ctx, type, msg, user, cb);
};

View file

@ -39,10 +39,10 @@ define([
type: type,
msg: content,
user: user
}, function (err, obj) {
cb(err || (obj && obj.error), obj);
if (err || (obj && obj.error)) {
return void console.error(err || obj.error);
}, function (obj) {
cb(obj && obj.error, obj);
if (obj && obj.error) {
return void console.error(obj.error);
}
});
};
@ -56,7 +56,16 @@ define([
var notif;
var avatar;
var userData = Util.find(data, ['content', 'msg', 'content', 'user']);
if (userData && typeof(userData) === "object" && userData.profile) {
if (Util.find(data, ['content', 'msg', 'type']) === 'BROADCAST_DELETE') {
return;
}
if (data.type === 'broadcast') {
avatar = h('i.fa.fa-bullhorn.cp-broadcast');
if (/^LOCAL\|/.test(data.content.hash)) {
$(avatar).addClass('preview');
}
} else if (userData && typeof(userData) === "object" && userData.profile) {
avatar = h('span.cp-avatar');
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
$(avatar).click(function (e) {
@ -64,7 +73,9 @@ define([
Common.openURL(Hash.hashToHref(userData.profile, 'profile'));
});
}
var order = -Math.floor((Util.find(data, ['content', 'msg', 'ctime']) || 0) / 1000);
notif = h('div.cp-notification', {
style: 'order:'+order+';',
'data-hash': data.content.hash
}, [
avatar,
@ -106,7 +117,7 @@ define([
// Call the onMessage handlers
var isNotification = function (type) {
return type === "notifications" || /^team-/.test(type);
return type === "notifications" || /^team-/.test(type) || type === "broadcast";
};
var pushMessage = function (data, handler) {
var todo = function (f) {
@ -142,7 +153,7 @@ define([
removeFromHistory(data.type, data.hash);
};
var onMessage = function (data, cb) {
var onMessage = mailbox.onMessage = function (data, cb) {
// data = { type: 'type', content: {msg: 'msg', hash: 'hash'} }
pushMessage(data);
if (data.content && typeof (data.content.getFormatText) === "function") {
@ -160,6 +171,11 @@ define([
hash: data.content.hash,
type: data.type
};
if (/^LOCAL\|/.test(dataObj.hash)) {
onViewed(dataObj);
cb();
return;
}
execCommand('DISMISS', dataObj, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
onViewed(dataObj);
@ -183,7 +199,7 @@ define([
cfg.onViewed(data);
});
}
if (typeof(cfg.onMessage) === "function") {
if (typeof(cfg.onMessage) === "function" && !cfg.history) {
onMessageHandlers.push(function (data, el) {
var type = data.type;
if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
@ -203,8 +219,8 @@ define([
var historyState = false;
var onHistory = function () {};
mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) {
if (historyState) { return void cb("ALREADY_CALLED"); }
mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) {
if (type !== "broadcast" && historyState) { return void cb("ALREADY_CALLED"); }
historyState = true;
var txid = Util.uid();
execCommand('LOAD_HISTORY', {

View file

@ -1,5 +1,6 @@
define([
'jquery',
'/api/config',
'/bower_components/nthen/index.js',
'/customize/messages.js',
'/common/sframe-chainpad-netflux-inner.js',
@ -26,6 +27,7 @@ define([
'/bower_components/localforage/dist/localforage.min.js'
], function (
$,
ApiConfig,
nThen,
Messages,
CpNfInner,
@ -210,9 +212,8 @@ define([
var modules = {};
funcs.makeUniversal = function (type, cfg) {
if (cfg && cfg.onEvent) {
modules[type] = {
onEvent: cfg.onEvent || function () {}
};
modules[type] = modules[type] || Util.mkEvent();
modules[type].reg(cfg.onEvent);
}
var sframeChan = funcs.getSframeChannel();
return {
@ -703,6 +704,12 @@ define([
});
};
funcs.isAdmin = function () {
var privateData = ctx.metadataMgr.getPrivateData();
return privateData.edPublic && Array.isArray(ApiConfig.adminKeys) &&
ApiConfig.adminKeys.indexOf(privateData.edPublic) !== -1;
};
funcs.mailbox = {};
Object.freeze(funcs);
@ -825,7 +832,7 @@ define([
ctx.sframeChan.on('EV_UNIVERSAL_EVENT', function (obj) {
var type = obj.type;
if (!type || !modules[type]) { return; }
modules[type].onEvent(obj.data);
modules[type].fire(obj.data);
});
ctx.cache = Cache.create(ctx.sframeChan);

View file

@ -2,6 +2,7 @@ define([
'jquery',
'/customize/application_config.js',
'/api/config',
'/api/broadcast',
'/common/common-ui-elements.js',
'/common/common-interface.js',
'/common/common-hash.js',
@ -11,7 +12,7 @@ define([
'/common/hyperscript.js',
'/common/messenger-ui.js',
'/customize/messages.js',
], function ($, Config, ApiConfig, UIElements, UI, Hash, Util, Feedback, MT, h,
], function ($, Config, ApiConfig, Broadcast, UIElements, UI, Hash, Util, Feedback, MT, h,
MessengerUI, Messages) {
var Common;
@ -45,6 +46,7 @@ MessengerUI, Messages) {
var TITLE_CLS = Bar.constants.title = "cp-toolbar-title";
var LINK_CLS = Bar.constants.link = "cp-toolbar-link";
var NOTIFICATIONS_CLS = Bar.constants.user = 'cp-toolbar-notifications';
var MAINTENANCE_CLS = Bar.constants.user = 'cp-toolbar-maintenance';
// User admin menu
var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown';
@ -78,6 +80,7 @@ MessengerUI, Messages) {
'class': USER_CLS
}).appendTo($topContainer);
$('<span>', {'class': LIMIT_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': MAINTENANCE_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer);
$('<span>', {'class': NOTIFICATIONS_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer);
$('<span>', {'class': USERADMIN_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer);
@ -1026,6 +1029,42 @@ MessengerUI, Messages) {
return $userAdmin;
};
Messages.broadcast_maintenance = "A maintenance is planned between <b>{0}</b> and <b>{1}</b>"; // XXX
var createMaintenance = function (toolbar, config) {
var $notif = toolbar.$top.find('.'+MAINTENANCE_CLS);
var button = h('button.cp-maintenance-wrench.fa.fa-wrench');
$notif.append(button);
var m = Broadcast.maintenance;
$(button).click(function () {
if (!m || !m.start || !m.end) { return; }
UI.alert(Messages._getKey('broadcast_maintenance', [
new Date(m.start).toLocaleString(),
new Date(m.end).toLocaleString(),
]), null, true);
});
Common.makeUniversal('broadcast', {
onEvent: function (obj) {
var cmd = obj.ev;
if (cmd !== "MAINTENANCE") { return; }
var data = obj.data;
if (!data) {
return void $notif.hide();
}
m = data;
$notif.css('display', '');
}
});
if (m && m.start && m.end) {
$notif.css('display', '');
} else {
$notif.hide();
}
};
var createNotifications = function (toolbar, config) {
var $notif = toolbar.$top.find('.'+NOTIFICATIONS_CLS).show();
var openNotifsApp = h('div.cp-notifications-gotoapp', h('p', Messages.openNotificationsApp || "Open notifications App"));
@ -1096,7 +1135,7 @@ MessengerUI, Messages) {
$button.addClass('fa-bell');
};
Common.mailbox.subscribe(['notifications', 'team'], {
Common.mailbox.subscribe(['notifications', 'team', 'broadcast'], {
onMessage: function (data, el) {
if (el) {
$(div).prepend(el);
@ -1287,6 +1326,7 @@ MessengerUI, Messages) {
tb['useradmin'] = createUserAdmin;
tb['unpinnedWarning'] = createUnpinnedWarning;
tb['notifications'] = createNotifications;
tb['maintenance'] = createMaintenance;
tb['pad'] = function () {
toolbar.$file.show();
@ -1323,6 +1363,7 @@ MessengerUI, Messages) {
};
addElement(config.displayed, {}, true);
addElement(['maintenance'], {}, true);
toolbar['linkToMain'] = createLinkToMain(toolbar, config);

File diff suppressed because one or more lines are too long

13
www/lib/datepicker/flatpickr.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -445,6 +445,9 @@ define([
};
ctx.FM = common.createFileManager(fmConfig);
ui.send = function (id, type, data, dest) {
return send(ctx, id, type, data, dest);
};
ui.sendForm = function (id, form, dest) {
return sendForm(ctx, id, form, dest);
};