Merge branch 'reminders' into staging

pull/1/head
ansuz 4 years ago
commit ae34456b55

@ -122,7 +122,7 @@
&.small { &.small {
line-height: initial; line-height: initial;
padding: 5px; padding: 5px;
height: auto; height: auto !important;
} }
&:hover, &:not(:disabled):not(.disabled):active, &:focus { &:hover, &:not(:disabled):not(.disabled):active, &:focus {

@ -178,6 +178,44 @@
} }
} }
.cp-calendar-add-notif {
flex-flow: column;
align-items: baseline !important;
margin: 10px 0;
.cp-notif-label {
color: @cp_sidebar-hint;
margin-right: 20px;
}
* {
font-size: @colortheme_app-font-size;
font-weight: normal;
}
& > div {
display: flex;
}
.cp-calendar-notif-list {
display: flex;
flex-flow: column;
.cp-notif-entry {
span:not(:last-child) {
margin-right: 20px;
}
}
}
.cp-notif-empty {
display: none;
}
.cp-calendar-notif-list:empty ~ .cp-notif-empty {
display: block;
}
.cp-calendar-notif-form {
align-items: center;
input {
width: 100px;
}
}
}
.cp-calendar-close { .cp-calendar-close {
height: auto; height: auto;
line-height: initial; line-height: initial;

@ -0,0 +1,199 @@
// This file is used when a user tries to export the entire CryptDrive.
// Calendars will be exported using this format instead of plain text.
define([
'/customize/pages.js',
], function (Pages) {
var module = {};
var getICSDate = function (str) {
var date = new Date(str);
var m = date.getUTCMonth() + 1;
var d = date.getUTCDate();
var h = date.getUTCHours();
var min = date.getUTCMinutes();
var year = date.getUTCFullYear().toString();
var month = m < 10 ? "0" + m : m.toString();
var day = d < 10 ? "0" + d : d.toString();
var hours = h < 10 ? "0" + h : h.toString();
var minutes = min < 10 ? "0" + min : min.toString();
return year + month + day + "T" + hours + minutes + "00Z";
}
var getDate = function (str, end) {
var date = new Date(str);
if (end) {
date.setDate(date.getDate() + 1);
}
var m = date.getUTCMonth() + 1;
var d = date.getUTCDate();
var year = date.getUTCFullYear().toString();
var month = m < 10 ? "0" + m : m.toString();
var day = d < 10 ? "0" + d : d.toString();
return year+month+day;
};
var MINUTE = 60;
var HOUR = MINUTE * 60;
var DAY = HOUR * 24;
module.main = function (userDoc) {
var content = userDoc.content;
var md = userDoc.metadata;
var ICS = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//CryptPad//CryptPad Calendar '+Pages.versionString+'//EN',
'METHOD:PUBLISH',
];
Object.keys(content).forEach(function (uid) {
var data = content[uid];
// DTSTAMP: now...
// UID: uid
var start, end;
if (data.isAllDay && data.startDay && data.endDay) {
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
Array.prototype.push.apply(ICS, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
]);
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
}
Array.prototype.push.apply(ICS, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(ICS, data.cp_hidden);
}
ICS.push('END:VEVENT');
});
ICS.push('END:VCALENDAR');
return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' });
};
module.import = function (content, id, cb) {
require(['/lib/ical.min.js'], function () {
var ICAL = window.ICAL;
var res = {};
try {
var jcalData = ICAL.parse(content);
var vcalendar = new ICAL.Component(jcalData);
} catch (e) {
return void cb(e);
}
var method = vcalendar.getFirstPropertyValue('method');
if (method !== "PUBLISH") { return void cb('NOT_SUPPORTED'); }
var events = vcalendar.getAllSubcomponents('vevent');
events.forEach(function (ev) {
var uid = ev.getFirstPropertyValue('uid');
if (!uid) { return; }
// Get start and end time
var isAllDay = false;
var start = ev.getFirstPropertyValue('dtstart');
var end = ev.getFirstPropertyValue('dtend');
if (start.isDate && end.isDate) {
isAllDay = true;
start = String(start);
end.adjust(-1); // Substract one day
end = String(end);
} else {
start = +start.toJSDate();
end = +end.toJSDate();
}
// Store other properties
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp'];
var hidden = [];
ev.getAllProperties().forEach(function (p) {
if (used.indexOf(p.name) !== -1) { return; }
// This is an unused property
hidden.push(p.toICALString());
});
// Get reminders
var reminders = [];
ev.getAllSubcomponents('valarm').forEach(function (al) {
var action = al.getFirstPropertyValue('action');
if (action !== 'DISPLAY') {
// Email notification: keep it in "hidden" and create a cryptpad notification
hidden.push(al.toString());
}
var trigger = al.getFirstPropertyValue('trigger');
var minutes = -trigger.toSeconds() / 60;
if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); }
});
// Create event
res[uid] = {
calendarId: id,
id: uid,
category: 'time',
title: ev.getFirstPropertyValue('summary'),
location: ev.getFirstPropertyValue('location'),
isAllDay: isAllDay,
start: start,
end: end,
reminders: reminders,
cp_hidden: hidden
};
if (!hidden.length) { delete res[uid].cp_hidden; }
if (!reminders.length) { delete res[uid].reminders; }
});
cb(null, res);
});
};
return module;
});

@ -1,5 +1,6 @@
define([ define([
'jquery', 'jquery',
'json.sortify',
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar.js', '/common/toolbar.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
@ -15,17 +16,20 @@ define([
'/customize/messages.js', '/customize/messages.js',
'/customize/application_config.js', '/customize/application_config.js',
'/lib/calendar/tui-calendar.min.js', '/lib/calendar/tui-calendar.min.js',
'/calendar/export.js',
'/common/inner/share.js', '/common/inner/share.js',
'/common/inner/access.js', '/common/inner/access.js',
'/common/inner/properties.js', '/common/inner/properties.js',
'/common/jscolor.js', '/common/jscolor.js',
'/bower_components/file-saver/FileSaver.min.js',
'css!/lib/calendar/tui-calendar.min.css', 'css!/lib/calendar/tui-calendar.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/calendar/app-calendar.less', 'less!/calendar/app-calendar.less',
], function ( ], function (
$, $,
JSONSortify,
Crypto, Crypto,
Toolbar, Toolbar,
nThen, nThen,
@ -41,9 +45,11 @@ define([
Messages, Messages,
AppConfig, AppConfig,
Calendar, Calendar,
Export,
Share, Access, Properties Share, Access, Properties
) )
{ {
var SaveAs = window.saveAs;
var APP = window.APP = { var APP = window.APP = {
calendars: {} calendars: {}
}; };
@ -77,6 +83,14 @@ Messages.calendar_loc = "Location";
Messages.calendar_location = "Location: {0}"; Messages.calendar_location = "Location: {0}";
Messages.calendar_allDay = "All day"; Messages.calendar_allDay = "All day";
Messages.calendar_minutes = "Minutes";
Messages.calendar_hours = "Hours";
Messages.calendar_days = "Days";
Messages.calendar_notifications = "Reminders";
Messages.calendar_addNotification = "Add reminder";
Messages.calendar_noNotification = "None";
var onCalendarsUpdate = Util.mkEvent(); var onCalendarsUpdate = Util.mkEvent();
var newCalendar = function (data, cb) { var newCalendar = function (data, cb) {
@ -103,6 +117,12 @@ Messages.calendar_allDay = "All day";
cb(null, obj); cb(null, obj);
}); });
}; };
var importICSCalendar = function (data, cb) {
APP.module.execCommand('IMPORT_ICS', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var newEvent = function (data, cb) { var newEvent = function (data, cb) {
APP.module.execCommand('CREATE_EVENT', data, function (obj) { APP.module.execCommand('CREATE_EVENT', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); } if (obj && obj.error) { return void cb(obj.error); }
@ -230,11 +250,17 @@ Messages.calendar_allDay = "All day";
})()) { getTime = undefined; } })()) { getTime = undefined; }
var templates = { var templates = {
popupSave: function (obj) {
APP.editModalData = obj.data && obj.data.root;
return Messages.settings_save;
},
popupUpdate: function(obj) {
APP.editModalData = obj.data && obj.data.root;
return Messages.calendar_update;
},
monthGridHeaderExceed: function(hiddenSchedules) { monthGridHeaderExceed: function(hiddenSchedules) {
return '<span class="tui-full-calendar-weekday-grid-more-schedules">' + Messages._getKey('calendar_more', [hiddenSchedules]) + '</span>'; return '<span class="tui-full-calendar-weekday-grid-more-schedules">' + Messages._getKey('calendar_more', [hiddenSchedules]) + '</span>';
}, },
popupSave: function () { return Messages.settings_save; },
popupUpdate: function() { return Messages.calendar_update; },
popupEdit: function() { return Messages.poll_edit; }, popupEdit: function() { return Messages.poll_edit; },
popupDelete: function() { return Messages.kanban_delete; }, popupDelete: function() { return Messages.kanban_delete; },
popupDetailLocation: function(schedule) { popupDetailLocation: function(schedule) {
@ -453,6 +479,79 @@ Messages.calendar_allDay = "All day";
return true; return true;
} }
}); });
if (!data.readOnly) {
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-upload',
},
content: h('span', Messages.importButton),
action: function (e) {
UIElements.importContent('text/calendar', function (res) {
Export.import(res, id, function (err, json) {
if (err) { return void UI.warn(Messages.importError); }
importICSCalendar({
id: id,
json: json
}, function (err) {
if (err) { return void UI.warn(Messages.error); }
UI.log(Messages.saved);
});
});
}, {
accept: ['.ics']
})();
return true;
}
});
}
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-download',
},
content: h('span', Messages.exportButton),
action: function (e) {
e.stopPropagation();
var cal = APP.calendars[id];
var suggestion = Util.find(cal, ['content', 'metadata', 'title']);
var types = [];
types.push({
tag: 'a',
attributes: {
'data-value': '.ics',
'href': '#'
},
content: '.ics'
});
var dropdownConfig = {
text: '.ics', // Button initial text
caretDown: true,
options: types, // Entries displayed in the menu
isSelect: true,
initialValue: '.ics',
common: common
};
var $select = UIElements.createDropdown(dropdownConfig);
UI.prompt(Messages.exportPrompt,
Util.fixFileName(suggestion), function (filename)
{
if (!(typeof(filename) === 'string' && filename)) { return; }
var ext = $select.getValue();
filename = filename + ext;
var blob = Export.main(cal.content);
SaveAs(blob, filename);
}, {
typeInput: $select[0]
});
return true;
}
});
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
@ -498,7 +597,7 @@ Messages.calendar_allDay = "All day";
if (cal.owned) { if (cal.owned) {
key += Messages.calendar_deleteOwned; key += Messages.calendar_deleteOwned;
} }
UI.confirm(Messages.calendar_deleteConfirm, function (yes) { UI.confirm(key, function (yes) {
if (!yes) { return; } if (!yes) { return; }
deleteCalendar({ deleteCalendar({
id: id, id: id,
@ -681,6 +780,7 @@ Messages.calendar_allDay = "All day";
// ie: recurrenceRule: DAILY|{uid} // ie: recurrenceRule: DAILY|{uid}
// Use template to hide "recurrenceRule" from the detailPopup or at least to use // Use template to hide "recurrenceRule" from the detailPopup or at least to use
// a non technical value // a non technical value
var reminders = APP.notificationsEntries;
var startDate = event.start._date; var startDate = event.start._date;
var endDate = event.end._date; var endDate = event.end._date;
@ -694,6 +794,7 @@ Messages.calendar_allDay = "All day";
start: +startDate, start: +startDate,
isAllDay: event.isAllDay, isAllDay: event.isAllDay,
end: +endDate, end: +endDate,
reminders: reminders,
}; };
newEvent(schedule, function (err) { newEvent(schedule, function (err) {
@ -711,6 +812,12 @@ Messages.calendar_allDay = "All day";
if (changes.start) { changes.start = +new Date(changes.start._date); } if (changes.start) { changes.start = +new Date(changes.start._date); }
var old = event.schedule; var old = event.schedule;
var oldReminders = Util.find(APP.calendars, [old.calendarId, 'content', 'content', old.id, 'reminders']);
var reminders = APP.notificationsEntries;
if (JSONSortify(oldReminders || []) !== JSONSortify(reminders)) {
changes.reminders = reminders;
}
updateEvent({ updateEvent({
ev: old, ev: old,
changes: changes changes: changes
@ -811,6 +918,113 @@ Messages.calendar_allDay = "All day";
}; };
var parseNotif = function (minutes) {
var res = {
unit: 'minutes',
value: minutes
};
var hours = minutes / 60;
if (!Number.isInteger(hours)) { return res; }
res.unit = 'hours';
res.value = hours;
var days = hours / 24;
if (!Number.isInteger(days)) { return res; }
res.unit = 'days';
res.value = days;
return res;
};
var getNotificationDropdown = function () {
var ev = APP.editModalData;
var calId = ev.selectedCal.id;
// XXX DEFAULT HERE [10] ==> 10 minutes before the event
var oldReminders = Util.find(APP.calendars, [calId, 'content', 'content', ev.id, 'reminders']) || [10];
APP.notificationsEntries = [];
var number = h('input.tui-full-calendar-content', {
type: "number",
value: 10,
min: 1,
max: 60
});
var $number = $(number);
var options = ['minutes', 'hours', 'days'].map(function (k) {
return {
tag: 'a',
attributes: {
'class': 'cp-calendar-reminder',
'data-value': k,
'href': '#',
},
content: Messages['calendar_'+k]
// Messages.calendar_minutes
// Messages.calendar_hours
// Messages.calendar_days
};
});
var dropdownConfig = {
text: Messages.calendar_minutes,
options: options, // Entries displayed in the menu
isSelect: true,
common: common,
buttonCls: 'btn btn-secondary',
caretDown: true,
};
var $block = UIElements.createDropdown(dropdownConfig);
$block.setValue('minutes');
var $types = $block.find('a');
$types.click(function () {
var mode = $(this).attr('data-value');
var max = mode === "minutes" ? 60 : 24;
$number.attr('max', max);
if ($number.val() > max) { $number.val(max); }
});
var addNotif = h('button.btn.btn-primary.fa.fa-plus');
var $list = $(h('div.cp-calendar-notif-list'));
var listContainer = h('div.cp-calendar-notif-list-container', [
h('span.cp-notif-label', Messages.calendar_notifications),
$list[0],
h('span.cp-notif-empty', Messages.calendar_noNotification)
]);
var addNotification = function (unit, value) {
var unitValue = (unit === "minutes") ? 1 : (unit === "hours" ? 60 : (60*24));
var del = h('button.btn.btn-danger.small.fa.fa-times');
var minutes = value * unitValue;
if ($list.find('[data-minutes="'+minutes+'"]').length) { return; }
var span = h('span.cp-notif-entry', {
'data-minutes': minutes
}, [
h('span', value),
h('span', Messages['calendar_'+unit]),
del
]);
$(del).click(function () {
$(span).remove();
var idx = APP.notificationsEntries.indexOf(minutes);
APP.notificationsEntries.splice(idx, 1);
});
$list.append(span);
APP.notificationsEntries.push(minutes);
};
$(addNotif).click(function () {
var unit = $block.getValue();
var value = $number.val();
addNotification(unit, value);
});
oldReminders.forEach(function (minutes) {
var p = parseNotif(minutes);
addNotification(p.unit, p.value);
});
return h('div.tui-full-calendar-popup-section.cp-calendar-add-notif', [
listContainer,
h('div.cp-calendar-notif-form', [
h('span.cp-notif-label', Messages.calendar_addNotification),
number,
$block[0],
addNotif
])
]);
};
var createToolbar = function () { var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications']; var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = { var configTb = {
@ -900,6 +1114,10 @@ Messages.calendar_allDay = "All day";
var isUpdate = Boolean($el.find('#tui-full-calendar-schedule-title').val()); var isUpdate = Boolean($el.find('#tui-full-calendar-schedule-title').val());
if (!isUpdate) { $el.find('.tui-full-calendar-dropdown-menu li').first().click(); } if (!isUpdate) { $el.find('.tui-full-calendar-dropdown-menu li').first().click(); }
var $button = $el.find('.tui-full-calendar-section-button-save');
var div = getNotificationDropdown();
$button.before(div);
var $cbox = $el.find('#tui-full-calendar-schedule-allday'); var $cbox = $el.find('#tui-full-calendar-schedule-allday');
var $start = $el.find('.tui-full-calendar-section-start-date'); var $start = $el.find('.tui-full-calendar-section-start-date');
var $dash = $el.find('.tui-full-calendar-section-date-dash'); var $dash = $el.find('.tui-full-calendar-section-date-dash');

@ -127,7 +127,7 @@ define([
dcAlert = undefined; dcAlert = undefined;
}; };
var importContent = function (type, f, cfg) { var importContent = UIElements.importContent = function (type, f, cfg) {
return function () { return function () {
var $files = $('<input>', {type:"file"}); var $files = $('<input>', {type:"file"});
if (cfg && cfg.accept) { if (cfg && cfg.accept) {

@ -2288,7 +2288,8 @@ define([
cache: rdyCfg.cache, cache: rdyCfg.cache,
noDrive: rdyCfg.noDrive, noDrive: rdyCfg.noDrive,
disableCache: localStorage['CRYPTPAD_STORE|disableCache'], disableCache: localStorage['CRYPTPAD_STORE|disableCache'],
driveEvents: !rdyCfg.noDrive //rdyCfg.driveEvents // Boolean driveEvents: !rdyCfg.noDrive, //rdyCfg.driveEvents // Boolean
lastVisit: Number(localStorage.lastVisit) || undefined
}; };
common.userHash = userHash; common.userHash = userHash;
@ -2560,6 +2561,12 @@ define([
AppConfig.afterLogin(common, waitFor()); AppConfig.afterLogin(common, waitFor());
} }
}).nThen(function () { }).nThen(function () {
// Last visit is used to warn you about missed events from your calendars
localStorage.lastVisit = +new Date();
setInterval(function () {
// Bump last visit every minute
localStorage.lastVisit = +new Date();
}, 60000);
f(void 0, env); f(void 0, env);
if (typeof(window.onhashchange) === 'function') { window.onhashchange(); } if (typeof(window.onhashchange) === 'function') { window.onhashchange(); }
}); });

@ -461,6 +461,76 @@ define([
} }
}; };
Messages.reminder_missed = "You missed <b>{0}</b> on {1}"; // XXX
Messages.reminder_now = "<b>{0}</b> is starting!"; // XXX
Messages.reminder_inProgress = "<b>{0}</b> has started on {1}"; // XXX
Messages.reminder_inProgressAllDay = "<b>{0}</b> is happening today"; // XXX
Messages.reminder_minutes = "<b>{0}</b> will start in {1} minutes!"; // XXX
Messages.reminder_time = "<b>{0}</b> will start today at {1}!"; // XXX
Messages.reminder_date = "<b>{0}</b> will start on {1}!"; // XXX
handlers['REMINDER'] = function (common, data) {
var content = data.content;
var msg = content.msg.content;
var missed = content.msg.missed;
var start = msg.start;
var title = Util.fixHTML(msg.title);
content.getFormatText = function () {
var now = +new Date();
// Events that have already started
var wasRefresh = content.autorefresh;
content.autorefresh = false;
var nowDateStr = new Date().toLocaleDateString();
var startDate = new Date(start);
if (msg.isAllDay && msg.startDay) {
startDate = new Date(msg.startDay);
}
// Missed events
if (start < now && missed) {
return Messages._getKey('reminder_missed', [title, startDate.toLocaleString()]);
}
// Starting now
if (start < now && wasRefresh) {
return Messages._getKey('reminder_now', [title]);
}
// In progress, is all day
if (start < now && msg.isAllDay) {
return Messages._getKey('reminder_inProgressAllDay', [title]);
}
// In progress, normal event
if (start < now) {
return Messages._getKey('reminder_inProgress', [title, startDate.toLocaleString()]);
}
// Not started yet
// No precise time for allDay events
if (msg.isAllDay) {
return Messages._getKey('reminder_date', [title, startDate.toLocaleDateString()]);
}
// In less than an hour: show countdown in minutes
if ((start - now) < 3600000) {
var minutes = Math.round((start - now) / 60000);
content.autorefresh = true;
return Messages._getKey('reminder_minutes', [title, minutes]);
}
// Not today: show full date
if (nowDateStr !== startDate.toLocaleDateString()) {
return Messages._getKey('reminder_date', [title, startDate.toLocaleString()]);
}
// Today: show time
return Messages._getKey('reminder_time', [title, startDate.toLocaleTimeString()]);
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText" // NOTE: don't forget to fixHTML everything returned by "getFormatText"
return { return {

@ -12,60 +12,6 @@ define([
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) { ], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
var Calendar = {}; var Calendar = {};
/* TODO
* Calendar
{
href,
roHref,
channel, (pinning)
title, (when created from the UI, own calendar has no title)
color
}
* Own drive
{
calendars: {
uid: calendar,
uid: calendar
}
}
* Team drive
{
calendars: {
uid: calendar,
uid: calendar
}
}
* Calendars are listmap
{
content: {},
metadata: {
title: "pewpewpew"
}
}
ctx.calendars[channel] = {
lm: lm,
proxy: lm.proxy?
stores: [teamId, teamId, 1]
}
* calendar app can subscribe to this module
* when a listmap changes, push an update for this calendar to subscribed tabs
* Ability to open a calendar not stored in the stores but from its URL directly
* No "userlist" visible in the UI
* No framework
*/
var getStore = function (ctx, id) { var getStore = function (ctx, id) {
if (!id || id === 1) { if (!id || id === 1) {
return ctx.store; return ctx.store;
@ -109,6 +55,15 @@ ctx.calendars[channel] = {
}, ctx.clients); }, ctx.clients);
}; };
var clearReminders = function (ctx, id) {
var calendar = ctx.calendars[id];
if (!calendar || !calendar.reminders) { return; }
// Clear existing reminders
Object.keys(calendar.reminders).forEach(function (uid) {
if (!Array.isArray(calendar.reminders[uid])) { return; }
calendar.reminders[uid].forEach(function (to) { clearTimeout(to); });
});
};
var closeCalendar = function (ctx, id) { var closeCalendar = function (ctx, id) {
var ctxCal = ctx.calendars[id]; var ctxCal = ctx.calendars[id];
if (!ctxCal) { return; } if (!ctxCal) { return; }
@ -116,6 +71,7 @@ ctx.calendars[channel] = {
// If the calendar doesn't exist in any other team, stop it and delete it from ctx // If the calendar doesn't exist in any other team, stop it and delete it from ctx
if (!ctxCal.stores.length) { if (!ctxCal.stores.length) {
ctxCal.lm.stop(); ctxCal.lm.stop();
clearReminders(ctx, id);
delete ctx.calendars[id]; delete ctx.calendars[id];
} }
}; };
@ -133,6 +89,105 @@ ctx.calendars[channel] = {
if (cal.title !== data.title) { cal.title = data.title; } if (cal.title !== data.title) { cal.title = data.title; }
}); });
}; };
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var now = +new Date();
var ev = Util.clone(_ev);
var uid = ev.id;
// Clear reminders for this event
if (Array.isArray(reminders[uid])) {
reminders[uid].forEach(function (to) { clearTimeout(to); });
}
reminders[uid] = [];
var last = ctx.store.data.lastVisit;
if (ev.isAllDay) {
if (ev.startDay) { ev.start = +new Date(ev.startDay); }
if (ev.endDay) {
var endDate = new Date(ev.endDay);
endDate.setHours(23);
endDate.setMinutes(59);
endDate.setSeconds(59);
ev.end = +endDate;
}
}
// XXX add a limit to make sure we don't go too far in the past?
var missed = useLastVisit && ev.start > last && ev.end <= now;
if (ev.end <= now && !missed) {
// No reminder for past events
delete reminders[uid];
return;
}
var send = function () {
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
if (hide) { return; }
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
ctx.store.mailbox.showMessage('reminders', {
msg: {
ctime: ctime,
type: "REMINDER",
missed: Boolean(missed),
content: ev
},
hash: 'REMINDER|'+uid
}, null, function () {
});
};
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
var notifs = ev.reminders || [];
notifs.sort(function (a, b) {
return a - b;
});
notifs.some(function (delayMinutes) {
var delay = delayMinutes * 60000;
var time = now + delay;
// setTimeout only work with 32bit timeout values. If the event is too far away,
// ignore this event for now
// FIXME: call this function again in xxx days to reload these missing timeout?
if (ev.start - time >= 2147483647) { return true; }
// If we're too late to send a notification, send it instantly and ignore
// all notifications that were supposed to be sent even earlier
if (ev.start <= time) {
sendNotif();
return true;
}
// It starts in more than "delay": prepare the notification
reminders[uid].push(setTimeout(function () {
sendNotif();
}, (ev.start - time)));
});
};
var addReminders = function (ctx, id, ev) {
var calendar = ctx.calendars[id];
if (!calendar || !calendar.reminders) { return; }
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
updateEventReminders(ctx, calendar.reminders, ev);
};
var addInitialReminders = function (ctx, id, useLastVisit) {
var calendar = ctx.calendars[id];
if (!calendar || !calendar.reminders) { return; }
if (Object.keys(calendar.reminders).length) { return; } // Already initialized
// No reminders for calendars not stored
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
// Re-add all reminders
var content = Util.find(calendar, ['proxy', 'content']);
if (!content) { return; }
Object.keys(content).forEach(function (uid) {
updateEventReminders(ctx, calendar.reminders, content[uid], useLastVisit);
});
};
var openChannel = function (ctx, cfg, _cb) { var openChannel = function (ctx, cfg, _cb) {
var cb = Util.once(Util.mkAsync(_cb || function () {})); var cb = Util.once(Util.mkAsync(_cb || function () {}));
var teamId = cfg.storeId; var teamId = cfg.storeId;
@ -192,6 +247,7 @@ ctx.calendars[channel] = {
readOnly: !data.href, readOnly: !data.href,
stores: [teamId], stores: [teamId],
roStores: data.href ? [] : [teamId], roStores: data.href ? [] : [teamId],
reminders: {},
hashes: {} hashes: {}
}; };
@ -231,6 +287,7 @@ ctx.calendars[channel] = {
if (c.lm) { c.lm.stop(); } if (c.lm) { c.lm.stop(); }
c.stores = []; c.stores = [];
sendUpdate(ctx, c); sendUpdate(ctx, c);
clearReminders(ctx, channel);
delete ctx.calendars[channel]; delete ctx.calendars[channel];
}; };
@ -289,6 +346,7 @@ ctx.calendars[channel] = {
c.cacheready = true; c.cacheready = true;
setTimeout(update); setTimeout(update);
if (cb) { cb(null, lm.proxy); } if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('ready', function (info) { }).on('ready', function (info) {
var md = info.metadata; var md = info.metadata;
c.owners = md.owners || []; c.owners = md.owners || [];
@ -305,9 +363,25 @@ ctx.calendars[channel] = {
} }
setTimeout(update); setTimeout(update);
if (cb) { cb(null, lm.proxy); } if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () { }).on('change', [], function () {
if (!c.ready) { return; } if (!c.ready) { return; }
setTimeout(update); setTimeout(update);
}).on('change', ['content'], function (o, n, p) {
if (p.length === 2 && n && !o) { // New event
addReminders(ctx, channel, n);
}
if (p.length === 2 && !n && o) { // Deleted event
addReminders(ctx, channel, {
id: p[1],
start: 0
});
}
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
}).on('change', ['metadata'], function () { }).on('change', ['metadata'], function () {
// if title or color have changed, update our local values // if title or color have changed, update our local values
var md = proxy.metadata; var md = proxy.metadata;
@ -402,6 +476,7 @@ ctx.calendars[channel] = {
decryptTeamCalendarHref(store, cal); decryptTeamCalendarHref(store, cal);
openChannel(ctx, { openChannel(ctx, {
storeId: storeId, storeId: storeId,
lastVisitNotif: true,
data: cal data: cal
}); });
}); });
@ -434,6 +509,23 @@ ctx.calendars[channel] = {
}); });
}; };
var importICSCalendar = function (ctx, data, cId, cb) {
var id = data.id;
var c = ctx.calendars[id];
if (!c || !c.proxy) { return void cb({error: "ENOENT"}); }
var json = data.json;
c.proxy.content = c.proxy.content || {};
Object.keys(json).forEach(function (uid) {
c.proxy.content[uid] = json[uid];
addReminders(ctx, id, json[uid]);
});
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
sendUpdate(ctx, c);
cb();
});
};
var openCalendar = function (ctx, data, cId, cb) { var openCalendar = function (ctx, data, cId, cb) {
var secret = Hash.getSecrets('calendar', data.hash, data.password); var secret = Hash.getSecrets('calendar', data.hash, data.password);
var hash = Hash.getEditHashFromKeys(secret); var hash = Hash.getEditHashFromKeys(secret);
@ -638,6 +730,7 @@ ctx.calendars[channel] = {
c.proxy.content[data.id] = data; c.proxy.content[data.id] = data;
Realtime.whenRealtimeSyncs(c.lm.realtime, function () { Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, data);
sendUpdate(ctx, c); sendUpdate(ctx, c);
cb(); cb();
}); });
@ -686,6 +779,18 @@ ctx.calendars[channel] = {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor()); Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); } if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
}).nThen(function () { }).nThen(function () {
if (newC) {
// Move reminders to the new calendar
addReminders(ctx, id, {
id: ev.id,
start: 0
});
addReminders(ctx, ev.calendarId, ev);
} else if (changes.start || changes.reminders || changes.isAllDay) {
// Update reminders
addReminders(ctx, id, ev);
}
sendUpdate(ctx, c); sendUpdate(ctx, c);
if (newC) { sendUpdate(ctx, newC); } if (newC) { sendUpdate(ctx, newC); }
cb(); cb();
@ -698,6 +803,10 @@ ctx.calendars[channel] = {
c.proxy.content = c.proxy.content || {}; c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id]; delete c.proxy.content[data.id];
Realtime.whenRealtimeSyncs(c.lm.realtime, function () { Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, {
id: data.id,
start: 0
});
sendUpdate(ctx, c); sendUpdate(ctx, c);
cb(); cb();
}); });
@ -721,7 +830,7 @@ ctx.calendars[channel] = {
emit: emit, emit: emit,
onReady: Util.mkEvent(true), onReady: Util.mkEvent(true),
calendars: {}, calendars: {},
clients: [], clients: []
}; };
initializeCalendars(ctx, waitFor(function (err) { initializeCalendars(ctx, waitFor(function (err) {
@ -785,6 +894,10 @@ ctx.calendars[channel] = {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void importCalendar(ctx, data, clientId, cb); return void importCalendar(ctx, data, clientId, cb);
} }
if (cmd === 'IMPORT_ICS') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void importICSCalendar(ctx, data, clientId, cb);
}
if (cmd === 'ADD') { if (cmd === 'ADD') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void addCalendar(ctx, data, clientId, cb); return void addCalendar(ctx, data, clientId, cb);

@ -169,6 +169,18 @@ proxy.mailboxes = {
var dismiss = function (ctx, data, cId, cb) { var dismiss = function (ctx, data, cId, cb) {
var type = data.type; var type = data.type;
var hash = data.hash; var hash = data.hash;
// Reminder messages don't persist
if (/^REMINDER\|/.test(hash)) {
cb();
delete ctx.boxes.reminders.content[hash];
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
return;
}
var box = ctx.boxes[type]; var box = ctx.boxes[type];
if (!box) { return void cb({error: 'NOT_LOADED'}); } if (!box) { return void cb({error: 'NOT_LOADED'}); }
var m = box.data || {}; var m = box.data || {};
@ -488,7 +500,13 @@ proxy.mailboxes = {
msg: ctx.boxes[type].content[h], msg: ctx.boxes[type].content[h],
hash: h hash: h
}; };
showMessage(ctx, type, message, cId); showMessage(ctx, type, message, cId, function (obj) {
if (obj.error) { return; }
// Notify only if "requiresNotif" is true
if (!message.msg || !message.msg.requiresNotif) { return; }
Notify.system(undefined, obj.msg);
delete message.msg.requiresNotif;
});
}); });
}); });
// Subscribe to new notifications // Subscribe to new notifications
@ -528,6 +546,10 @@ proxy.mailboxes = {
initializeHistory(ctx); initializeHistory(ctx);
} }
ctx.boxes.reminders = {
content: {}
};
Object.keys(mailboxes).forEach(function (key) { Object.keys(mailboxes).forEach(function (key) {
if (TYPES.indexOf(key) === -1) { return; } if (TYPES.indexOf(key) === -1) { return; }
var m = mailboxes[key]; var m = mailboxes[key];
@ -568,6 +590,21 @@ proxy.mailboxes = {
}); });
}; };
mailbox.showMessage = function (type, msg, cId, cb) {
if (type === "reminders" && msg) {
ctx.boxes.reminders.content[msg.hash] = msg.msg;
if (!ctx.clients.length) {
ctx.boxes.reminders.content[msg.hash].requiresNotif = true;
}
// Hide existing messages for this event
hideMessage(ctx, type, msg.hash, ctx.clients);
}
showMessage(ctx, type, msg, cId, function (obj) {
Notify.system(undefined, obj.msg);
if (cb) { cb(); }
});
};
mailbox.open = function (key, m, cb, team, opts) { mailbox.open = function (key, m, cb, team, opts) {
if (TYPES.indexOf(key) === -1 && !team) { return; } if (TYPES.indexOf(key) === -1 && !team) { return; }
openChannel(ctx, key, m, cb, opts); openChannel(ctx, key, m, cb, opts);

@ -65,6 +65,8 @@ define([
if (/^LOCAL\|/.test(data.content.hash)) { if (/^LOCAL\|/.test(data.content.hash)) {
$(avatar).addClass('preview'); $(avatar).addClass('preview');
} }
} else if (data.type === 'reminders') {
avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
} else if (userData && typeof(userData) === "object" && userData.profile) { } else if (userData && typeof(userData) === "object" && userData.profile) {
avatar = h('span.cp-avatar'); avatar = h('span.cp-avatar');
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name); Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
@ -85,6 +87,15 @@ define([
if (typeof(data.content.getFormatText) === "function") { if (typeof(data.content.getFormatText) === "function") {
$(notif).find('.cp-notification-content p').html(data.content.getFormatText()); $(notif).find('.cp-notification-content p').html(data.content.getFormatText());
if (data.content.autorefresh) {
var it = setInterval(function () {
if (!data.content.autorefresh) {
clearInterval(it);
return;
}
$(notif).find('.cp-notification-content p').html(data.content.getFormatText());
}, 60000);
}
} }
if (data.content.isClickable) { if (data.content.isClickable) {
@ -117,7 +128,7 @@ define([
// Call the onMessage handlers // Call the onMessage handlers
var isNotification = function (type) { var isNotification = function (type) {
return type === "notifications" || /^team-/.test(type) || type === "broadcast"; return type === "notifications" || /^team-/.test(type) || type === "broadcast" || type === "reminders";
}; };
var pushMessage = function (data, handler) { var pushMessage = function (data, handler) {
var todo = function (f) { var todo = function (f) {

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

File diff suppressed because one or more lines are too long

@ -95,6 +95,9 @@ define([
'kanban': [ // Msg.settings_cat_kanban 'kanban': [ // Msg.settings_cat_kanban
'cp-settings-kanban-tags', 'cp-settings-kanban-tags',
], ],
'notifications': [
'cp-settings-notif-calendar'
],
'subscription': { 'subscription': {
onClick: function() { onClick: function() {
var urls = common.getMetadataMgr().getPrivateData().accounts; var urls = common.getMetadataMgr().getPrivateData().accounts;
@ -1562,6 +1565,43 @@ define([
cb($d); cb($d);
}, true); }, true);
Messages.settings_notifCalendarTitle = "Calendar notifications"; // XXX
Messages.settings_notifCalendarHint = "You can disable completely calendar notifications for incoming events.";
Messages.settings_notifCalendarCheckbox = "Enable calendar notifications";
makeBlock('notif-calendar', function(cb) { // Msg.settings_notifCalendarHint, .settings_notifCalendarTitle
var $cbox = $(UI.createCheckbox('cp-settings-cache',
Messages.settings_notifCalendarCheckbox,
false, { label: { class: 'noTitle' } }));
var spinner = UI.makeSpinner($cbox);
var $checkbox = $cbox.find('input').on('change', function() {
spinner.spin();
var val = !$checkbox.is(':checked');
common.setAttribute(['general', 'calendar', 'hideNotif'], val, function(e) {
if (e) {
console.error(e);
// error: restore previous value
if (val) { $checkbox.attr('checked', ''); }
else { $checkbox.attr('checked', 'checked'); }
spinner.hide();
return void console.error(e);
}
spinner.done();
});
});
common.getAttribute(['general', 'calendar', 'hideNotif'], function(e, val) {
if (e) { return void console.error(e); }
if (!val) {
$checkbox.attr('checked', 'checked');
}
});
cb($cbox[0]);
}, true);
// Settings app // Settings app
var createUsageButton = function() { var createUsageButton = function() {
@ -1591,8 +1631,10 @@ define([
subscription: 'fa fa-star-o', subscription: 'fa fa-star-o',
kanban: 'cptools cptools-kanban', kanban: 'cptools cptools-kanban',
style: 'cptools cptools-palette', style: 'cptools cptools-palette',
notifications: 'fa fa-bell'
}; };
Messages.settings_cat_notifications = Messages.notificationsPage;
var createLeftside = function() { var createLeftside = function() {
var $categories = $('<div>', { 'class': 'cp-sidebarlayout-categories' }) var $categories = $('<div>', { 'class': 'cp-sidebarlayout-categories' })
.appendTo(APP.$leftside); .appendTo(APP.$leftside);

Loading…
Cancel
Save