From c882a4294dd4039329e285826701a26e0e9337c7 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sat, 14 Jun 2025 16:56:32 +0100 Subject: [PATCH] group notifications regardless of when they happened - fix #633 --- .../api/endpoints/i/notifications-grouped.ts | 99 +++++++++++++------ 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index b9c41b057d..4b0b9d0884 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -104,53 +104,88 @@ export default class extends Endpoint { // eslint- } // grouping - let groupedNotifications = [notifications[0]] as MiGroupedNotification[]; - for (let i = 1; i < notifications.length; i++) { - const notification = notifications[i]; - const prev = notifications[i - 1]; - let prevGroupedNotification = groupedNotifications.at(-1)!; + const groupedNotifications : MiGroupedNotification[] = []; + // keep track of where reaction / renote notifications are, by note id + const reactionIdxByNoteId = new Map(); + const renoteIdxByNoteId = new Map(); - if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) { - if (prevGroupedNotification.type !== 'reaction:grouped') { - groupedNotifications[groupedNotifications.length - 1] = { + // group notifications by type+note; notice that we don't try to + // split groups if they span a long stretch of time, because + // it's probably overkill: if the user has very few + // notifications, there should be very little difference; if the + // user has many notifications, the pagination will break the + // groups + + // scan `notifications` newest-to-oldest + for (let i = 0; i < notifications.length; i++) { + const notification = notifications[i]; + + if (notification.type === 'reaction') { + const reactionIdx = reactionIdxByNoteId.get(notification.noteId); + if (reactionIdx === undefined) { + // first reaction to this note that we see, add it as-is + // and remember where we put it + groupedNotifications.push(notification); + reactionIdxByNoteId.set(notification.noteId, groupedNotifications.length - 1); + continue; + } + + let prevReaction = groupedNotifications[reactionIdx]; + // if the previous reaction is not a group, make it into one + if (prevReaction.type !== 'reaction:grouped') { + prevReaction = groupedNotifications[reactionIdx] = { type: 'reaction:grouped', - id: '', - createdAt: prev.createdAt, - noteId: prev.noteId!, + id: prevReaction.id, // this will be the newest id in this group + createdAt: prevReaction.createdAt, + noteId: prevReaction.noteId!, reactions: [{ - userId: prev.notifierId!, - reaction: prev.reaction!, + userId: prevReaction.notifierId!, + reaction: prevReaction.reaction!, }], }; - prevGroupedNotification = groupedNotifications.at(-1)!; } - (prevGroupedNotification as FilterUnionByProperty).reactions.push({ + // add this new reaction to the existing group + (prevReaction as FilterUnionByProperty).reactions.push({ userId: notification.notifierId!, reaction: notification.reaction!, }); - prevGroupedNotification.id = notification.id; - continue; - } - if (prev.type === 'renote' && notification.type === 'renote' && prev.targetNoteId === notification.targetNoteId) { - if (prevGroupedNotification.type !== 'renote:grouped') { - groupedNotifications[groupedNotifications.length - 1] = { - type: 'renote:grouped', - id: '', - createdAt: notification.createdAt, - noteId: prev.noteId!, - userIds: [prev.notifierId!], - }; - prevGroupedNotification = groupedNotifications.at(-1)!; - } - (prevGroupedNotification as FilterUnionByProperty).userIds.push(notification.notifierId!); - prevGroupedNotification.id = notification.id; continue; } + if (notification.type === 'renote') { + const renoteIdx = renoteIdxByNoteId.get(notification.targetNoteId); + if (renoteIdx === undefined) { + // first renote of this note that we see, add it as-is and + // remember where we put it + groupedNotifications.push(notification); + renoteIdxByNoteId.set(notification.targetNoteId, groupedNotifications.length - 1); + continue; + } + + let prevRenote = groupedNotifications[renoteIdx]; + // if the previous renote is not a group, make it into one + if (prevRenote.type !== 'renote:grouped') { + prevRenote = groupedNotifications[renoteIdx] = { + type: 'renote:grouped', + id: prevRenote.id, // this will be the newest id in this group + createdAt: prevRenote.createdAt, + noteId: prevRenote.noteId!, + userIds: [prevRenote.notifierId!], + }; + } + // add this new renote to the existing group + (prevRenote as FilterUnionByProperty).userIds.push(notification.notifierId!); + continue; + } + + // not a groupable notification, just push it groupedNotifications.push(notification); } - groupedNotifications = groupedNotifications.slice(0, ps.limit); + // sort the groups by their id, newest first + groupedNotifications.sort( + (a, b) => a.id < b.id ? 1 : a.id > b.id ? -1 : 0, + ); return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id); });