diff --git a/src/NotificationsApp.vue b/src/NotificationsApp.vue index d494e609a..ef67c0396 100644 --- a/src/NotificationsApp.vue +++ b/src/NotificationsApp.vue @@ -21,22 +21,50 @@
+ +
+ +
+ - +
    - - + +
-
+
0) { + return t('notifications', 'No notifications in this category') + } return t('notifications', 'No notifications') }, @@ -217,6 +253,44 @@ export default { return '' }, + + notificationGroups() { + const groups = new Map() + for (const n of this.notifications) { + if (this.activeFilter === 'mentions' && n.objectType !== 'mention') continue + if (this.activeFilter === 'files' && !['files', 'files_sharing'].includes(n.app)) continue + if (this.activeFilter === 'other' && (n.objectType === 'mention' || ['files', 'files_sharing'].includes(n.app))) continue + if (!groups.has(n.app)) groups.set(n.app, []) + groups.get(n.app).push(n) + } + return [...groups.entries()].map(([app, items]) => ({ app, items })) + }, + + filterTabs() { + const counts = { mentions: 0, files: 0, other: 0 } + for (const n of this.notifications) { + if (n.objectType === 'mention') counts.mentions++ + else if (['files', 'files_sharing'].includes(n.app)) counts.files++ + else counts.other++ + } + const tabs = [{ id: 'all', label: t('notifications', 'All') }] + if (counts.mentions) tabs.push({ id: 'mentions', label: t('notifications', 'Mentions'), count: counts.mentions }) + if (counts.files) tabs.push({ id: 'files', label: t('notifications', 'Files'), count: counts.files }) + if (counts.other) tabs.push({ id: 'other', label: t('notifications', 'Other'), count: counts.other }) + return tabs + }, + + showFilterTabs() { + return this.filterTabs.length > 2 + }, + }, + + watch: { + notificationGroups(groups) { + if (groups.length === 0 && this.activeFilter !== 'all') { + this.activeFilter = 'all' + } + }, }, mounted() { @@ -342,11 +416,34 @@ 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) }, + formatAppName(app) { + return app.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) + }, + + toggleGroup(app) { + if (this.collapsedGroups.has(app)) { + this.collapsedGroups.delete(app) + } else { + this.collapsedGroups.add(app) + } + }, + + visibleIndex(groupIdx, itemIdx) { + let idx = 0 + for (let g = 0; g < groupIdx; g++) { + idx += this.notificationGroups[g].items.length + } + return idx + itemIdx + }, + /** * Update the title to show * if there are new notifications * diff --git a/src/styles/styles.scss b/src/styles/styles.scss index 96524b4a9..dc15f1979 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -123,3 +123,127 @@ svg { } } } + +// Filter tabs (only rendered when notifications span 3+ categories) +.notification-filter-tabs { + display: flex; + gap: 2px; + padding: 6px 8px; + border-bottom: 1px solid var(--color-border); + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { display: none; } +} + +.notification-filter-tab { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + border: none; + border-radius: 20px; + background: transparent; + color: var(--color-text-maxcontrast); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; + white-space: nowrap; + + &:hover { + background: var(--color-background-hover); + color: var(--color-main-text); + } + + &--active { + background: var(--color-primary-element); + color: var(--color-primary-element-text); + + &:hover { background: var(--color-primary-element); } + + .notification-filter-count { + background: rgba(255, 255, 255, 0.25); + color: inherit; + } + } +} + +.notification-filter-count { + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 8px; + background: var(--color-background-dark); + color: var(--color-text-maxcontrast); + font-size: 10px; + line-height: 16px; + text-align: center; + box-sizing: border-box; +} + +// Group header — collapsible per-app section title +.notification-group-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-maxcontrast); + background: var(--color-background-hover); + cursor: pointer; + user-select: none; + list-style: none; + + &:hover { + background: var(--color-background-dark); + } + + .notification-group-name { + flex: 1; + } + + .notification-group-badge { + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 9px; + background: var(--color-primary-element); + color: var(--color-primary-element-text); + font-size: 10px; + line-height: 18px; + text-align: center; + text-transform: none; + letter-spacing: 0; + box-sizing: border-box; + } + + .material-design-icon { + transition: transform 0.2s ease; + + &.notification-group-chevron--collapsed { + transform: rotate(-90deg); + } + } +} + +// Subtle staggered entrance animation per notification item +#notifications.header-menu--opened .notification { + animation: nc-notif-item-in 0.3s cubic-bezier(0.2, 0, 0, 1) both; + animation-delay: calc(100ms + var(--anim-index, 0) * 40ms); +} + +@keyframes nc-notif-item-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: none; + } +}