Fix mentions and notifications drawer.

Thanks to xet7 !

Fixes #6062,
fixes #6003,
fixes #5996,
fixes #5720,
fixes #5911,
fixes #5792,
fixes #5163,
fixes #4431,
fixes #4126,
fixes #3363,
fixes #3150
This commit is contained in:
Lauri Ojansivu 2026-01-14 21:02:10 +02:00
parent 0d5dd3082c
commit 20b5e2ab8f
14 changed files with 225 additions and 72 deletions

View file

@ -199,4 +199,5 @@ template(name="activity")
else if(currentData.timeValue)
| {{_ activity.activityType currentData.timeValue}}
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
if($neq mode 'none')
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}

View file

@ -45,15 +45,16 @@ BlazeComponent.extendComponent({
match: /\B@([\w.-]*)$/,
search(term, callback) {
const currentBoard = Utils.getCurrentBoard();
const searchTerm = term.toLowerCase();
callback(
_.union(
currentBoard
.activeMembers()
.map(member => {
const user = ReactiveCache.getUser(member.userId);
const username = user.username;
const fullName = user.profile && user.profile !== undefined && user.profile.fullname ? user.profile.fullname : "";
return username.includes(term) || fullName.includes(term) ? user : null;
const username = user.username.toLowerCase();
const fullName = user.profile && user.profile !== undefined && user.profile.fullname ? user.profile.fullname.toLowerCase() : "";
return username.includes(searchTerm) || fullName.includes(searchTerm) ? user : null;
})
.filter(Boolean), [...specialHandles])
);

View file

@ -12,8 +12,8 @@
display: none;
}
#notifications-drawer .notification .read-status {
width: 4vw;
padding: 0 1.3vw 0 0;
width: 2vw;
padding: 0 0.5vw 0 0;
}
#notifications-drawer .notification .read-status input {
width: 3vw;
@ -27,6 +27,9 @@
display: block;
color: #bbb;
}
#notifications-drawer .notification .read-status .activity-type.hidden {
display: none;
}
#notifications-drawer .notification .details .activity a.member {
margin: 0px 0px 0px 0px;
padding: 0px;
@ -53,6 +56,13 @@
color: #999;
font-style: italic;
}
#notifications-drawer .notification .details .notification-date {
margin-top: 4px;
}
#notifications-drawer .notification .details .notification-date small {
font-size: 0.85em;
color: #999;
}
#notifications-drawer .notification .remove a:hover {
color: #eb4646 !important;
}

View file

@ -5,6 +5,8 @@ template(name='notification')
+notificationIcon(activityData)
.details
+activity(activity=activityData mode='none')
.notification-date
small.quiet {{activityDate}}
if read
.remove
a(title="{{_ 'delete'}}") 🗑️

View file

@ -3,10 +3,14 @@ import { ReactiveCache } from '/imports/reactiveCache';
Template.notification.events({
'click .read-status .materialCheckBox'() {
const update = {};
update[`profile.notifications.${this.index}.read`] = this.read
? null
: Date.now();
Users.update(Meteor.userId(), { $set: update });
const newReadValue = this.read ? null : Date.now();
update[`profile.notifications.${this.index}.read`] = newReadValue;
Users.update(Meteor.userId(), { $set: update }, (error, result) => {
if (error) {
console.error('Error updating notification:', error);
}
});
},
'click .remove a'() {
ReactiveCache.getCurrentUser().removeNotification(this.activityData._id);
@ -27,4 +31,16 @@ Template.notification.helpers({
const activity = ReactiveCache.getActivity(activityId);
return activity && activity.userId;
},
activityDate() {
const activity = this.activityData;
if (!activity || !activity.createdAt) return '';
const user = ReactiveCache.getCurrentUser();
if (!user) return '';
const dateFormat = user.getDateFormat ? user.getDateFormat() : 'L';
const timeFormat = user.getTimeFormat ? user.getTimeFormat() : 'LT';
return moment(activity.createdAt).format(`${dateFormat} ${timeFormat}`);
},
});

View file

@ -21,7 +21,7 @@ template(name='notificationIcon')
else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
span.activity-type(title="checklist item") ☑️
else if($in activityType 'addComment')
span.activity-type(title="comment") 💬
span.activity-type.hidden(title="comment")
else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
span.activity-type(title="custom field") 🧩
else if($in activityType 'addedLabel' 'removedLabel')

View file

@ -23,12 +23,66 @@ section#notifications-drawer .header {
border-bottom: 1px solid #dbdbdb;
z-index: 2;
}
section#notifications-drawer .header .toggle-read {
section#notifications-drawer .header .notification-menu-toggle {
position: absolute;
left: 16px;
top: calc(50% - 8px);
top: calc(50% - 12px);
font-size: 20px;
cursor: pointer;
color: #333;
line-height: 24px;
}
section#notifications-drawer .header .notification-menu-toggle:hover {
color: #2980b9;
}
section#notifications-drawer .header .notification-menu {
position: absolute;
left: 16px;
top: 44px;
background: white;
border: 1px solid #dbdbdb;
border-radius: 3px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
min-width: 220px;
z-index: 100;
display: none;
}
section#notifications-drawer .header .notification-menu.is-open {
display: block;
}
section#notifications-drawer .header .notification-menu .menu-section {
padding: 4px 0;
}
section#notifications-drawer .header .notification-menu .menu-divider {
border-top: 1px solid #dbdbdb;
margin: 4px 0;
}
section#notifications-drawer .header .notification-menu .menu-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
color: #333;
white-space: nowrap;
}
section#notifications-drawer .header .notification-menu .menu-item:hover {
background-color: #f5f5f5;
}
section#notifications-drawer .header .notification-menu .menu-item.selected {
background-color: #e8f4f8;
}
section#notifications-drawer .header .notification-menu .menu-item .check-icon {
width: 20px;
min-width: 20px;
margin-right: 8px;
text-align: center;
color: #2980b9;
font-weight: bold;
}
section#notifications-drawer .header .notification-menu .menu-item .menu-icon {
margin-right: 8px;
font-size: 16px;
}
section#notifications-drawer .header h5 {
text-align: center;
margin: 0;
@ -42,19 +96,7 @@ section#notifications-drawer .header .close {
line-height: 24px;
opacity: 1;
}
section#notifications-drawer .all-read,
section#notifications-drawer .remove-read {
color: #2980b9;
background-color: #fafafa;
margin: 8px 16px 12px;
display: inline-block;
}
section#notifications-drawer .remove-read {
float: right;
}
section#notifications-drawer .remove-read:hover {
color: #eb4646 !important;
}
section#notifications-drawer ul.notifications {
display: block;
padding: 0px 16px 0px 16px;

View file

@ -1,20 +1,42 @@
template(name='notificationsDrawer')
section#notifications-drawer(class="{{#if $.Session.get 'showReadNotifications'}}show-read{{/if}}")
.header
if $.Session.get 'showReadNotifications'
a.toggle-read {{_ 'filter-by-unread'}}
else
a.toggle-read {{_ 'view-all'}}
a.notification-menu-toggle ☰
.notification-menu(class="{{#if $.Session.get 'showNotificationMenu'}}is-open{{/if}}")
.menu-section
a.menu-item(class="{{#unless $.Session.get 'showReadNotifications'}}selected{{/unless}}")
span.check-icon {{#unless $.Session.get 'showReadNotifications'}}✓{{/unless}}
span.menu-icon 📭
span {{_ 'filter-by-unread'}}
a.menu-item(class="{{#if $.Session.get 'showReadNotifications'}}selected{{/if}}")
span.check-icon {{#if $.Session.get 'showReadNotifications'}}✓{{/if}}
span.menu-icon 📋
span {{_ 'view-all'}}
.menu-divider
.menu-section
if($gt unreadNotifications 0)
a.menu-item.mark-all-read
span.check-icon
span.menu-icon ✅
span {{_ 'mark-all-as-read'}}
if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
a.menu-item.mark-all-unread
span.check-icon
span.menu-icon 📬
span {{_ 'mark-all-as-unread'}}
if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
a.menu-item.delete-read
span.check-icon
span.menu-icon 🗑️
span {{_ 'remove-all-read'}}
a.menu-item.delete-all
span.check-icon
span.menu-icon 🗑️
span {{_ 'delete-all-notifications'}}
h5 {{_ 'notifications'}}
if($gt unreadNotifications 0)
|(#{unreadNotifications})
a.close ❌
ul.notifications
each transformedProfile.notifications
each notifications
+notification(activityData=activityObj index=dbIndex read=read)
if($gt unreadNotifications 0)
a.all-read {{_ 'mark-all-as-read'}}
if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
a.remove-read
| 🗑️
| {{_ 'remove-all-read'}}

View file

@ -14,41 +14,83 @@ Template.notificationsDrawer.onCreated(function() {
});
Template.notificationsDrawer.helpers({
notifications() {
const user = ReactiveCache.getCurrentUser();
return user ? user.notifications() : [];
},
transformedProfile() {
return ReactiveCache.getCurrentUser();
},
readNotifications() {
const readNotifications = _.filter(
ReactiveCache.getCurrentUser().profile.notifications,
v => !!v.read,
);
const user = ReactiveCache.getCurrentUser();
const list = user ? user.notifications() : [];
const readNotifications = _.filter(list, v => !!v.read);
return readNotifications.length;
},
});
Template.notificationsDrawer.events({
'click .all-read'() {
const notifications = ReactiveCache.getCurrentUser().profile.notifications;
for (const index in notifications) {
if (notifications.hasOwnProperty(index) && !notifications[index].read) {
const update = {};
update[`profile.notifications.${index}.read`] = Date.now();
Users.update(Meteor.userId(), { $set: update });
'click .notification-menu-toggle'(event) {
event.stopPropagation();
Session.set('showNotificationMenu', !Session.get('showNotificationMenu'));
},
'click .notification-menu .menu-item'(event) {
const target = event.currentTarget;
if (target.classList.contains('mark-all-read')) {
const notifications = ReactiveCache.getCurrentUser().profile.notifications;
for (const index in notifications) {
if (notifications.hasOwnProperty(index) && !notifications[index].read) {
const update = {};
update[`profile.notifications.${index}.read`] = Date.now();
Users.update(Meteor.userId(), { $set: update });
}
}
Session.set('showNotificationMenu', false);
} else if (target.classList.contains('mark-all-unread')) {
const notifications = ReactiveCache.getCurrentUser().profile.notifications;
for (const index in notifications) {
if (notifications.hasOwnProperty(index) && notifications[index].read) {
const update = {};
update[`profile.notifications.${index}.read`] = null;
Users.update(Meteor.userId(), { $set: update });
}
}
Session.set('showNotificationMenu', false);
} else if (target.classList.contains('delete-read')) {
const user = ReactiveCache.getCurrentUser();
for (const notification of user.profile.notifications) {
if (notification.read) {
user.removeNotification(notification.activity);
}
}
Session.set('showNotificationMenu', false);
} else if (target.classList.contains('delete-all')) {
if (confirm(TAPi18n.__('delete-all-notifications-confirm'))) {
const user = ReactiveCache.getCurrentUser();
const notificationsCopy = [...user.profile.notifications];
for (const notification of notificationsCopy) {
user.removeNotification(notification.activity);
}
}
Session.set('showNotificationMenu', false);
} else if (target.classList.contains('selected')) {
// Already selected, do nothing
Session.set('showNotificationMenu', false);
} else {
// Toggle view
Session.set('showReadNotifications', !Session.get('showReadNotifications'));
Session.set('showNotificationMenu', false);
}
},
'click .close'() {
Session.set('showNotificationMenu', false);
toggleNotificationsDrawer();
},
'click .toggle-read'() {
Session.set('showReadNotifications', !Session.get('showReadNotifications'));
},
'click .remove-read'() {
const user = ReactiveCache.getCurrentUser();
for (const notification of user.profile.notifications) {
if (notification.read) {
user.removeNotification(notification.activity);
}
'click'(event) {
// Close menu when clicking outside
if (!event.target.closest('.notification-menu') && !event.target.closest('.notification-menu-toggle')) {
Session.set('showNotificationMenu', false);
}
},
});