From 6e76a800673ccd1dbbf9481fdcb7a476090d367b Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 09:24:23 +0000 Subject: [PATCH 1/9] Modify /poll so that it blocks until an event is received Send an event when a notification is added --- lib/account.js | 4 ++++ public/app.js | 4 ++-- routes/admin.js | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/account.js b/lib/account.js index 9f705f0..c898ea1 100644 --- a/lib/account.js +++ b/lib/account.js @@ -31,6 +31,9 @@ import { getActivity, createActivity } from './notes.js'; +import { + UserEvent +} from '../lib/UserEvent.js'; import debug from 'debug'; import MarkdownIt from 'markdown-it'; @@ -228,6 +231,7 @@ export const addNotification = (notification) => { notification: notification, }); writeNotifications(notifications); + UserEvent.sendEvent('notification'); } const writeNotifications = (notifications) => { diff --git a/public/app.js b/public/app.js index fdce550..74a1606 100644 --- a/public/app.js +++ b/public/app.js @@ -89,7 +89,7 @@ const app = { fetch('/private/poll','get').then((json) => { const res = JSON.parse(json); app.alertNewPosts(res); - setTimeout(() => app.pollForPosts(), 30000); // poll every 5 seconds + setTimeout(() => app.pollForPosts(), 1000); // poll every 1 seconds, endpoint will stall until event occurs }).catch((err) => { console.error(err); }); @@ -242,4 +242,4 @@ const app = { } return false; } -} \ No newline at end of file +} diff --git a/routes/admin.js b/routes/admin.js index bc56357..28a768b 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -31,6 +31,9 @@ import { import { ActivityPub } from '../lib/ActivityPub.js'; +import { + UserEvent +} from '../lib/UserEvent.js'; const logger = debug('ono:admin'); router.get('/index', async (req, res) => { @@ -38,12 +41,12 @@ router.get('/index', async (req, res) => { }); router.get('/poll', async (req, res) => { + await UserEvent.waitForEvent(); const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification); const notifications = getNotifications().filter((n) => n.time > sinceNotifications); - const { activitystream } = await getActivitySince(sincePosts, true); @@ -516,4 +519,4 @@ router.post('/boost', async (req, res) => { }); } writeBoosts(boosts); -}); \ No newline at end of file +}); From 9c3bf60223f4830acf8f757f16902597fde77e29 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 10:18:51 +0000 Subject: [PATCH 2/9] Add missing UserEvent.js file --- lib/UserEvent.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/UserEvent.js diff --git a/lib/UserEvent.js b/lib/UserEvent.js new file mode 100644 index 0000000..9c4de6b --- /dev/null +++ b/lib/UserEvent.js @@ -0,0 +1,28 @@ +import { once, EventEmitter } from 'events'; +import debug from 'debug'; + +const logger = debug('ono:event'); + +/** + * UserEvent - a class for handling real-time event updates + */ +export class UserEventClient { + + constructor() { + this.emitter = new EventEmitter(); + } + + sendEvent(e, data) { + logger('SENDING EVENT ' + data); + this.emitter.emit('poll', data); + } + + async waitForEvent() { + const [data] = await once(this.emitter, 'poll'); + logger('RECEIVED EVENT ' + data); + } + +} + +export const UserEvent = new UserEventClient(); + From 3e437557ecf0636c64b4d0fc9f9d8582f0c823a1 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 21:12:55 +0000 Subject: [PATCH 3/9] On first poll, immediately return with results --- public/app.js | 4 +++- routes/admin.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app.js b/public/app.js index f33098b..b0cbbf8 100644 --- a/public/app.js +++ b/public/app.js @@ -38,6 +38,7 @@ const getCookie = (name) => { } const app = { + firstPoll: true, newPosts: 0, newNotifications: 0, latestPost: (date) => { @@ -102,7 +103,8 @@ const app = { }, pollForPosts: () => { - fetch('/private/poll','get').then((json) => { + fetch('/private/poll' + (app.firstPoll ? '?nowait=1' : ''),'get').then((json) => { + app.firstPoll = false; const res = JSON.parse(json); app.alertNewPosts(res); setTimeout(() => app.pollForPosts(), 1000); // poll every 1 seconds, endpoint will stall until event occurs diff --git a/routes/admin.js b/routes/admin.js index be50e1c..330197b 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -41,7 +41,9 @@ router.get('/index', async (req, res) => { }); router.get('/poll', async (req, res) => { - await UserEvent.waitForEvent(); + if (!req.query.nowait) { + await UserEvent.waitForEvent(); + } const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification); From e89101221243c27afcbf6bf20696adfe700b5906 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Thu, 22 Dec 2022 21:28:12 +0000 Subject: [PATCH 4/9] When /poll request is closed, abort polling. https://nodejs.org/api/events.html#eventsonemitter-eventname-options --- lib/UserEvent.js | 8 +++++++- routes/admin.js | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/UserEvent.js b/lib/UserEvent.js index 9c4de6b..cf5a07d 100644 --- a/lib/UserEvent.js +++ b/lib/UserEvent.js @@ -10,6 +10,7 @@ export class UserEventClient { constructor() { this.emitter = new EventEmitter(); + this.ac = new AbortController(); } sendEvent(e, data) { @@ -17,8 +18,13 @@ export class UserEventClient { this.emitter.emit('poll', data); } + abort() { + this.ac.abort(); + this.ac = new AbortController(); + } + async waitForEvent() { - const [data] = await once(this.emitter, 'poll'); + const [data] = await once(this.emitter, 'poll', { signal: this.ac.signal }); logger('RECEIVED EVENT ' + data); } diff --git a/routes/admin.js b/routes/admin.js index 330197b..ed3881f 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -42,9 +42,16 @@ router.get('/index', async (req, res) => { router.get('/poll', async (req, res) => { if (!req.query.nowait) { - await UserEvent.waitForEvent(); + req.on('close', function (err){ + UserEvent.abort(); + return; + }); + try { + await UserEvent.waitForEvent(); + } catch(e) { + // we got aborted + } } - const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification); const notifications = getNotifications().filter((n) => n.time > sinceNotifications); From 4df90f291b7d9bfb5c7cec7e33aff44583cc6737 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 18:43:35 +0000 Subject: [PATCH 5/9] Raise notification for DM. Ignore self-DMs, remove tools (boost, like, edit) on notifications --- design/notifications.handlebars | 4 ++++ design/partials/note.handlebars | 2 ++ lib/account.js | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/design/notifications.handlebars b/design/notifications.handlebars index 477bc51..567a1b3 100644 --- a/design/notifications.handlebars +++ b/design/notifications.handlebars @@ -20,6 +20,10 @@
💬 {{../actor.name}} mentioned you {{timesince ../time}}
{{> note actor=../actor note=../note}} {{/isEq}} + {{#isEq notification.type "DM"}} +
💬 {{../actor.name}} direct messaged you {{timesince ../time}}
+ {{> note actor=../actor note=../note}} + {{/isEq}} {{#isEq notification.type "Like"}}
⭐️ {{../actor.name}} liked your post {{timesince ../time}}
diff --git a/design/partials/note.handlebars b/design/partials/note.handlebars index 27139b2..7b17205 100644 --- a/design/partials/note.handlebars +++ b/design/partials/note.handlebars @@ -25,6 +25,7 @@
{{/each}} diff --git a/lib/account.js b/lib/account.js index e3551ad..b9d667b 100644 --- a/lib/account.js +++ b/lib/account.js @@ -119,6 +119,13 @@ export const acceptDM = (dm, inboxUser) => { writeInboxIndex(inboxIndex); + if (dm.attributedTo !== ActivityPub.actor.id) { + addNotification({ + type: 'DM', + actor: dm.attributedTo, + object: dm.inReplyTo + }); + } } export const isMyPost = (activity) => { From 71ba5e921f5939c730bb59829bd64648292a1c36 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 19:08:35 +0000 Subject: [PATCH 6/9] Add direct link from DM notification to message viewer --- design/notifications.handlebars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/notifications.handlebars b/design/notifications.handlebars index 567a1b3..5dd7b41 100644 --- a/design/notifications.handlebars +++ b/design/notifications.handlebars @@ -21,7 +21,7 @@ {{> note actor=../actor note=../note}} {{/isEq}} {{#isEq notification.type "DM"}} -
💬 {{../actor.name}} direct messaged you {{timesince ../time}}
+ {{> note actor=../actor note=../note}} {{/isEq}} {{#isEq notification.type "Like"}} From 7d160680284bed45ce0f1036692f2f151d356e1d Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 19:22:06 +0000 Subject: [PATCH 7/9] Use addNotification for incoming unread main timeline messages to update unread counter --- routes/admin.js | 5 +++-- routes/inbox.js | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/routes/admin.js b/routes/admin.js index ed3881f..1adbbb5 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -53,8 +53,9 @@ router.get('/poll', async (req, res) => { } } const sincePosts = new Date(req.cookies.latestPost).getTime(); - const sinceNotifications = parseInt(req.cookies.latestNotification); - const notifications = getNotifications().filter((n) => n.time > sinceNotifications); + const sinceNotifications = parseInt(req.cookies.latestNotification);//.filter((n) => {n.}); + // notification mechanism used to indicate there are unread posts, but they shouldn't appear in notifications tab + const notifications = getNotifications().filter((n) => n.notification.type !== 'NewPost').filter((n) => n.time > sinceNotifications); const inboxIndex = getInboxIndex(); const unreadDM = Object.keys(inboxIndex).filter((k) => { return !inboxIndex[k].lastRead || inboxIndex[k].lastRead < inboxIndex[k].latest; diff --git a/routes/inbox.js b/routes/inbox.js index 8cea8a4..d4df026 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -154,6 +154,11 @@ router.post('/', async (req, res) => { } else if (!incomingRequest.object.inReplyTo) { // this is a NEW post - most likely from a follower await createActivity(incomingRequest.object); + addNotification({ + type: 'NewPost', + actor: incomingRequest.object.attributedTo, + object: incomingRequest.object.id + }); } else { // this is a reply // from a following From be94b6dca18e47b4c0e811fa12ae3773bd0ccbef Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Mon, 26 Dec 2022 19:27:40 +0000 Subject: [PATCH 8/9] If GET /poll fails (e.g. timeout from nginx), repoll. Previously it would give up and the user would get no more notifications --- public/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app.js b/public/app.js index b0cbbf8..54c7ec1 100644 --- a/public/app.js +++ b/public/app.js @@ -110,6 +110,7 @@ const app = { setTimeout(() => app.pollForPosts(), 1000); // poll every 1 seconds, endpoint will stall until event occurs }).catch((err) => { console.error(err); + setTimeout(() => app.pollForPosts(), 1000); // poll every 1 seconds, endpoint will stall until event occurs }); }, toggleBoost: (el, postId) => { From 0d7eb7070f4eb14da65dbcbabbd755109e422722 Mon Sep 17 00:00:00 2001 From: Toby Jaffey Date: Tue, 27 Dec 2022 16:57:15 +0000 Subject: [PATCH 9/9] No not raise DMs or unread messages as notifications, instead trigger UserEvent directly - causing the long poll to end and the messages counts to be read. --- design/notifications.handlebars | 6 +----- lib/account.js | 6 +----- routes/admin.js | 2 +- routes/inbox.js | 13 +++++-------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/design/notifications.handlebars b/design/notifications.handlebars index 5dd7b41..72401c7 100644 --- a/design/notifications.handlebars +++ b/design/notifications.handlebars @@ -20,10 +20,6 @@
💬 {{../actor.name}} mentioned you {{timesince ../time}}
{{> note actor=../actor note=../note}} {{/isEq}} - {{#isEq notification.type "DM"}} - - {{> note actor=../actor note=../note}} - {{/isEq}} {{#isEq notification.type "Like"}}
⭐️ {{../actor.name}} liked your post {{timesince ../time}}
@@ -46,4 +42,4 @@ {{/if}}{{/each}} app.pollForPosts(); - \ No newline at end of file + diff --git a/lib/account.js b/lib/account.js index b9d667b..3c49358 100644 --- a/lib/account.js +++ b/lib/account.js @@ -120,11 +120,7 @@ export const acceptDM = (dm, inboxUser) => { writeInboxIndex(inboxIndex); if (dm.attributedTo !== ActivityPub.actor.id) { - addNotification({ - type: 'DM', - actor: dm.attributedTo, - object: dm.inReplyTo - }); + UserEvent.sendEvent('dm'); } } diff --git a/routes/admin.js b/routes/admin.js index 1adbbb5..5e88058 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -55,7 +55,7 @@ router.get('/poll', async (req, res) => { const sincePosts = new Date(req.cookies.latestPost).getTime(); const sinceNotifications = parseInt(req.cookies.latestNotification);//.filter((n) => {n.}); // notification mechanism used to indicate there are unread posts, but they shouldn't appear in notifications tab - const notifications = getNotifications().filter((n) => n.notification.type !== 'NewPost').filter((n) => n.time > sinceNotifications); + const notifications = getNotifications().filter((n) => n.time > sinceNotifications); const inboxIndex = getInboxIndex(); const unreadDM = Object.keys(inboxIndex).filter((k) => { return !inboxIndex[k].lastRead || inboxIndex[k].lastRead < inboxIndex[k].latest; diff --git a/routes/inbox.js b/routes/inbox.js index d4df026..e8aa82d 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -29,10 +29,11 @@ import debug from 'debug'; import { isIndexed } from '../lib/storage.js'; +import { + UserEvent +} from '../lib/UserEvent.js'; const logger = debug('ono:inbox'); - - router.post('/', async (req, res) => { const incomingRequest = req.body; @@ -154,11 +155,7 @@ router.post('/', async (req, res) => { } else if (!incomingRequest.object.inReplyTo) { // this is a NEW post - most likely from a follower await createActivity(incomingRequest.object); - addNotification({ - type: 'NewPost', - actor: incomingRequest.object.attributedTo, - object: incomingRequest.object.id - }); + UserEvent.sendEvent('msg'); } else { // this is a reply // from a following @@ -185,4 +182,4 @@ router.post('/', async (req, res) => { logger('Unknown request format:', incomingRequest); } return res.status(200).send(); -}); \ No newline at end of file +});