+
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;
+ }
+}