Instance notification prototype

pull/1/head
yflory 4 years ago
parent 7ab529d4e9
commit fee8a88169

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

@ -254,6 +254,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 === '00000000000000000000000000000000') {
allowed = Env.admins;
}
if (HK.isUserSessionAllowed(allowed, session)) { return; }
w.abort();

@ -883,6 +883,10 @@ 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(); }
if (channel.id === '00000000000000000000000000000000' && msgStruct[1] !== null) {
return void cb('ERESTRICTED_ADMIN');
}
const isCp = /^cp\|/.test(msgStruct[4]);
let id;
if (isCp) {

@ -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,30 @@
}
}
.cp-admin-broadcast-form {
margin-top: 30px;
& > button:last-child {
margin-top: 30px !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;
}
}
}
}

@ -14,6 +14,9 @@ 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',
@ -31,7 +34,8 @@ define([
Util,
Hash,
Keys,
Support
Support,
Flatpickr
)
{
var APP = {
@ -67,6 +71,9 @@ define([
'cp-admin-support-list',
'cp-admin-support-init'
],
'broadcast': [ // Msg.admin_cat_support
'cp-admin-broadcast'
],
'performance': [ // Msg.admin_cat_performance
'cp-admin-refresh-performance',
'cp-admin-performance-profiling',
@ -930,6 +937,279 @@ define([
return;
};
// Messages.admin_cat_broadcast // XXX
// Messages.admin_broadcastHint // XXX
// Messages.admin_broadcastTitle // XXX
Messages.broadcast_maintenance = 'maintenance';// XXX
Messages.broadcast_survey = 'survey'; // XXX
Messages.broadcast_version = 'version'; // XXX
Messages.broadcast_custom = 'custom'; // XXX
Messages.broadcast_newVersionReload = 'Force a worker reload on all clients'; // XXX
Messages.broadcast_surveyURL = 'Survey URL';
Messages.broadcast_translations = 'Translations';
Messages.broadcast_defaultLanguage = 'Default language';
Messages.broadcast_start = 'Start time';
Messages.broadcast_end = 'End time';
var getBroadcastForm = function ($form, key) {
$form.empty();
var getData = function () {
return false;
};
var reset = function () {};
var button = h('button.btn.btn-primary', Messages.support_formButton);
var $button = $(button);
var send = function () {
var data = getData();
if (data === false) { return void UI.warn(Messages.error); }
$button.prop('disabled', 'disabled');
common.mailbox.sendTo('BROADCAST_'+key.toUpperCase(), data, {}, function (err) {
$button.prop('disabled', '');
if (err) { return UI.warn(Messages.error); }
reset();
UI.log(Messages.saved);
});
};
$button.click(function () {
send();
});
if (key === 'custom') {
(function () {
var container = h('div.cp-broadcast-container');
var $container = $(container);
var languages = Messages._languages;
var keys = Object.keys(languages).sort();
var onPreview = function (l) {
// XXX
};
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));
});
};
var removeLang = function (l) {
$container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove();
};
var addLang = function (l) {
if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; }
var preview = h('button.btn.btn-secondary', Messages.share_linkOpen);
$(preview).click(function () {
onPreview(l);
});
var bcastDefault = Messages.broadcast_defaultLanguage;
$container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [
h('h4', languages[l]),
h('label', Messages.kanban_body),
h('textarea'),
UI.createRadio('broadcastDefault', null, bcastDefault, false, {
'data-lang': l,
label: {class: 'noTitle'}
}),
preview
]));
reorder();
};
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];
});
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
};
};
reset = function () {
$container.find('.cp-broadcast-lang textarea').each(function (i, el) {
$(el).val('');
});
};
$form.append([
h('label', Messages.broadcast_translations),
h('div.cp-broadcast-languages', boxes),
container,
button
]);
})();
return;
}
if (key === 'maintenance') {
(function () {
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()));
}
});
getData = function () {
var start = +new Date($start.val());
var end = +new Date($start.val());
if (isNaN(start) || isNaN(end)) {
console.error('Invalid dates');
return false;
}
return {
start: start,
end: end
};
};
reset = function () {
$start.val('');
$end.val('');
};
$form.append([
h('label', Messages.broadcast_start),
start,
h('label', Messages.broadcast_end),
end,
h('br'),
button
]);
})();
return;
}
if (key === 'version') {
(function () {
var $cbox = $(UI.createCheckbox('cp-admin-version-reload',
Messages.broadcast_newVersionReload,
false, { label: { class: 'noTitle' } }));
var $checkbox = $cbox.find('input');
getData = function () {
return {
reload: $checkbox.is(':checked')
};
};
reset = function () {
$checkbox[0].checked = false;
};
$form.append([
$cbox[0],
h('br'),
button
]);
})();
return;
}
if (key === 'survey') {
(function () {
var label = h('label', Messages.broadcast_surveyURL);
var input = h('input');
var $input = $(input);
getData = function () {
var url = $input.val();
if (!Util.isValidURL(url)) {
console.error('Invalid URL');
return false;
}
return {
url: url
};
};
reset = function () {
$input.val('');
};
$form.append([
label,
input,
h('br'),
button
]);
})();
return;
}
};
create['broadcast'] = function () {
var key = 'broadcast';
var $div = makeBlock(key);
var form = h('div.cp-admin-broadcast-form')
var $select = $(h('div.cp-dropdown-container')).appendTo($div);
var $form = $(form).appendTo($div);
var categories = [
'maintenance',
'survey',
'version',
'custom'
];
categories = categories.map(function (key) {
return {
tag: 'a',
content: h('span', Messages['broadcast_'+key]),
action: function () {
getBroadcastForm($form, key);
}
};
});
var dropdownCfg = {
text: Messages.support_category,
angleDown: 1,
options: categories,
container: $select,
isSelect: true
};
UIElements.createDropdown(dropdownCfg);
return $div;
};
var onRefreshPerformance = Util.mkEvent();
create['refresh-performance'] = function () {
@ -1010,6 +1290,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',
};

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

@ -406,6 +406,87 @@ 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_newMaintenance = "A maintenance is planned between <b>{0}</b> and <b>{1}</b>"; // XXX
handlers['BROADCAST_MAINTENANCE'] = function (common, data) {
var content = data.content;
var msg = content.msg.content;
content.getFormatText = function () {
return Messages._getKey('broadcast_newMaintenance', [
new Date(msg.start).toLocaleString(),
new Date(msg.end).toLocaleString(),
]);
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
Messages.broadcast_newVersion = "A new version is available. Reload the page to discover the new features!"; // XXX
handlers['BROADCAST_VERSION'] = function (common, data) {
var content = data.content;
content.getFormatText = function () {
return Messages.broadcast_newVersion;
};
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;
// Check if our language is available
var toShow = text[Messages._languageUsed];
// Otherwise, fallback to the default language if it exists
if (!toShow && defaultL) { toShow = text[defaultL]; }
// No translation available, dismiss
if (!toShow) { defaultDismiss(common, data)(); }
var slice = toShow.length > 500;
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, 500) + '...';
}
return toShow;
};
if (slice) {
content.handler = function () {
// XXX Allow markdown (sanitized)?
var content = h('div', [
h('h4', Messages.broadcast_newCustom),
h('div', 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 {

@ -1,4 +1,5 @@
define([
'/api/config',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
@ -7,17 +8,20 @@ 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, Util, Hash, Realtime, Messaging, Notify, Handlers, CpNetflux, Crypto) {
var Mailbox = {};
var TYPES = [
'notifications',
'supportadmin',
'support'
'support',
'broadcast'
];
var BLOCKING_TYPES = [
];
var BROADCAST_CHAN = '00000000000000000000000000000000';
var initializeMailboxes = function (ctx, mailboxes) {
if (!mailboxes['notifications']) {
mailboxes.notifications = {
@ -39,6 +43,16 @@ define([
if (res.error) { console.error(res); }
});
}
if (!mailboxes['broadcast']) {
mailboxes.broadcast = {
channel: BROADCAST_CHAN,
lastKnownHash: '', // XXX load /api/brooadcast to set this hash
decrypted: true,
viewed: []
};
} else {
// XXX update lastKnownHash from /api/broadcast
}
};
/*
@ -80,33 +94,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);
// 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
};
// Always send your data
if (typeof(msg) === "object" && !msg.user) {
var myData = Messaging.createData(ctx.store.proxy, false);
msg.user = myData;
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({
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 +151,7 @@ proxy.mailboxes = {
}
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
user.channel,
channel,
ciphertext
], function (err /*, response */) {
if (err) {
@ -239,8 +269,11 @@ 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,

@ -106,7 +106,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) {

@ -1081,7 +1081,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);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

Loading…
Cancel
Save