From e2883ca7c5c6e3e475398bbd4e4bc13572b8a089 Mon Sep 17 00:00:00 2001 From: Frank Karlitschek Date: Tue, 28 Apr 2026 14:59:57 +0200 Subject: [PATCH] feat(ui): snooze notifications for 1 hour, 4 hours, or until tomorrow Each notification now exposes a clock-icon NcActions menu with 1 hour / 4 hours / Tomorrow snooze options. Snoozed items are hidden from the panel until their wake time, with the snooze map persisted in localStorage so reloads (and other tabs after a fetch) honor it. * NotificationItem.vue: snooze NcActions menu in the heading, emits 'snooze' with the chosen minutes * NotificationsApp.vue: snoozed map, visibleNotifications computed filters them out, expired entries are pruned at the start of every _fetch * onRemove(index) becomes onRemove(notification) so the v-for over visibleNotifications doesn't have to thread an index back into the full notifications array * styles.scss: spacing for the snooze action button Note: this is the client-side version. Snooze state is per-browser; moving it to the server requires API support, which is tracked as follow-up. Signed-off-by: Frank Karlitschek --- src/Components/NotificationItem.vue | 37 +++++++++++++++++++++++-- src/NotificationsApp.vue | 43 +++++++++++++++++++++++++---- src/styles/styles.scss | 5 ++++ 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/Components/NotificationItem.vue b/src/Components/NotificationItem.vue index 4cfc85060..fbce483d5 100644 --- a/src/Components/NotificationItem.vue +++ b/src/Components/NotificationItem.vue @@ -16,6 +16,27 @@ ignoreSeconds :format="{ timeStyle: 'short', dateStyle: 'long' }" :timestamp="timestamp" /> + + + + + {{ t('notifications', '1 hour') }} + + + + {{ t('notifications', '4 hours') }} + + + + {{ t('notifications', 'Tomorrow') }} + + @@ -23,7 +23,7 @@
@@ -32,10 +32,11 @@ :key="-2016" :notification="fairUsePolicyNotification" /> + @remove="onRemove(notification)" + @snooze="onSnooze" /> @@ -187,6 +188,9 @@ export default { pushEndpoints: null, open: false, + + /** Map of notificationId → timestamp when the snooze expires */ + snoozed: JSON.parse(localStorage.getItem('notifications:snoozed') ?? '{}'), } }, @@ -217,6 +221,11 @@ export default { return '' }, + + visibleNotifications() { + const now = Date.now() + return this.notifications.filter(n => (this.snoozed[n.notificationId] ?? 0) <= now) + }, }, mounted() { @@ -342,11 +351,31 @@ export default { }) }, - onRemove(index) { - this.notifications.splice(index, 1) + onRemove(notification) { + const idx = this.notifications.findIndex(n => n.notificationId === notification.notificationId) + if (idx !== -1) { + this.notifications.splice(idx, 1) + } setCurrentTabAsActive(this.tabId) }, + onSnooze({ notification, minutes }) { + const wakeAt = Date.now() + minutes * 60 * 1000 + this.snoozed = { ...this.snoozed, [notification.notificationId]: wakeAt } + localStorage.setItem('notifications:snoozed', JSON.stringify(this.snoozed)) + }, + + _clearExpiredSnooze() { + const now = Date.now() + const active = Object.fromEntries( + Object.entries(this.snoozed).filter(([, wakeAt]) => wakeAt > now), + ) + if (Object.keys(active).length !== Object.keys(this.snoozed).length) { + this.snoozed = active + localStorage.setItem('notifications:snoozed', JSON.stringify(active)) + } + }, + /** * Update the title to show * if there are new notifications * @@ -408,6 +437,8 @@ export default { * Performs the AJAX request to retrieve the notifications */ async _fetch(force = false) { + this._clearExpiredSnooze() + if (this.notifications.length && this.notifications[0].notificationId > this.webNotificationsThresholdId) { this.webNotificationsThresholdId = this.notifications[0].notificationId } diff --git a/src/styles/styles.scss b/src/styles/styles.scss index 96524b4a9..40d889a1b 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -123,3 +123,8 @@ svg { } } } + +// Snooze action menu — keep the clock button subtle and compact +.notification-snooze-button { + margin-inline-end: 2px; +}