From 5ec4e39413399cdc9962b47c63a8e1f6834fa372 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 9 Apr 2021 18:10:13 +0200 Subject: [PATCH 01/24] Add basic reminders --- www/common/notifications.js | 19 +++ www/common/outer/calendar.js | 172 ++++++++++++++++++---------- www/common/outer/mailbox.js | 25 ++++ www/common/sframe-common-mailbox.js | 4 +- www/common/toolbar.js | 2 +- 5 files changed, 162 insertions(+), 60 deletions(-) diff --git a/www/common/notifications.js b/www/common/notifications.js index d364bad6c..018093c2a 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -459,6 +459,25 @@ define([ } }; + Messages.reminder_minutes = "{0} will start in {1} minutes!"; // XXX + Messages.reminder_hour = "{0} will start in 1 hour!"; // XXX + handlers['REMINDER'] = function (common, data) { + var content = data.content; + var msg = content.msg.content; + var now = +new Date(); + var start = msg.start; + content.getFormatText = function () { + if ((start - now) > 600000) { + return Messages._getKey('reminder_hour', [Util.fixHTML(msg.title)]); + } + var minutes = Math.round((start - now) / 60000); + return Messages._getKey('reminder_minutes', [Util.fixHTML(msg.title), minutes]); + }; + if (!content.archived) { + content.dismissHandler = defaultDismiss(common, data); + } + }; + // NOTE: don't forget to fixHTML everything returned by "getFormatText" return { diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index e02ad1412..b3d4c707a 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -12,60 +12,6 @@ define([ ], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) { 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) { if (!id || id === 1) { return ctx.store; @@ -109,6 +55,15 @@ ctx.calendars[channel] = { }, 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 ctxCal = ctx.calendars[id]; 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 (!ctxCal.stores.length) { ctxCal.lm.stop(); + clearReminders(ctx, id); delete ctx.calendars[id]; } }; @@ -133,12 +89,83 @@ ctx.calendars[channel] = { if (cal.title !== data.title) { cal.title = data.title; } }); }; + + var updateEventReminders = function (ctx, reminders, ev) { + var now = +new Date(); + var time10 = now + (600 * 1000); // 10 minutes from now + var time60 = now + (3600 * 1000); // 1 hour from now + var uid = ev.id; + + // Clear reminders for this event + if (Array.isArray(reminders[uid])) { + reminders[uid].forEach(function (to) { clearTimeout(to); }); + } + reminders[uid] = []; + + // No reminder for past events + if (ev.start <= now) { + delete reminders[uid]; + return; + } + + var send = function () { + console.error(ev); + ctx.store.mailbox.showMessage('reminders', { + msg: { + ctime: +new Date(), + type: "REMINDER", + content: ev + }, + hash: 'REMINDER|'+uid + }, null, function () { + }); + }; + var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); }; + + if (ev.start <= time10) { + sendNotif(); + return; + } + + // It starts in more than 10 minutes: prepare the 10 minutes notification + reminders[uid].push(setTimeout(function () { + sendNotif(); + }, (ev.start - time10))); + + if (ev.start <= time60) { return; } + + // It starts in more than 1 hour: prepare the 1 hour notification + reminders[uid].push(setTimeout(function () { + sendNotif(); + }, (ev.start - time60))); + }; + 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) { + 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]); + }); + }; var openChannel = function (ctx, cfg, _cb) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var teamId = cfg.storeId; var data = cfg.data; var channel = data.channel; - console.error(cfg); if (!channel) { return; } var c = ctx.calendars[channel]; @@ -193,6 +220,7 @@ ctx.calendars[channel] = { readOnly: !data.href, stores: [teamId], roStores: data.href ? [] : [teamId], + reminders: {}, hashes: {} }; @@ -232,6 +260,7 @@ ctx.calendars[channel] = { if (c.lm) { c.lm.stop(); } c.stores = []; sendUpdate(ctx, c); + clearReminders(ctx, channel); delete ctx.calendars[channel]; }; @@ -290,6 +319,7 @@ ctx.calendars[channel] = { c.cacheready = true; setTimeout(update); if (cb) { cb(null, lm.proxy); } + addInitialReminders(ctx, channel); }).on('ready', function (info) { var md = info.metadata; c.owners = md.owners || []; @@ -306,9 +336,25 @@ ctx.calendars[channel] = { } setTimeout(update); if (cb) { cb(null, lm.proxy); } + addInitialReminders(ctx, channel); }).on('change', [], function () { if (!c.ready) { return; } 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 () { // if title or color have changed, update our local values var md = proxy.metadata; @@ -627,6 +673,7 @@ ctx.calendars[channel] = { c.proxy.content = c.proxy.content || {}; c.proxy.content[data.id] = data; Realtime.whenRealtimeSyncs(c.lm.realtime, function () { + addReminders(ctx, id, data); sendUpdate(ctx, c); cb(); }); @@ -655,6 +702,7 @@ ctx.calendars[channel] = { ev[key] = changes[key]; }); + // Move to a different calendar? if (changes.calendarId && newC) { newC.proxy.content[data.ev.id] = Util.clone(ev); @@ -664,7 +712,10 @@ ctx.calendars[channel] = { nThen(function (waitFor) { Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor()); if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); } - }).nThen(cb); + }).nThen(function () { + if (changes.start) { addReminders(ctx, id, ev); } + cb(); + }); }; var deleteEvent = function (ctx, data, cId, cb) { var id = data.calendarId; @@ -672,7 +723,12 @@ ctx.calendars[channel] = { if (!c) { return void cb({error: "ENOENT"}); } c.proxy.content = c.proxy.content || {}; delete c.proxy.content[data.id]; - Realtime.whenRealtimeSyncs(c.lm.realtime, cb); + Realtime.whenRealtimeSyncs(c.lm.realtime, function () { + addReminders(ctx, id, { + id: data.id, + start: 0 + }); + }); }; var removeClient = function (ctx, cId) { @@ -693,7 +749,7 @@ ctx.calendars[channel] = { emit: emit, onReady: Util.mkEvent(true), calendars: {}, - clients: [], + clients: [] }; initializeCalendars(ctx, waitFor(function (err) { diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 6605a55ee..eea275093 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -174,6 +174,18 @@ proxy.mailboxes = { var dismiss = function (ctx, data, cId, cb) { var type = data.type; 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]; if (!box) { return void cb({error: 'NOT_LOADED'}); } var m = box.data || {}; @@ -548,6 +560,10 @@ proxy.mailboxes = { initializeHistory(ctx); } + ctx.boxes.reminders = { + content: {} + }; + Object.keys(mailboxes).forEach(function (key) { if (TYPES.indexOf(key) === -1) { return; } var m = mailboxes[key]; @@ -588,6 +604,15 @@ proxy.mailboxes = { }); }; + mailbox.showMessage = function (type, msg, cId, cb) { + if (type === "reminders" && msg) { + ctx.boxes.reminders.content[msg.hash] = msg.msg; + // Hide existing messages for this event + hideMessage(ctx, type, msg.hash, ctx.clients); + } + showMessage(ctx, type, msg, cId, cb); + }; + mailbox.open = function (key, m, cb, team, opts) { if (TYPES.indexOf(key) === -1 && !team) { return; } openChannel(ctx, key, m, cb, opts); diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js index 861ff8d53..215a4866d 100644 --- a/www/common/sframe-common-mailbox.js +++ b/www/common/sframe-common-mailbox.js @@ -65,6 +65,8 @@ define([ if (/^LOCAL\|/.test(data.content.hash)) { $(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) { avatar = h('span.cp-avatar'); Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name); @@ -117,7 +119,7 @@ define([ // Call the onMessage handlers 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 todo = function (f) { diff --git a/www/common/toolbar.js b/www/common/toolbar.js index e2ad32ea6..583a9168a 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -1148,7 +1148,7 @@ MessengerUI, Messages) { $button.addClass('fa-bell'); }; - Common.mailbox.subscribe(['notifications', 'team', 'broadcast'], { + Common.mailbox.subscribe(['notifications', 'team', 'broadcast', 'reminders'], { onMessage: function (data, el) { if (el) { $(div).prepend(el); From 8ebc71cd963b3736de9ee3da0f7ac7976f186289 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 12 Apr 2021 11:19:12 +0200 Subject: [PATCH 02/24] Disable calendar notifications --- www/common/outer/calendar.js | 3 ++- www/common/outer/mailbox.js | 10 +++++++-- www/settings/inner.js | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index b3d4c707a..07640a711 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -109,7 +109,8 @@ define([ } var send = function () { - console.error(ev); + var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']); + if (hide) { console.error('hidden'); return; } ctx.store.mailbox.showMessage('reminders', { msg: { ctime: +new Date(), diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index eea275093..4c652e292 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -520,7 +520,10 @@ proxy.mailboxes = { msg: ctx.boxes[type].content[h], hash: h }; - showMessage(ctx, type, message, cId); + showMessage(ctx, type, message, cId, function (obj) { + Notify.system(undefined, obj.msg); + cb(); + }); }); }); // Subscribe to new notifications @@ -610,7 +613,10 @@ proxy.mailboxes = { // Hide existing messages for this event hideMessage(ctx, type, msg.hash, ctx.clients); } - showMessage(ctx, type, msg, cId, cb); + showMessage(ctx, type, msg, cId, function (obj) { + Notify.system(undefined, obj.msg); + if (cb) { cb(); } + }); }; mailbox.open = function (key, m, cb, team, opts) { diff --git a/www/settings/inner.js b/www/settings/inner.js index 5404d399c..338b1ccc6 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -95,6 +95,9 @@ define([ 'kanban': [ // Msg.settings_cat_kanban 'cp-settings-kanban-tags', ], + 'notifications': [ + 'cp-settings-notif-calendar' + ], 'subscription': { onClick: function() { var urls = common.getMetadataMgr().getPrivateData().accounts; @@ -1566,6 +1569,43 @@ define([ cb($d); }, true); + Messages.settings_notifCalendarTitle = "Calendar notifications"; + 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 var createUsageButton = function() { @@ -1595,8 +1635,10 @@ define([ subscription: 'fa fa-star-o', kanban: 'cptools cptools-kanban', style: 'cptools cptools-palette', + notifications: 'fa fa-bell' }; + Messages.settings_cat_notifications = Messages.notificationsPage; var createLeftside = function() { var $categories = $('
', { 'class': 'cp-sidebarlayout-categories' }) .appendTo(APP.$leftside); From f3fd7011f831a0fdcbc06a407b799fba4dab0efd Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 12 Apr 2021 11:21:40 +0200 Subject: [PATCH 03/24] lint compliance --- www/common/outer/mailbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 4c652e292..34af071d6 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -183,7 +183,7 @@ proxy.mailboxes = { return clientId !== cId; })); return; - }; + } var box = ctx.boxes[type]; From a6c5ddf1f3daac7427fb95bb6f64883cdabed635 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 12 Apr 2021 14:16:42 +0200 Subject: [PATCH 04/24] Remove a console log --- www/common/outer/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 07640a711..e66db22a2 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -110,7 +110,7 @@ define([ var send = function () { var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']); - if (hide) { console.error('hidden'); return; } + if (hide) { return; } ctx.store.mailbox.showMessage('reminders', { msg: { ctime: +new Date(), From e5d923e52107c5db4dcd8daa99e69447899f65b6 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 12 Apr 2021 16:38:39 +0200 Subject: [PATCH 05/24] Fix calendar notifications timeout --- www/common/outer/calendar.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index e66db22a2..016e95abb 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -128,6 +128,11 @@ define([ return; } + // setTimeout only work with 32bit timeout values. + // FIXME: call this function again in xxx days to reload these missing timeout? + if (ev.start - time10 >= 2147483647) { return; } + + // It starts in more than 10 minutes: prepare the 10 minutes notification reminders[uid].push(setTimeout(function () { sendNotif(); From 11c0b752bde01ec9f5d4bf5f20e566f3d9d0904e Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 12 Apr 2021 17:08:18 +0200 Subject: [PATCH 06/24] Add notifications for past events since our last visit --- www/common/cryptpad-common.js | 9 ++++++++- www/common/notifications.js | 22 ++++++++++++++++++++-- www/common/outer/calendar.js | 29 +++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index b639eebab..51ec09ef6 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -2288,7 +2288,8 @@ define([ cache: rdyCfg.cache, noDrive: rdyCfg.noDrive, 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; @@ -2570,6 +2571,12 @@ define([ AppConfig.afterLogin(common, waitFor()); } }).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); if (typeof(window.onhashchange) === 'function') { window.onhashchange(); } }); diff --git a/www/common/notifications.js b/www/common/notifications.js index 018093c2a..35ebb447c 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -459,19 +459,37 @@ define([ } }; + Messages.reminder_missed = "You missed {0} on {1}"; // XXX + Messages.reminder_inProgress = "{0} has started on {1}"; // XXX + Messages.reminder_inProgressAllDay = "{0} is happening today"; // XXX Messages.reminder_minutes = "{0} will start in {1} minutes!"; // XXX Messages.reminder_hour = "{0} will start in 1 hour!"; // XXX handlers['REMINDER'] = function (common, data) { var content = data.content; var msg = content.msg.content; + var missed = content.msg.missed; var now = +new Date(); var start = msg.start; + var title = Util.fixHTML(msg.title); content.getFormatText = function () { + // Missed events + if (start < now && missed) { + return Messages._getKey('reminder_missed', [title, new Date(start).toLocaleString()]); + } + // 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, new Date(start).toLocaleString()]); + } + // Not started yet if ((start - now) > 600000) { - return Messages._getKey('reminder_hour', [Util.fixHTML(msg.title)]); + return Messages._getKey('reminder_hour', [title]); } var minutes = Math.round((start - now) / 60000); - return Messages._getKey('reminder_minutes', [Util.fixHTML(msg.title), minutes]); + return Messages._getKey('reminder_minutes', [title, minutes]); }; if (!content.archived) { content.dismissHandler = defaultDismiss(common, data); diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 016e95abb..2113dcc69 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -90,31 +90,43 @@ define([ }); }; - var updateEventReminders = function (ctx, reminders, ev) { + var updateEventReminders = function (ctx, reminders, ev, useLastVisit) { var now = +new Date(); var time10 = now + (600 * 1000); // 10 minutes from now var time60 = now + (3600 * 1000); // 1 hour from now var uid = ev.id; + ctx.store.data.lastVisit = 1617922639683; // XXX Friday Apr 09 + // Clear reminders for this event if (Array.isArray(reminders[uid])) { reminders[uid].forEach(function (to) { clearTimeout(to); }); } reminders[uid] = []; - // No reminder for past events - if (ev.start <= now) { + var last = ctx.store.data.lastVisit; + // 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; } + // XXX + // TODO + // use custom notifications per event + // if you missed a notification, show it instantly (eg: load cryptpad 45min before an event, show the 1h notification) + 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: +new Date(), + ctime: ctime, type: "REMINDER", + missed: Boolean(missed), content: ev }, hash: 'REMINDER|'+uid @@ -152,7 +164,7 @@ define([ updateEventReminders(ctx, calendar.reminders, ev); }; - var addInitialReminders = function (ctx, id) { + 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 @@ -164,7 +176,7 @@ define([ var content = Util.find(calendar, ['proxy', 'content']); if (!content) { return; } Object.keys(content).forEach(function (uid) { - updateEventReminders(ctx, calendar.reminders, content[uid]); + updateEventReminders(ctx, calendar.reminders, content[uid], useLastVisit); }); }; var openChannel = function (ctx, cfg, _cb) { @@ -325,7 +337,7 @@ define([ c.cacheready = true; setTimeout(update); if (cb) { cb(null, lm.proxy); } - addInitialReminders(ctx, channel); + addInitialReminders(ctx, channel, cfg.lastVisitNotif); }).on('ready', function (info) { var md = info.metadata; c.owners = md.owners || []; @@ -342,7 +354,7 @@ define([ } setTimeout(update); if (cb) { cb(null, lm.proxy); } - addInitialReminders(ctx, channel); + addInitialReminders(ctx, channel, cfg.lastVisitNotif); }).on('change', [], function () { if (!c.ready) { return; } setTimeout(update); @@ -455,6 +467,7 @@ define([ decryptTeamCalendarHref(store, cal); openChannel(ctx, { storeId: storeId, + lastVisitNotif: true, data: cal }); }); From 362a439cb548ad6ebaf22dd0983b66f66bd64ba7 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 12 Apr 2021 17:55:56 +0200 Subject: [PATCH 07/24] Support custom event notifications --- www/common/notifications.js | 40 ++++++++++++++++++++++++----- www/common/outer/calendar.js | 38 ++++++++++++++------------- www/common/sframe-common-mailbox.js | 9 +++++++ 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/www/common/notifications.js b/www/common/notifications.js index 35ebb447c..f797a984b 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -460,22 +460,37 @@ define([ }; Messages.reminder_missed = "You missed {0} on {1}"; // XXX + Messages.reminder_now = "{0} is starting!"; // XXX Messages.reminder_inProgress = "{0} has started on {1}"; // XXX Messages.reminder_inProgressAllDay = "{0} is happening today"; // XXX Messages.reminder_minutes = "{0} will start in {1} minutes!"; // XXX - Messages.reminder_hour = "{0} will start in 1 hour!"; // XXX + Messages.reminder_time = "{0} will start today at {1}!"; // XXX + Messages.reminder_date = "{0} will start on {1}!"; // XXX + var getDate = function (time) { + return new Date(time).toLocaleDateString(); + }; handlers['REMINDER'] = function (common, data) { var content = data.content; var msg = content.msg.content; var missed = content.msg.missed; - var now = +new Date(); var start = msg.start; var title = Util.fixHTML(msg.title); + var i = 0; content.getFormatText = function () { + var now = +new Date(); + + // Events that have already started + var wasRefresh = content.autorefresh; + content.autorefresh = false; + // Missed events if (start < now && missed) { return Messages._getKey('reminder_missed', [title, new Date(start).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]); @@ -484,12 +499,25 @@ define([ if (start < now) { return Messages._getKey('reminder_inProgress', [title, new Date(start).toLocaleString()]); } + // Not started yet - if ((start - now) > 600000) { - return Messages._getKey('reminder_hour', [title]); + + // 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 + var nowDateStr = new Date().toLocaleDateString(); + var startDate = new Date(start); + if (nowDateStr !== startDate.toLocaleDateString()) { + return Messages._getKey('reminder_date', [title, startDate.toLocaleString()]); } - var minutes = Math.round((start - now) / 60000); - return Messages._getKey('reminder_minutes', [title, minutes]); + + // Today: show time + return Messages._getKey('reminder_time', [title, startDate.toLocaleTimeString()]); }; if (!content.archived) { content.dismissHandler = defaultDismiss(common, data); diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 2113dcc69..c23b647c3 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -96,7 +96,7 @@ define([ var time60 = now + (3600 * 1000); // 1 hour from now var uid = ev.id; - ctx.store.data.lastVisit = 1617922639683; // XXX Friday Apr 09 + //ctx.store.data.lastVisit = 1617922639683; // XXX Friday Apr 09, used to test // Clear reminders for this event if (Array.isArray(reminders[uid])) { @@ -135,27 +135,29 @@ define([ }; var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); }; - if (ev.start <= time10) { - sendNotif(); - return; - } - - // setTimeout only work with 32bit timeout values. - // FIXME: call this function again in xxx days to reload these missing timeout? - if (ev.start - time10 >= 2147483647) { return; } + var notifs = [600000, 3600000]; // 10min, 60min + notifs.sort(); + notifs.some(function (delay) { + var time = now + delay; - // It starts in more than 10 minutes: prepare the 10 minutes notification - reminders[uid].push(setTimeout(function () { - sendNotif(); - }, (ev.start - time10))); + // 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 (ev.start <= time60) { return; } + // 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 1 hour: prepare the 1 hour notification - reminders[uid].push(setTimeout(function () { - sendNotif(); - }, (ev.start - time60))); + // 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]; diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js index 215a4866d..7ea717d35 100644 --- a/www/common/sframe-common-mailbox.js +++ b/www/common/sframe-common-mailbox.js @@ -87,6 +87,15 @@ define([ if (typeof(data.content.getFormatText) === "function") { $(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) { From 656ebf432e8acfe3fde81acc8a075608e1bb2077 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 10:35:22 +0200 Subject: [PATCH 08/24] Add XXX --- www/common/notifications.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/www/common/notifications.js b/www/common/notifications.js index f797a984b..2a7f31fbf 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -483,21 +483,27 @@ define([ var wasRefresh = content.autorefresh; content.autorefresh = false; + var nowDateStr = new Date().toLocaleDateString(); + var startDate = new Date(start); + // Missed events if (start < now && missed) { - return Messages._getKey('reminder_missed', [title, new Date(start).toLocaleString()]); + 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 + // XXX fix this... + // XXX start and end time may not matter... + // XXX and timezone? if (start < now && msg.isAllDay) { return Messages._getKey('reminder_inProgressAllDay', [title]); } // In progress, normal event if (start < now) { - return Messages._getKey('reminder_inProgress', [title, new Date(start).toLocaleString()]); + return Messages._getKey('reminder_inProgress', [title, startDate.toLocaleString()]); } // Not started yet @@ -510,8 +516,6 @@ define([ } // Not today: show full date - var nowDateStr = new Date().toLocaleDateString(); - var startDate = new Date(start); if (nowDateStr !== startDate.toLocaleDateString()) { return Messages._getKey('reminder_date', [title, startDate.toLocaleString()]); } From 95c16f5c6559b165c82a3c4225131b93b55c8019 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 15:21:24 +0200 Subject: [PATCH 09/24] Custom reminders --- customize.dist/src/less2/include/forms.less | 2 +- www/calendar/app-calendar.less | 35 +++++ www/calendar/inner.js | 143 +++++++++++++++++++- www/common/outer/calendar.js | 7 +- www/settings/inner.js | 2 +- 5 files changed, 181 insertions(+), 8 deletions(-) diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 969c873c7..ffe061fa3 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -122,7 +122,7 @@ &.small { line-height: initial; padding: 5px; - height: auto; + height: auto !important; } &:hover, &:not(:disabled):not(.disabled):active, &:focus { diff --git a/www/calendar/app-calendar.less b/www/calendar/app-calendar.less index bc0f47a48..daf1321a7 100644 --- a/www/calendar/app-calendar.less +++ b/www/calendar/app-calendar.less @@ -161,6 +161,41 @@ } } + .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; + } + } + .cp-calendar-close { height: auto; line-height: initial; diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 830674341..7000df482 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -1,5 +1,6 @@ define([ 'jquery', + 'json.sortify', '/bower_components/chainpad-crypto/crypto.js', '/common/toolbar.js', '/bower_components/nthen/index.js', @@ -26,6 +27,7 @@ define([ 'less!/calendar/app-calendar.less', ], function ( $, + JSONSortify, Crypto, Toolbar, nThen, @@ -76,6 +78,14 @@ Messages.calendar_loc = "Location"; Messages.calendar_location = "Location: {0}"; 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 newCalendar = function (data, cb) { @@ -225,8 +235,15 @@ Messages.calendar_allDay = "All day"; })()) { getTime = undefined; } var templates = { - popupSave: function () { return Messages.settings_save; }, - popupUpdate: function() { return Messages.calendar_update; }, + // XXX find template for "xxx more" in month view + 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; + }, popupEdit: function() { return Messages.poll_edit; }, popupDelete: function() { return Messages.kanban_delete; }, popupDetailLocation: function(schedule) { @@ -490,7 +507,7 @@ Messages.calendar_allDay = "All day"; if (cal.owned) { key += Messages.calendar_deleteOwned; } - UI.confirm(Messages.calendar_deleteConfirm, function (yes) { + UI.confirm(key, function (yes) { if (!yes) { return; } deleteCalendar({ id: id, @@ -671,6 +688,7 @@ Messages.calendar_allDay = "All day"; // ie: recurrenceRule: DAILY|{uid} // Use template to hide "recurrenceRule" from the detailPopup or at least to use // a non technical value + var reminders = APP.notificationsEntries; var schedule = { id: Util.uid(), @@ -681,6 +699,7 @@ Messages.calendar_allDay = "All day"; start: event.start, isAllDay: event.isAllDay, end: event.end, + reminders: reminders }; newEvent(schedule, function (err) { @@ -698,6 +717,12 @@ Messages.calendar_allDay = "All day"; if (changes.start) { changes.start = +new Date(changes.start._date); } 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({ ev: old, changes: changes @@ -791,6 +816,114 @@ 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 list = []; + 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 displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications']; var configTb = { @@ -877,6 +1010,10 @@ Messages.calendar_allDay = "All day"; } var isUpdate = Boolean($el.find('#tui-full-calendar-schedule-title').val()); 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 onCalendarEditPopup = function (el) { var $el = $(el); diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 11516e2ca..33bf7390b 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -135,10 +135,11 @@ define([ }; var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); }; - var notifs = [600000, 3600000]; // 10min, 60min + var notifs = ev.reminders || []; notifs.sort(); - notifs.some(function (delay) { + 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, @@ -734,7 +735,7 @@ define([ Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor()); if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); } }).nThen(function () { - if (changes.start) { addReminders(ctx, id, ev); } + if (changes.start || changes.reminders) { addReminders(ctx, id, ev); } sendUpdate(ctx, c); if (newC) { sendUpdate(ctx, newC); } cb(); diff --git a/www/settings/inner.js b/www/settings/inner.js index 7152994fd..85aa27c10 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -1565,7 +1565,7 @@ define([ cb($d); }, true); - Messages.settings_notifCalendarTitle = "Calendar notifications"; + Messages.settings_notifCalendarTitle = "Calendar notifications"; // XXX Messages.settings_notifCalendarHint = "You can disable completely calendar notifications for incoming events."; Messages.settings_notifCalendarCheckbox = "Enable calendar notifications"; From 1526f933edff237a9393d4dada96ad3780e18d2a Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 15:24:22 +0200 Subject: [PATCH 10/24] lint compliance --- www/calendar/inner.js | 1 - www/common/notifications.js | 4 ---- www/common/outer/calendar.js | 2 -- 3 files changed, 7 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 7000df482..881d6ee2d 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -844,7 +844,6 @@ Messages.calendar_noNotification = "None"; max: 60 }); var $number = $(number); - var list = []; var options = ['minutes', 'hours', 'days'].map(function (k) { return { tag: 'a', diff --git a/www/common/notifications.js b/www/common/notifications.js index 46c92a147..2c6a4b24c 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -468,16 +468,12 @@ define([ Messages.reminder_minutes = "{0} will start in {1} minutes!"; // XXX Messages.reminder_time = "{0} will start today at {1}!"; // XXX Messages.reminder_date = "{0} will start on {1}!"; // XXX - var getDate = function (time) { - return new Date(time).toLocaleDateString(); - }; 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); - var i = 0; content.getFormatText = function () { var now = +new Date(); diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 33bf7390b..88ea20766 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -92,8 +92,6 @@ define([ var updateEventReminders = function (ctx, reminders, ev, useLastVisit) { var now = +new Date(); - var time10 = now + (600 * 1000); // 10 minutes from now - var time60 = now + (3600 * 1000); // 1 hour from now var uid = ev.id; //ctx.store.data.lastVisit = 1617922639683; // XXX Friday Apr 09, used to test From 70b201003c49175603e348a85817153020873fc9 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 15:31:06 +0200 Subject: [PATCH 11/24] Fix calendar notification UI --- www/calendar/app-calendar.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/calendar/app-calendar.less b/www/calendar/app-calendar.less index daf1321a7..77262ce12 100644 --- a/www/calendar/app-calendar.less +++ b/www/calendar/app-calendar.less @@ -193,6 +193,9 @@ } .cp-calendar-notif-form { align-items: center; + input { + width: 100px; + } } } From 1d9a05e782446cd41286a007f8d3914297fe22c5 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 16:46:04 +0200 Subject: [PATCH 12/24] Fix reminders --- www/common/outer/calendar.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 41fdc40bc..ed6da0531 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -754,7 +754,18 @@ define([ Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor()); if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); } }).nThen(function () { - if (changes.start || changes.reminders) { addReminders(ctx, id, ev); } + 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); if (newC) { sendUpdate(ctx, newC); } cb(); From a60e1078f9eeb6c4f47f2ae43a824e94a050ace3 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 16:54:46 +0200 Subject: [PATCH 13/24] Fix reminders for all day events --- www/common/notifications.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/www/common/notifications.js b/www/common/notifications.js index 2c6a4b24c..7a64815cc 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -483,6 +483,9 @@ define([ 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) { @@ -493,9 +496,6 @@ define([ return Messages._getKey('reminder_now', [title]); } // In progress, is all day - // XXX fix this... - // XXX start and end time may not matter... - // XXX and timezone? if (start < now && msg.isAllDay) { return Messages._getKey('reminder_inProgressAllDay', [title]); } @@ -506,6 +506,11 @@ define([ // 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); From 2163c5c31f1f7b2f64d2735f2b5ead2e6cff9a4f Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 17:44:35 +0200 Subject: [PATCH 14/24] Fix allDay events notifications --- www/common/outer/calendar.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index ed6da0531..9d399f310 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -90,8 +90,9 @@ define([ }); }; - var updateEventReminders = function (ctx, reminders, ev, useLastVisit) { + var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) { var now = +new Date(); + var ev = Util.clone(_ev); var uid = ev.id; //ctx.store.data.lastVisit = 1617922639683; // XXX Friday Apr 09, used to test @@ -103,6 +104,12 @@ define([ reminders[uid] = []; var last = ctx.store.data.lastVisit; + + if (ev.isAllDay) { + if (ev.startDay) { ev.start = +new Date(ev.startDay); } + if (ev.endDay) { ev.end = +new Date(ev.endDay); } + } + // 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) { From 867e0e8cd38209861f947c462c5a59d1a6f15a35 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 13 Apr 2021 17:45:18 +0200 Subject: [PATCH 15/24] Fix event creation --- www/common/outer/calendar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 9d399f310..cafa028f1 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -704,8 +704,8 @@ define([ data.startDay = startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate(); data.endDay = endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate(); } else { - delete ev.startDay; - delete ev.endDay; + delete data.startDay; + delete data.endDay; } c.proxy.content = c.proxy.content || {}; From a0b67cc364107bc5da4316b447343cb0fbbca0c9 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 14 Apr 2021 11:09:21 +0200 Subject: [PATCH 16/24] Fix allDay reminders notifications end time --- www/common/outer/calendar.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index cafa028f1..90d0e9c6b 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -107,7 +107,13 @@ define([ if (ev.isAllDay) { if (ev.startDay) { ev.start = +new Date(ev.startDay); } - if (ev.endDay) { ev.end = +new Date(ev.endDay); } + if (ev.endDay) { + var endDate = new Date(obj.endDay); + endDate.setHours(23); + endDate.setMinutes(59); + endDate.setSeconds(59); + obj.end = +endDate; + } } // XXX add a limit to make sure we don't go too far in the past? From 6131e89505e7d81fd1324cce0d03b17678f8d877 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 14 Apr 2021 11:42:41 +0200 Subject: [PATCH 17/24] Fix type error in reminders --- www/common/outer/calendar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 90d0e9c6b..0d5fa1f2d 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -108,11 +108,11 @@ define([ if (ev.isAllDay) { if (ev.startDay) { ev.start = +new Date(ev.startDay); } if (ev.endDay) { - var endDate = new Date(obj.endDay); + var endDate = new Date(ev.endDay); endDate.setHours(23); endDate.setMinutes(59); endDate.setSeconds(59); - obj.end = +endDate; + ev.end = +endDate; } } From a2d52d7026c4e737f9992cf607d90ca512027914 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 14 Apr 2021 15:47:57 +0200 Subject: [PATCH 18/24] Fix undefined notifications --- www/common/outer/mailbox.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index fa8da5712..a7269c6e5 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -501,8 +501,11 @@ proxy.mailboxes = { hash: h }; 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); - cb(); + delete message.msg.requiresNotif; }); }); }); @@ -590,6 +593,9 @@ 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); } From 8a7f5556c44b8c00610c398229e1f8f3190ab1b4 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 11:10:11 +0200 Subject: [PATCH 19/24] Calendar export --- www/calendar/export.js | 121 +++++++++++++++++++++++++++++++++++++++++ www/calendar/inner.js | 63 +++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 www/calendar/export.js diff --git a/www/calendar/export.js b/www/calendar/export.js new file mode 100644 index 000000000..0d5921e73 --- /dev/null +++ b/www/calendar/export.js @@ -0,0 +1,121 @@ +// 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' + ]); + // XXX ACTION:EMAIL + // XXX ATTENDEE:mailto:xxx@xxx.xxx + // XXX SUMMARY:Alarm notification + }); + } + + // XXX add hidden data (from imports) + + ICS.push('END:VEVENT'); + }); + + ICS.push('END:VCALENDAR'); + + return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' }); + }; + + return module; +}); + + diff --git a/www/calendar/inner.js b/www/calendar/inner.js index f74f3cfaf..9986206f0 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -15,12 +15,14 @@ define([ '/customize/messages.js', '/customize/application_config.js', '/lib/calendar/tui-calendar.min.js', + '/calendar/export.js', '/common/inner/share.js', '/common/inner/access.js', '/common/inner/properties.js', '/common/jscolor.js', + '/bower_components/file-saver/FileSaver.min.js', 'css!/lib/calendar/tui-calendar.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/calendar/app-calendar.less', @@ -41,9 +43,11 @@ define([ Messages, AppConfig, Calendar, + Export, Share, Access, Properties ) { + var SaveAs = window.saveAs; var APP = window.APP = { calendars: {} }; @@ -453,6 +457,65 @@ Messages.calendar_allDay = "All day"; return true; } }); + + if (!data.readOnly) { + options.push({ + tag: 'a', + attributes: { + 'class': 'fa fa-upload', + }, + content: h('span', Messages.importButton), + action: function (e) { + e.stopPropagation(); + 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({ tag: 'a', attributes: { From bd2cfea3d9045b998f9ce86ee6bc90a401165c6b Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 11:11:46 +0200 Subject: [PATCH 20/24] Fix reminders order --- www/common/outer/calendar.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 0d5fa1f2d..3ae006d63 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -147,7 +147,9 @@ define([ var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); }; var notifs = ev.reminders || []; - notifs.sort(); + notifs.sort(function (a, b) { + return a - b; + }); notifs.some(function (delayMinutes) { var delay = delayMinutes * 60000; From 6ced7316002cb304db2cb9f22836054d237d1406 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 17:45:38 +0200 Subject: [PATCH 21/24] Calendar import --- www/calendar/export.js | 87 ++++++++++++++++++++++++++++++-- www/calendar/inner.js | 22 +++++++- www/common/common-ui-elements.js | 2 +- www/common/outer/calendar.js | 20 ++++++++ 4 files changed, 125 insertions(+), 6 deletions(-) diff --git a/www/calendar/export.js b/www/calendar/export.js index 0d5921e73..e470c5d33 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -99,13 +99,12 @@ define([ 'TRIGGER:'+str, 'END:VALARM' ]); - // XXX ACTION:EMAIL - // XXX ATTENDEE:mailto:xxx@xxx.xxx - // XXX SUMMARY:Alarm notification }); } - // XXX add hidden data (from imports) + if (Array.isArray(data.cp_hidden)) { + Array.prototype.push.apply(ICS, data.cp_hidden); + } ICS.push('END:VEVENT'); }); @@ -115,6 +114,86 @@ define([ 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') { + // XXX email: maybe keep a notification in CryptPad? + hidden.push(al.toString()); + return; + } + 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; }); diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 9986206f0..a33bff39c 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -107,6 +107,12 @@ Messages.calendar_allDay = "All day"; 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) { APP.module.execCommand('CREATE_EVENT', data, function (obj) { if (obj && obj.error) { return void cb(obj.error); } @@ -466,7 +472,21 @@ Messages.calendar_allDay = "All day"; }, content: h('span', Messages.importButton), action: function (e) { - e.stopPropagation(); + 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; } }); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6ec1cf420..578e716e3 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -127,7 +127,7 @@ define([ dcAlert = undefined; }; - var importContent = function (type, f, cfg) { + var importContent = UIElements.importContent = function (type, f, cfg) { return function () { var $files = $('', {type:"file"}); if (cfg && cfg.accept) { diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index b9f25462c..bd17b37f8 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -434,6 +434,22 @@ 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]; + }); + + Realtime.whenRealtimeSyncs(c.lm.realtime, function () { + sendUpdate(ctx, c); + cb(); + }); + }; + var openCalendar = function (ctx, data, cId, cb) { var secret = Hash.getSecrets('calendar', data.hash, data.password); var hash = Hash.getEditHashFromKeys(secret); @@ -785,6 +801,10 @@ ctx.calendars[channel] = { if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } 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 (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } return void addCalendar(ctx, data, clientId, cb); From bd23cf49fcbd80c9de75e819d47025a8720a1b0c Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 17:48:42 +0200 Subject: [PATCH 22/24] Update reminders when importing a calendar --- www/common/outer/calendar.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 1bdde4724..5f39004c1 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -95,8 +95,6 @@ define([ var ev = Util.clone(_ev); var uid = ev.id; - //ctx.store.data.lastVisit = 1617922639683; // XXX Friday Apr 09, used to test - // Clear reminders for this event if (Array.isArray(reminders[uid])) { reminders[uid].forEach(function (to) { clearTimeout(to); }); @@ -124,11 +122,6 @@ define([ return; } - // XXX - // TODO - // use custom notifications per event - // if you missed a notification, show it instantly (eg: load cryptpad 45min before an event, show the 1h notification) - var send = function () { var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']); if (hide) { return; } @@ -524,6 +517,7 @@ define([ 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 () { From 4c39664a66640ef535094eeded4d7e49ea0da4d7 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 17:58:37 +0200 Subject: [PATCH 23/24] Add missing file --- www/lib/ical.min.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 www/lib/ical.min.js diff --git a/www/lib/ical.min.js b/www/lib/ical.min.js new file mode 100644 index 000000000..e92401be0 --- /dev/null +++ b/www/lib/ical.min.js @@ -0,0 +1,2 @@ +"object"==typeof module?ICAL=module.exports:"object"!=typeof ICAL&&(this.ICAL={}),ICAL.foldLength=75,ICAL.newLineChar="\r\n",ICAL.helpers={updateTimezones:function(t){var e,r,n,i,a,s;if(!t||"vcalendar"!==t.name)return t;for(e=t.getAllSubcomponents(),r=[],n={},a=0;a>18&63,r=a>>12&63,n=a>>6&63,i=63&a,c[u++]=s.charAt(e)+s.charAt(r)+s.charAt(n)+s.charAt(i),o>16&255,r=s>>8&255,n=255&s,c[h++]=64==i?String.fromCharCode(e):64==a?String.fromCharCode(e,r):String.fromCharCode(e,r,n),u=this.changes.length)break}var s=this.changes[n];if(s.utcOffset-s.prevUtcOffset<0&&0=this.changes.length?this.changes.length-1:e},_ensureCoverage:function(t){if(-1==ICAL.Timezone._minimumExpansionYear){var e=ICAL.Time.now();ICAL.Timezone._minimumExpansionYear=e.year}var r=t;if(rICAL.Timezone.MAX_YEAR&&(r=ICAL.Timezone.MAX_YEAR),!this.changes.length||this.expandedUntilYeart)&&l);)n.year=l.year,n.month=l.month,n.day=l.day,n.hour=l.hour,n.minute=l.minute,n.second=l.second,n.isDate=l.isDate,ICAL.Timezone.adjust_change(n,0,0,0,-n.prevUtcOffset),r.push(n)}}else(n=s()).year=i.year,n.month=i.month,n.day=i.day,n.hour=i.hour,n.minute=i.minute,n.second=i.second,ICAL.Timezone.adjust_change(n,0,0,0,-n.prevUtcOffset),r.push(n);return r},toString:function(){return this.tznames?this.tznames:this.tzid}},ICAL.Timezone._compare_change_fn=function(t,e){return t.yeare.year?1:t.monthe.month?1:t.daye.day?1:t.houre.hour?1:t.minutee.minute?1:t.seconde.second?1:0},ICAL.Timezone.convert_time=function(t,e,r){if(t.isDate||e.tzid==r.tzid||e==ICAL.Timezone.localTimezone||r==ICAL.Timezone.localTimezone)return t.zone=r,t;var n=e.utcOffset(t);return t.adjust(0,0,0,-n),n=r.utcOffset(t),t.adjust(0,0,0,n),null},ICAL.Timezone.fromData=function(t){return(new ICAL.Timezone).fromData(t)},ICAL.Timezone.utcTimezone=ICAL.Timezone.fromData({tzid:"UTC"}),ICAL.Timezone.localTimezone=ICAL.Timezone.fromData({tzid:"floating"}),ICAL.Timezone.adjust_change=function(t,e,r,n,i){return ICAL.Time.prototype.adjust.call(t,e,r,n,i,t)},ICAL.Timezone._minimumExpansionYear=-1,ICAL.Timezone.MAX_YEAR=2035,ICAL.Timezone.EXTRA_COVERAGE=5}(),ICAL.TimezoneService=function(){var r,t={get count(){return Object.keys(r).length},reset:function(){r=Object.create(null);var t=ICAL.Timezone.utcTimezone;r.Z=t,r.UTC=t,r.GMT=t},has:function(t){return!!r[t]},get:function(t){return r[t]},register:function(t,e){if(t instanceof ICAL.Component&&"vtimezone"===t.name&&(t=(e=new ICAL.Timezone(t)).tzid),!(e instanceof ICAL.Timezone))throw new TypeError("timezone must be ICAL.Timezone or ICAL.Component");r[t]=e},remove:function(t){return delete r[t]}};return t.reset(),t}(),function(){function t(e){Object.defineProperty(ICAL.Time.prototype,e,{get:function(){return this._pendingNormalization&&(this._normalize(),this._pendingNormalization=!1),this._time[e]},set:function(t){return"isDate"===e&&t&&!this._time.isDate&&this.adjust(0,0,0,0),this._cachedUnixTime=null,this._pendingNormalization=!0,this._time[e]=t}})}ICAL.Time=function(t,e){var r=(this.wrappedJSObject=this)._time=Object.create(null);r.year=0,r.month=1,r.day=1,r.hour=0,r.minute=0,r.second=0,r.isDate=!1,this.fromData(t,e)},ICAL.Time._dowCache={},ICAL.Time._wnCache={},ICAL.Time.prototype={icalclass:"icaltime",_cachedUnixTime:null,get icaltype(){return this.isDate?"date":"date-time"},zone:null,_pendingNormalization:!1,clone:function(){return new ICAL.Time(this._time,this.zone)},reset:function(){this.fromData(ICAL.Time.epochTime),this.zone=ICAL.Timezone.utcTimezone},resetTo:function(t,e,r,n,i,a,s){this.fromData({year:t,month:e,day:r,hour:n,minute:i,second:a,zone:s})},fromJSDate:function(t,e){return t?e?(this.zone=ICAL.Timezone.utcTimezone,this.year=t.getUTCFullYear(),this.month=t.getUTCMonth()+1,this.day=t.getUTCDate(),this.hour=t.getUTCHours(),this.minute=t.getUTCMinutes(),this.second=t.getUTCSeconds()):(this.zone=ICAL.Timezone.localTimezone,this.year=t.getFullYear(),this.month=t.getMonth()+1,this.day=t.getDate(),this.hour=t.getHours(),this.minute=t.getMinutes(),this.second=t.getSeconds()):this.reset(),this._cachedUnixTime=null,this},fromData:function(t,e){if(t)for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r)){if("icaltype"===r)continue;this[r]=t[r]}if(e&&(this.zone=e),!t||"isDate"in t?t&&"isDate"in t&&(this.isDate=t.isDate):this.isDate=!("hour"in t),t&&"timezone"in t){var n=ICAL.TimezoneService.get(t.timezone);this.zone=n||ICAL.Timezone.localTimezone}return t&&"zone"in t&&(this.zone=t.zone),this.zone||(this.zone=ICAL.Timezone.localTimezone),this._cachedUnixTime=null,this},dayOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=(this.year<<12)+(this.month<<8)+(this.day<<3)+e;if(r in ICAL.Time._dowCache)return ICAL.Time._dowCache[r];var n=this.day,i=this.month+(this.month<3?12:0),a=this.year-(this.month<3?1:0),s=n+a+ICAL.helpers.trunc(26*(i+1)/10)+ICAL.helpers.trunc(a/4);return s=((s+=6*ICAL.helpers.trunc(a/100)+ICAL.helpers.trunc(a/400))+7-e)%7+1,ICAL.Time._dowCache[r]=s},dayOfYear:function(){var t=ICAL.Time.isLeapYear(this.year)?1:0;return ICAL.Time.daysInYearPassedMonth[t][this.month-1]+this.day},startOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.clone();return r.day-=(this.dayOfWeek()+7-e)%7,r.isDate=!0,r.hour=0,r.minute=0,r.second=0,r},endOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.clone();return r.day+=(7-this.dayOfWeek()+e-ICAL.Time.SUNDAY)%7,r.isDate=!0,r.hour=0,r.minute=0,r.second=0,r},startOfMonth:function(){var t=this.clone();return t.day=1,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},endOfMonth:function(){var t=this.clone();return t.day=ICAL.Time.daysInMonth(t.month,t.year),t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},startOfYear:function(){var t=this.clone();return t.day=1,t.month=1,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},endOfYear:function(){var t=this.clone();return t.day=31,t.month=12,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},startDoyWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.dayOfWeek()-e;return r<0&&(r+=7),this.dayOfYear()-r},getDominicalLetter:function(){return ICAL.Time.getDominicalLetter(this.year)},nthWeekDay:function(t,e){var r,n=ICAL.Time.daysInMonth(this.month,this.year),i=e,a=0,s=this.clone();if(0<=i){s.day=1,0!=i&&i--,a=s.day;var o=t-s.dayOfWeek();o<0&&(o+=7),a+=o,a-=t,r=t}else{s.day=n,i++,(r=s.dayOfWeek()-t)<0&&(r+=7),r=n-r}return a+(r+=7*i)},isNthWeekDay:function(t,e){var r=this.dayOfWeek();return 0===e&&r===t||this.nthWeekDay(t,e)===this.day},weekNumber:function(t){var e,r=(this.year<<12)+(this.month<<8)+(this.day<<3)+t;if(r in ICAL.Time._wnCache)return ICAL.Time._wnCache[r];var n=this.clone();n.isDate=!0;var i=this.year;12==n.month&&25ICAL.Time.daysInYearPassedMonth[a][12])return a=ICAL.Time.isLeapYear(r)?1:0,n-=ICAL.Time.daysInYearPassedMonth[a][12],r++,ICAL.Time.fromDayOfYear(n,r);i.year=r,i.isDate=!0;for(var s=11;0<=s;s--)if(n>ICAL.Time.daysInYearPassedMonth[a][s]){i.month=s+1,i.day=n-ICAL.Time.daysInYearPassedMonth[a][s];break}return i.auto_normalize=!0,i},ICAL.Time.fromStringv2=function(t){return new ICAL.Time({year:parseInt(t.substr(0,4),10),month:parseInt(t.substr(5,2),10),day:parseInt(t.substr(8,2),10),isDate:!0})},ICAL.Time.fromDateString=function(t){return new ICAL.Time({year:ICAL.helpers.strictParseInt(t.substr(0,4)),month:ICAL.helpers.strictParseInt(t.substr(5,2)),day:ICAL.helpers.strictParseInt(t.substr(8,2)),isDate:!0})},ICAL.Time.fromDateTimeString=function(t,e){if(t.length<19)throw new Error('invalid date-time value: "'+t+'"');var r;return t[19]&&"Z"===t[19]?r="Z":e&&(r=e.getParameter("tzid")),new ICAL.Time({year:ICAL.helpers.strictParseInt(t.substr(0,4)),month:ICAL.helpers.strictParseInt(t.substr(5,2)),day:ICAL.helpers.strictParseInt(t.substr(8,2)),hour:ICAL.helpers.strictParseInt(t.substr(11,2)),minute:ICAL.helpers.strictParseInt(t.substr(14,2)),second:ICAL.helpers.strictParseInt(t.substr(17,2)),timezone:r})},ICAL.Time.fromString=function(t,e){return 10ICAL.Time.THURSDAY&&(r.day+=7),i>ICAL.Time.THURSDAY&&(r.day-=7),r.day-=n-i,r},ICAL.Time.getDominicalLetter=function(t){var e="GFEDCBA",r=(t+(t/4|0)+(t/400|0)-(t/100|0)-1)%7;return ICAL.Time.isLeapYear(t)?e[(6+r)%7]+e[r]:e[r]},ICAL.Time.epochTime=ICAL.Time.fromData({year:1970,month:1,day:1,hour:0,minute:0,second:0,isDate:!1,timezone:"Z"}),ICAL.Time._cmp_attr=function(t,e,r){return t[r]>e[r]?1:t[r] '+e);if(void 0!==r&&rs||0==this.last.day)throw new Error("Malformed values in BYDAY part")}else if(this.has_by_data("BYMONTHDAY")&&this.last.day<0){s=ICAL.Time.daysInMonth(this.last.month,this.last.year);this.last.day=s+this.last.day+1}},next:function(){var t,e=this.last?this.last.clone():null;if(this.rule.count&&this.occurrence_number>=this.rule.count||this.rule.until&&0i)){if(n<0)n=i+(n+1);else if(0===n)continue;-1===a.indexOf(n)&&a.push(n)}return a.sort(function(t,e){return t-e})},_byDayAndMonthDay:function(t){var e,r,n,i,a=this.by_data.BYDAY,s=0,o=a.length,u=0,h=this,c=this.last.day;function l(){for(i=ICAL.Time.daysInMonth(h.last.month,h.last.year),e=h.normalizeByMonthDayRules(h.last.year,h.last.month,h.by_data.BYMONTHDAY),n=e.length;e[s]<=c&&(!t||e[s]!=c)&&s=this.by_data.BYMONTHDAY.length&&(this.by_indices.BYMONTHDAY=0,this.increment_month());e=ICAL.Time.daysInMonth(this.last.month,this.last.year);(a=this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY])<0&&(a=e+a+1),ee?t=0:this.last.day=this.by_data.BYMONTHDAY[0]}return t},next_weekday_by_week:function(){var t=0;if(0==this.next_hour())return t;if(!this.has_by_data("BYDAY"))return 1;for(;;){var e=new ICAL.Time;this.by_indices.BYDAY++,this.by_indices.BYDAY==Object.keys(this.by_data.BYDAY).length&&(this.by_indices.BYDAY=0,t=1);var r=this.by_data.BYDAY[this.by_indices.BYDAY],n=this.ruleDayOfWeek(r)[1];(n-=this.rule.wkst)<0&&(n+=7),e.year=this.last.year,e.month=this.last.month,e.day=this.last.day;var i=e.startDoyWeek(this.rule.wkst);if(!(n+i<1)||t){var a=ICAL.Time.fromDayOfYear(i+n,this.last.year);return this.last.year=a.year,this.last.month=a.month,this.last.day=a.day,t}}},next_year:function(){if(0==this.next_hour())return 0;if(++this.days_index==this.days.length)for(this.days_index=0;this.increment_year(this.rule.interval),this.expand_year_days(this.last.year),0==this.days.length;);return this._nextByYearDay(),1},_nextByYearDay:function(){var t=this.days[this.days_index],e=this.last.year;t<1&&(t+=1,e+=1);var r=ICAL.Time.fromDayOfYear(t,e);this.last.day=r.day,this.last.month=r.month},ruleDayOfWeek:function(t,e){var r=t.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);return r?[parseInt(r[1]||0,10),t=ICAL.Recur.icalDayToNumericDay(r[2],e)]:[0,0]},next_generic:function(t,e,r,n,i){var a=t in this.by_data,s=this.rule.freq==e,o=0;if(i&&0==this[i]())return o;if(a){this.by_indices[t]++;this.by_indices[t];var u=this.by_data[t];this.by_indices[t]==u.length&&(this.by_indices[t]=0,o=1),this.last[r]=u[this.by_indices[t]]}else s&&this["increment_"+r](this.rule.interval);return a&&o&&s&&this["increment_"+n](1),o},increment_monthday:function(t){for(var e=0;er&&(this.last.day-=r,this.increment_month())}},increment_month:function(){if(this.last.day=1,this.has_by_data("BYMONTH"))this.by_indices.BYMONTH++,this.by_indices.BYMONTH==this.by_data.BYMONTH.length&&(this.by_indices.BYMONTH=0,this.increment_year(1)),this.last.month=this.by_data.BYMONTH[this.by_indices.BYMONTH];else{"MONTHLY"==this.rule.freq?this.last.month+=this.rule.interval:this.last.month++,this.last.month--;var t=ICAL.helpers.trunc(this.last.month/12);this.last.month%=12,this.last.month++,0!=t&&this.increment_year(t)}},increment_year:function(t){this.last.year+=t},increment_generic:function(t,e,r,n){this.last[e]+=t;var i=ICAL.helpers.trunc(this.last[e]/r);this.last[e]%=r,0!=i&&this["increment_"+n](i)},has_by_data:function(t){return t in this.rule.parts},expand_year_days:function(t){var e=new ICAL.Time;this.days=[];var r={},n=["BYDAY","BYWEEKNO","BYMONTHDAY","BYMONTH","BYYEARDAY"];for(var i in n)if(n.hasOwnProperty(i)){var a=n[i];a in this.rule.parts&&(r[a]=this.rule.parts[a])}if("BYMONTH"in r&&"BYWEEKNO"in r){var s=1,o={};e.year=t,e.isDate=!0;for(var u=0;ue[0]?1:e[0]>t[0]?-1:0}return t.prototype={THISANDFUTURE:"THISANDFUTURE",exceptions:null,strictExceptions:!1,relateException:function(t){if(this.isRecurrenceException())throw new Error("cannot relate exception to exceptions");if(t instanceof ICAL.Component&&(t=new ICAL.Event(t)),this.strictExceptions&&t.uid!==this.uid)throw new Error("attempted to relate unrelated exception");var e=t.recurrenceId.toString();if((this.exceptions[e]=t).modifiesFuture()){var r=[t.recurrenceId.toUnixTime(),e],n=ICAL.helpers.binsearchInsert(this.rangeExceptions,r,i);this.rangeExceptions.splice(n,0,r)}},modifiesFuture:function(){return!!this.component.hasProperty("recurrence-id")&&this.component.getFirstProperty("recurrence-id").getParameter("range")===this.THISANDFUTURE},findRangeException:function(t){if(!this.rangeExceptions.length)return null;var e=t.toUnixTime(),r=ICAL.helpers.binsearchInsert(this.rangeExceptions,[e],i);if(--r<0)return null;var n=this.rangeExceptions[r];return e Date: Thu, 15 Apr 2021 18:09:11 +0200 Subject: [PATCH 24/24] Convert email notifications --- www/calendar/export.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/calendar/export.js b/www/calendar/export.js index e470c5d33..6019290c6 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -162,9 +162,8 @@ define([ ev.getAllSubcomponents('valarm').forEach(function (al) { var action = al.getFirstPropertyValue('action'); if (action !== 'DISPLAY') { - // XXX email: maybe keep a notification in CryptPad? + // Email notification: keep it in "hidden" and create a cryptpad notification hidden.push(al.toString()); - return; } var trigger = al.getFirstPropertyValue('trigger'); var minutes = -trigger.toSeconds() / 60;