mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-05 03:51:26 +01:00
Chat notifs (#11832)
* first part of chat notifs * moved default notif to manage page * spec * notifs * delete settings on room delete
This commit is contained in:
committed by
GitHub
parent
f377650161
commit
61f036ce1d
@@ -40,11 +40,13 @@ chatsAPI.create = async function (caller, data) {
|
||||
if (!data) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const isPublic = data.type === 'public';
|
||||
const isAdmin = await user.isAdministrator(caller.uid);
|
||||
if (isPublic && !isAdmin) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
if (!data.uids || !Array.isArray(data.uids)) {
|
||||
throw new Error(`[[error:wrong-parameter-type, uids, ${typeof data.uids}, Array]]`);
|
||||
}
|
||||
@@ -55,6 +57,11 @@ chatsAPI.create = async function (caller, data) {
|
||||
if (isPublic && (!Array.isArray(data.groups) || !data.groups.length)) {
|
||||
throw new Error('[[error:no-groups-selected]]');
|
||||
}
|
||||
|
||||
data.notificationSetting = isPublic ?
|
||||
messaging.notificationSettings.ATMENTION :
|
||||
messaging.notificationSettings.ALLMESSAGES;
|
||||
|
||||
await Promise.all(data.uids.map(async uid => messaging.canMessageUser(caller.uid, uid)));
|
||||
const roomId = await messaging.newRoom(caller.uid, data);
|
||||
|
||||
@@ -108,18 +115,21 @@ chatsAPI.update = async (caller, data) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
const [roomData, isAdmin] = await Promise.all([
|
||||
messaging.getRoomData(data.roomId),
|
||||
user.isAdministrator(caller.uid),
|
||||
]);
|
||||
if (!roomData) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
if (data.hasOwnProperty('groups')) {
|
||||
const [roomData, isAdmin] = await Promise.all([
|
||||
messaging.getRoomData(data.roomId),
|
||||
user.isAdministrator(caller.uid),
|
||||
]);
|
||||
if (!roomData) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
if (roomData.public && isAdmin) {
|
||||
await db.setObjectField(`chat:room:${data.roomId}`, 'groups', JSON.stringify(data.groups));
|
||||
}
|
||||
}
|
||||
if (data.hasOwnProperty('notificationSetting') && isAdmin) {
|
||||
await db.setObjectField(`chat:room:${data.roomId}`, 'notificationSetting', data.notificationSetting);
|
||||
}
|
||||
return messaging.loadRoom(caller.uid, {
|
||||
roomId: data.roomId,
|
||||
});
|
||||
|
||||
@@ -25,6 +25,11 @@ require('./rooms')(Messaging);
|
||||
require('./unread')(Messaging);
|
||||
require('./notifications')(Messaging);
|
||||
|
||||
Messaging.notificationSettings = Object.create(null);
|
||||
Messaging.notificationSettings.NONE = 1;
|
||||
Messaging.notificationSettings.ATMENTION = 2;
|
||||
Messaging.notificationSettings.ALLMESSAGES = 3;
|
||||
|
||||
Messaging.messageExists = async mid => db.exists(`message:${mid}`);
|
||||
|
||||
Messaging.getMessages = async (params) => {
|
||||
|
||||
@@ -12,6 +12,23 @@ const meta = require('../meta');
|
||||
module.exports = function (Messaging) {
|
||||
// Only used to notify a user of a new chat message
|
||||
Messaging.notifyQueue = {};
|
||||
|
||||
Messaging.setUserNotificationSetting = async (uid, roomId, value) => {
|
||||
if (parseInt(value, 10) === -1) {
|
||||
// go back to default
|
||||
return await db.deleteObjectField(`chat:room:${roomId}:notification:settings`, uid);
|
||||
}
|
||||
await db.setObjectField(`chat:room:${roomId}:notification:settings`, uid, parseInt(value, 10));
|
||||
};
|
||||
|
||||
Messaging.getUidsNotificationSetting = async (uids, roomId) => {
|
||||
const [settings, roomData] = await Promise.all([
|
||||
db.getObjectFields(`chat:room:${roomId}:notification:settings`, uids),
|
||||
Messaging.getRoomData(roomId, ['notificationSetting']),
|
||||
]);
|
||||
return uids.map(uid => parseInt(settings[uid] || roomData.notificationSetting, 10));
|
||||
};
|
||||
|
||||
Messaging.notifyUsersInRoom = async (fromUid, roomId, messageObj) => {
|
||||
const isPublic = parseInt(await db.getObjectField(`chat:room:${roomId}`, 'public'), 10) === 1;
|
||||
|
||||
@@ -34,13 +51,15 @@ module.exports = function (Messaging) {
|
||||
// delivers unread public msg to all online users on the chats page
|
||||
io.in(`chat_room_public_${roomId}`).emit('event:chats.public.unread', unreadData);
|
||||
}
|
||||
if (messageObj.system || isPublic) {
|
||||
if (messageObj.system) {
|
||||
return;
|
||||
}
|
||||
|
||||
// push unread count only for private rooms
|
||||
const uids = await Messaging.getAllUidsInRoomFromSet(`chat:room:${roomId}:uids:online`);
|
||||
Messaging.pushUnreadCount(uids, unreadData);
|
||||
if (!isPublic) {
|
||||
const uids = await Messaging.getAllUidsInRoomFromSet(`chat:room:${roomId}:uids:online`);
|
||||
Messaging.pushUnreadCount(uids, unreadData);
|
||||
}
|
||||
|
||||
// Delayed notifications
|
||||
let queueObj = Messaging.notifyQueue[`${fromUid}:${roomId}`];
|
||||
@@ -65,27 +84,41 @@ module.exports = function (Messaging) {
|
||||
};
|
||||
|
||||
async function sendNotification(fromUid, roomId, messageObj) {
|
||||
const { displayname } = messageObj.fromUser;
|
||||
const isGroupChat = await Messaging.isGroupChat(roomId);
|
||||
const notification = await notifications.create({
|
||||
type: isGroupChat ? 'new-group-chat' : 'new-chat',
|
||||
subject: `[[email:notif.chat.subject, ${displayname}]]`,
|
||||
bodyShort: `[[notifications:new_message_from, ${displayname}]]`,
|
||||
bodyLong: messageObj.content,
|
||||
nid: `chat_${fromUid}_${roomId}`,
|
||||
from: fromUid,
|
||||
path: `/chats/${messageObj.roomId}`,
|
||||
});
|
||||
fromUid = parseInt(fromUid, 10);
|
||||
|
||||
const [settings, roomData] = await Promise.all([
|
||||
db.getObject(`chat:room:${roomId}:notification:settings`),
|
||||
Messaging.getRoomData(roomId, ['notificationSetting']),
|
||||
]);
|
||||
const roomDefault = roomData.notificationSetting;
|
||||
const uidsToNotify = [];
|
||||
const { ALLMESSAGES } = Messaging.notificationSettings;
|
||||
await batch.processSortedSet(`chat:room:${roomId}:uids:online`, async (uids) => {
|
||||
uids = uids.filter(
|
||||
uid => (parseInt((settings && settings[uid]) || roomDefault, 10) === ALLMESSAGES) &&
|
||||
fromUid !== parseInt(uid, 10)
|
||||
);
|
||||
const hasRead = await Messaging.hasRead(uids, roomId);
|
||||
uids = uids.filter((uid, index) => !hasRead[index] && parseInt(fromUid, 10) !== parseInt(uid, 10));
|
||||
|
||||
notifications.push(notification, uids);
|
||||
uidsToNotify.push(...uids.filter((uid, index) => !hasRead[index]));
|
||||
}, {
|
||||
reverse: true,
|
||||
batch: 500,
|
||||
interval: 1000,
|
||||
interval: 100,
|
||||
});
|
||||
|
||||
if (uidsToNotify.length) {
|
||||
const { displayname } = messageObj.fromUser;
|
||||
const isGroupChat = await Messaging.isGroupChat(roomId);
|
||||
const notification = await notifications.create({
|
||||
type: isGroupChat ? 'new-group-chat' : 'new-chat',
|
||||
subject: `[[email:notif.chat.subject, ${displayname}]]`,
|
||||
bodyShort: `[[notifications:new_message_from, ${displayname}]]`,
|
||||
bodyLong: messageObj.content,
|
||||
nid: `chat_${fromUid}_${roomId}`,
|
||||
from: fromUid,
|
||||
path: `/chats/${messageObj.roomId}`,
|
||||
});
|
||||
await notifications.push(notification, uidsToNotify);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +54,15 @@ module.exports = function (Messaging) {
|
||||
data.groupChat = parseInt(data.groupChat, 10) === 1;
|
||||
}
|
||||
|
||||
if (!fields.length || fields.includes('notificationSetting')) {
|
||||
data.notificationSetting = data.notificationSetting ||
|
||||
(
|
||||
data.public ?
|
||||
Messaging.notificationSettings.ATMENTION :
|
||||
Messaging.notificationSettings.ALLMESSAGES
|
||||
);
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('groups') || !fields.length || fields.includes('groups')) {
|
||||
try {
|
||||
data.groups = JSON.parse(data.groups || '[]');
|
||||
@@ -76,6 +85,7 @@ module.exports = function (Messaging) {
|
||||
const room = {
|
||||
roomId: roomId,
|
||||
timestamp: now,
|
||||
notificationSetting: data.notificationSetting,
|
||||
};
|
||||
|
||||
if (data.hasOwnProperty('roomName') && data.roomName) {
|
||||
@@ -145,10 +155,14 @@ module.exports = function (Messaging) {
|
||||
...roomIds.map(id => `chat:room:${id}:uids`),
|
||||
...roomIds.map(id => `chat:room:${id}:owners`),
|
||||
...roomIds.map(id => `chat:room:${id}:uids:online`),
|
||||
...roomIds.map(id => `chat:room:${id}:notification:settings`),
|
||||
]),
|
||||
db.sortedSetRemove('chat:rooms', roomIds),
|
||||
db.sortedSetRemove('chat:rooms:public', roomIds),
|
||||
db.sortedSetRemove('chat:rooms:public:order', roomIds),
|
||||
db.sortedSetRemove([
|
||||
'chat:rooms',
|
||||
'chat:rooms:public',
|
||||
'chat:rooms:public:order',
|
||||
'chat:rooms:public:lastpost',
|
||||
], roomIds),
|
||||
]);
|
||||
cache.del([
|
||||
'chat:rooms:public:all',
|
||||
@@ -448,7 +462,36 @@ module.exports = function (Messaging) {
|
||||
await db.sortedSetAdd(`chat:room:${roomId}:uids:online`, Date.now(), uid);
|
||||
}
|
||||
|
||||
const [canReply, users, messages, settings, isOwner, onlineUids] = await Promise.all([
|
||||
async function getNotificationOptions() {
|
||||
const userSetting = await db.getObjectField(`chat:room:${roomId}:notification:settings`, uid);
|
||||
const roomDefault = room.notificationSetting;
|
||||
const currentSetting = userSetting || roomDefault;
|
||||
const labels = {
|
||||
[Messaging.notificationSettings.NONE]: { label: '[[modules:chat.notification-setting-none]]', icon: 'fa-ban' },
|
||||
[Messaging.notificationSettings.ATMENTION]: { label: '[[modules:chat.notification-setting-at-mention-only]]', icon: 'fa-at' },
|
||||
[Messaging.notificationSettings.ALLMESSAGES]: { label: '[[modules:chat.notification-setting-all-messages]]', icon: 'fa-comment-o' },
|
||||
};
|
||||
const options = [
|
||||
{
|
||||
label: '[[modules:chat.notification-setting-room-default]]',
|
||||
subLabel: labels[roomDefault].label || '',
|
||||
icon: labels[roomDefault].icon,
|
||||
value: -1,
|
||||
selected: userSetting === null,
|
||||
},
|
||||
];
|
||||
Object.keys(labels).forEach((key) => {
|
||||
options.push({
|
||||
label: labels[key].label,
|
||||
icon: labels[key].icon,
|
||||
value: key,
|
||||
selected: parseInt(userSetting, 10) === parseInt(key, 10),
|
||||
});
|
||||
});
|
||||
return { options, selectedIcon: labels[currentSetting].icon };
|
||||
}
|
||||
|
||||
const [canReply, users, messages, settings, isOwner, onlineUids, notifOptions] = await Promise.all([
|
||||
Messaging.canReply(roomId, uid),
|
||||
Messaging.getUsersInRoomFromSet(`chat:room:${roomId}:uids:online`, roomId, 0, 39, true),
|
||||
Messaging.getMessages({
|
||||
@@ -460,6 +503,7 @@ module.exports = function (Messaging) {
|
||||
user.getSettings(uid),
|
||||
Messaging.isRoomOwner(uid, roomId),
|
||||
io.getUidsInRoom(`chat_room_${roomId}`),
|
||||
getNotificationOptions(),
|
||||
]);
|
||||
|
||||
users.forEach((user) => {
|
||||
@@ -481,6 +525,8 @@ module.exports = function (Messaging) {
|
||||
room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2;
|
||||
room.isAdminOrGlobalMod = isAdmin || isGlobalMod;
|
||||
room.isAdmin = isAdmin;
|
||||
room.notificationOptions = notifOptions.options;
|
||||
room.notificationOptionsIcon = notifOptions.selectedIcon;
|
||||
|
||||
const payload = await plugins.hooks.fire('filter:messaging.loadRoom', { uid, data, room });
|
||||
return payload.room;
|
||||
|
||||
@@ -33,6 +33,25 @@ module.exports = function (Messaging) {
|
||||
};
|
||||
|
||||
Messaging.hasRead = async (uids, roomId) => {
|
||||
if (!uids.length) {
|
||||
return [];
|
||||
}
|
||||
const roomData = await Messaging.getRoomData(roomId);
|
||||
if (!roomData) {
|
||||
return uids.map(() => false);
|
||||
}
|
||||
if (roomData.public) {
|
||||
const [userTimestamps, mids] = await Promise.all([
|
||||
db.getObjectsFields(uids.map(uid => `uid:${uid}:chat:rooms:read`), [roomId]),
|
||||
db.getSortedSetRevRangeWithScores(`chat:room:${roomId}:mids`, 0, 0),
|
||||
]);
|
||||
const lastMsgTimestamp = mids[0] ? mids[0].score : 0;
|
||||
return uids.map(
|
||||
(uid, index) => !userTimestamps[index] ||
|
||||
!userTimestamps[index][roomId] ||
|
||||
parseInt(userTimestamps[index][roomId], 10) > lastMsgTimestamp
|
||||
);
|
||||
}
|
||||
const isMembers = await db.isMemberOfSortedSets(
|
||||
uids.map(uid => `uid:${uid}:chat:rooms:unread`),
|
||||
roomId
|
||||
|
||||
@@ -159,7 +159,7 @@ Notifications.push = async function (notification, uids) {
|
||||
winston.error(err.stack);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
async function pushToUids(uids, notification) {
|
||||
|
||||
@@ -200,4 +200,17 @@ SocketModules.chats.toggleOwner = async (socket, data) => {
|
||||
await Messaging.toggleOwner(data.uid, data.roomId);
|
||||
};
|
||||
|
||||
SocketModules.chats.setNotificationSetting = async (socket, data) => {
|
||||
if (!data || !utils.isNumber(data.value) || !data.roomId) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const inRoom = await Messaging.isUserInRoom(socket.uid, data.roomId);
|
||||
if (!inRoom) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
await Messaging.setUserNotificationSetting(socket.uid, data.roomId, data.value);
|
||||
};
|
||||
|
||||
require('../promisify')(SocketModules);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<div class="mb-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">[[modules:chat.room-name-optional]]</label>
|
||||
<input component="chat/room/name" class="form-control"/>
|
||||
<label class="form-label text-nowrap">[[modules:chat.room-name-optional]]</label>
|
||||
<input component="chat/room/name" class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="dropdown mb-3">
|
||||
<div class="dropdown">
|
||||
<label class="form-label">[[modules:chat.add-user]]</label>
|
||||
<input component="chat/search" class="form-control" type="text" placeholder="[[global:user-search-prompt]]" data-bs-toggle="dropdown"/>
|
||||
<ul component="chat/search/list" class="dropdown-menu p-1 overflow-auto" style="max-height: 400px;">
|
||||
@@ -15,7 +16,7 @@
|
||||
{{{ end }}}
|
||||
</ul>
|
||||
</div>
|
||||
<ul component="chat/room/users" class="list-group">
|
||||
<ul component="chat/room/users" class="list-group mt-2">
|
||||
{{{ each selectedUsers }}}
|
||||
<li class="list-group-item d-flex gap-2 align-items-center justify-content-between" component="chat/user" data-uid="{./uid}">
|
||||
<a href="#" class="text-reset text-decoration-none">{buildAvatar(@value, "24px", true)} {./username}</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="mb-3">
|
||||
<div class="">
|
||||
<label class="form-label">[[modules:chat.add-user]]</label>
|
||||
<input component="chat/manage/user/add/search" class="form-control" type="text" placeholder="[[global:user-search-prompt]]" />
|
||||
<p class="text-danger"></p>
|
||||
@@ -12,16 +12,29 @@
|
||||
<li class="list-group-item"><i class="fa fa-spinner fa-spin"></i> [[modules:chat.retrieving-users]]</li>
|
||||
</ul>
|
||||
|
||||
{{{ if (user.isAdmin && group.public ) }}}
|
||||
{{{ if user.isAdmin }}}
|
||||
<hr/>
|
||||
<div class="d-flex gap-2 mb-3 align-items-center justify-content-between">
|
||||
<label class="form-label text-nowrap mb-0">[[modules:chat.default-notification-setting]]</label>
|
||||
<select component="chat/room/notification/setting" class="form-select" style="width: 200px;">
|
||||
<option value="1" {{{ if (room.notificationSetting == "1") }}}selected{{{ end }}}>[[modules:chat.notification-setting-none]]</option>
|
||||
<option value="2" {{{ if (room.notificationSetting == "2") }}}selected{{{ end }}}>[[modules:chat.notification-setting-at-mention-only]]</option>
|
||||
<option value="3" {{{ if (room.notificationSetting == "3") }}}selected{{{ end }}}>[[modules:chat.notification-setting-all-messages]]</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{{ if room.public }}}
|
||||
|
||||
<label class="form-label">[[modules:chat.select-groups]]</label>
|
||||
|
||||
<select component="chat/room/groups" class="form-select mb-1" multiple size="10">
|
||||
<select component="chat/room/groups" class="form-select mb-3" multiple size="10">
|
||||
{{{ each groups }}}
|
||||
<option value="{./displayName}" {{{ if ./selected }}}selected{{{ end }}}>{./displayName}</option>
|
||||
{{{ end }}}
|
||||
</select>
|
||||
{{{ end }}}
|
||||
<div class="d-flex justify-content-end">
|
||||
<button component="chat/manage/save/groups" class="btn btn-sm btn-primary">[[global:save]]</button>
|
||||
<button component="chat/manage/save" class="btn btn-sm btn-primary">[[global:save]]</button>
|
||||
</div>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
Reference in New Issue
Block a user