From 5ec4e39413399cdc9962b47c63a8e1f6834fa372 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 9 Apr 2021 18:10:13 +0200 Subject: [PATCH] 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);