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:
Barış Soner Uşaklı
2023-07-21 15:31:34 -04:00
committed by GitHub
parent f377650161
commit 61f036ce1d
15 changed files with 249 additions and 46 deletions

View File

@@ -97,14 +97,14 @@
"nodebb-plugin-emoji": "5.1.3", "nodebb-plugin-emoji": "5.1.3",
"nodebb-plugin-emoji-android": "4.0.0", "nodebb-plugin-emoji-android": "4.0.0",
"nodebb-plugin-markdown": "12.1.7", "nodebb-plugin-markdown": "12.1.7",
"nodebb-plugin-mentions": "4.3.2", "nodebb-plugin-mentions": "4.3.3",
"nodebb-plugin-ntfy": "1.1.0", "nodebb-plugin-ntfy": "1.1.0",
"nodebb-plugin-spam-be-gone": "2.1.1", "nodebb-plugin-spam-be-gone": "2.1.1",
"nodebb-rewards-essentials": "0.2.3", "nodebb-rewards-essentials": "0.2.3",
"nodebb-theme-harmony": "1.1.13", "nodebb-theme-harmony": "1.1.14",
"nodebb-theme-lavender": "7.1.3", "nodebb-theme-lavender": "7.1.3",
"nodebb-theme-peace": "2.1.3", "nodebb-theme-peace": "2.1.3",
"nodebb-theme-persona": "13.2.6", "nodebb-theme-persona": "13.2.7",
"nodebb-widget-essentials": "7.0.13", "nodebb-widget-essentials": "7.0.13",
"nodemailer": "6.9.4", "nodemailer": "6.9.4",
"nprogress": "0.2.0", "nprogress": "0.2.0",

View File

@@ -36,6 +36,12 @@
"chat.public.groups-help": "To create a chat room that is visible to all users select registered-users from the group list.", "chat.public.groups-help": "To create a chat room that is visible to all users select registered-users from the group list.",
"chat.manage-room": "Manage Chat Room", "chat.manage-room": "Manage Chat Room",
"chat.add-user": "Add User", "chat.add-user": "Add User",
"chat.notification-settings": "Notification Settings",
"chat.default-notification-setting": "Default Notification Setting",
"chat.notification-setting-room-default": "Room Default",
"chat.notification-setting-none": "No notifications",
"chat.notification-setting-at-mention-only": "@mention only",
"chat.notification-setting-all-messages": "All messages",
"chat.select-groups": "Select Groups", "chat.select-groups": "Select Groups",
"chat.add-user-help": "Search for users here. When selected, the user will be added to the chat. The new user will not be able to see chat messages written before they were added to the conversation. Only room owners (<i class=\"fa fa-star text-warning\"></i>) may remove users from chat rooms.", "chat.add-user-help": "Search for users here. When selected, the user will be added to the chat. The new user will not be able to see chat messages written before they were added to the conversation. Only room owners (<i class=\"fa fa-star text-warning\"></i>) may remove users from chat rooms.",
"chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?",

View File

@@ -22,6 +22,8 @@ RoomObject:
timestamp: timestamp:
type: number type: number
description: Timestamp of when room was created description: Timestamp of when room was created
notificationSetting:
type: number
MessageObject: MessageObject:
type: object type: object
properties: properties:

View File

@@ -39,6 +39,12 @@ get:
timestamp: timestamp:
type: number type: number
description: Timestamp of when room was created description: Timestamp of when room was created
notificationSetting:
type: number
notificationOptions:
type: array
notificationOptionsIcon:
type: string
messages: messages:
type: array type: array
items: items:
@@ -318,6 +324,8 @@ get:
type: string type: string
chatWithMessage: chatWithMessage:
type: string type: string
notificationSetting:
type: number
publicRooms: publicRooms:
type: array type: array
items: items:

View File

@@ -99,6 +99,8 @@ define('forum/chats', [
}); });
userList.init(roomId, mainWrapper); userList.init(roomId, mainWrapper);
Chats.addPublicRoomSortHandler(); Chats.addPublicRoomSortHandler();
Chats.addTooltipHandler();
Chats.addNotificationSettingHandler();
}; };
Chats.addPublicRoomSortHandler = function () { Chats.addPublicRoomSortHandler = function () {
@@ -122,6 +124,37 @@ define('forum/chats', [
} }
}; };
Chats.addTooltipHandler = function () {
$('[data-manual-tooltip]').tooltip({
trigger: 'manual',
animation: false,
placement: 'bottom',
}).on('mouseenter', function (ev) {
const target = $(ev.target);
const isDropdown = target.hasClass('dropdown-menu') || !!target.parents('.dropdown-menu').length;
if (!isDropdown) {
$(this).tooltip('show');
}
}).on('click mouseleave', function () {
$(this).tooltip('hide');
});
};
Chats.addNotificationSettingHandler = function () {
const notifSettingEl = $('[component="chat/notification/setting"]');
notifSettingEl.find('[data-value]').on('click', async function () {
notifSettingEl.find('i.fa-check').addClass('hidden');
const $this = $(this);
$this.find('i.fa-check').removeClass('hidden');
$('[component="chat/notification/setting/icon"]').attr('class', `fa ${$this.attr('data-icon')}`);
await socket.emit('modules.chats.setNotificationSetting', {
roomId: ajaxify.data.roomId,
value: $this.attr('data-value'),
});
});
};
Chats.addUploadHandler = function (options) { Chats.addUploadHandler = function (options) {
uploadHelpers.init({ uploadHelpers.init({
dragDropAreaEl: options.dragDropAreaEl, dragDropAreaEl: options.dragDropAreaEl,

View File

@@ -23,7 +23,7 @@ define('forum/chats/manage', [
const html = await app.parseAndTranslate('modals/manage-room', { const html = await app.parseAndTranslate('modals/manage-room', {
groups, groups,
user: app.user, user: app.user,
group: ajaxify.data, room: ajaxify.data,
}); });
modal = bootbox.dialog({ modal = bootbox.dialog({
title: '[[modules:chat.manage-room]]', title: '[[modules:chat.manage-room]]',
@@ -67,14 +67,28 @@ define('forum/chats/manage', [
}); });
}); });
modal.find('[component="chat/manage/save/groups"]').on('click', (ev) => { modal.find('[component="chat/manage/save"]').on('click', () => {
const btn = $(ev.target); const notifSettingEl = modal.find('[component="chat/room/notification/setting"]');
api.put(`/chats/${roomId}`, { api.put(`/chats/${roomId}`, {
groups: modal.find('[component="chat/room/groups"]').val(), groups: modal.find('[component="chat/room/groups"]').val(),
notificationSetting: notifSettingEl.val(),
}).then((payload) => { }).then((payload) => {
ajaxify.data.groups = payload.groups; ajaxify.data.groups = payload.groups;
btn.addClass('btn-success'); ajaxify.data.notificationSetting = payload.notificationSetting;
setTimeout(() => btn.removeClass('btn-success'), 1000); const roomDefaultOption = payload.notificationOptions[0];
$('[component="chat/notification/setting"] [data-icon]').first().attr(
'data-icon', roomDefaultOption.icon
);
$('[component="chat/notification/setting/sub-label"]').translateText(
roomDefaultOption.subLabel
);
if (roomDefaultOption.selected) {
$('[component="chat/notification/setting/icon"]').attr(
'class', `fa ${roomDefaultOption.icon}`
);
}
modal.modal('hide');
}).catch(alerts.error); }).catch(alerts.error);
}); });
}); });

View File

@@ -40,11 +40,13 @@ chatsAPI.create = async function (caller, data) {
if (!data) { if (!data) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} }
const isPublic = data.type === 'public'; const isPublic = data.type === 'public';
const isAdmin = await user.isAdministrator(caller.uid); const isAdmin = await user.isAdministrator(caller.uid);
if (isPublic && !isAdmin) { if (isPublic && !isAdmin) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
if (!data.uids || !Array.isArray(data.uids)) { if (!data.uids || !Array.isArray(data.uids)) {
throw new Error(`[[error:wrong-parameter-type, uids, ${typeof data.uids}, Array]]`); 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)) { if (isPublic && (!Array.isArray(data.groups) || !data.groups.length)) {
throw new Error('[[error:no-groups-selected]]'); 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))); await Promise.all(data.uids.map(async uid => messaging.canMessageUser(caller.uid, uid)));
const roomId = await messaging.newRoom(caller.uid, data); const roomId = await messaging.newRoom(caller.uid, data);
@@ -108,7 +115,6 @@ chatsAPI.update = async (caller, data) => {
}); });
} }
} }
if (data.hasOwnProperty('groups')) {
const [roomData, isAdmin] = await Promise.all([ const [roomData, isAdmin] = await Promise.all([
messaging.getRoomData(data.roomId), messaging.getRoomData(data.roomId),
user.isAdministrator(caller.uid), user.isAdministrator(caller.uid),
@@ -116,10 +122,14 @@ chatsAPI.update = async (caller, data) => {
if (!roomData) { if (!roomData) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} }
if (data.hasOwnProperty('groups')) {
if (roomData.public && isAdmin) { if (roomData.public && isAdmin) {
await db.setObjectField(`chat:room:${data.roomId}`, 'groups', JSON.stringify(data.groups)); 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, { return messaging.loadRoom(caller.uid, {
roomId: data.roomId, roomId: data.roomId,
}); });

View File

@@ -25,6 +25,11 @@ require('./rooms')(Messaging);
require('./unread')(Messaging); require('./unread')(Messaging);
require('./notifications')(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.messageExists = async mid => db.exists(`message:${mid}`);
Messaging.getMessages = async (params) => { Messaging.getMessages = async (params) => {

View File

@@ -12,6 +12,23 @@ const meta = require('../meta');
module.exports = function (Messaging) { module.exports = function (Messaging) {
// Only used to notify a user of a new chat message // Only used to notify a user of a new chat message
Messaging.notifyQueue = {}; 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) => { Messaging.notifyUsersInRoom = async (fromUid, roomId, messageObj) => {
const isPublic = parseInt(await db.getObjectField(`chat:room:${roomId}`, 'public'), 10) === 1; 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 // delivers unread public msg to all online users on the chats page
io.in(`chat_room_public_${roomId}`).emit('event:chats.public.unread', unreadData); io.in(`chat_room_public_${roomId}`).emit('event:chats.public.unread', unreadData);
} }
if (messageObj.system || isPublic) { if (messageObj.system) {
return; return;
} }
// push unread count only for private rooms // push unread count only for private rooms
if (!isPublic) {
const uids = await Messaging.getAllUidsInRoomFromSet(`chat:room:${roomId}:uids:online`); const uids = await Messaging.getAllUidsInRoomFromSet(`chat:room:${roomId}:uids:online`);
Messaging.pushUnreadCount(uids, unreadData); Messaging.pushUnreadCount(uids, unreadData);
}
// Delayed notifications // Delayed notifications
let queueObj = Messaging.notifyQueue[`${fromUid}:${roomId}`]; let queueObj = Messaging.notifyQueue[`${fromUid}:${roomId}`];
@@ -65,6 +84,29 @@ module.exports = function (Messaging) {
}; };
async function sendNotification(fromUid, roomId, messageObj) { async function sendNotification(fromUid, roomId, messageObj) {
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);
uidsToNotify.push(...uids.filter((uid, index) => !hasRead[index]));
}, {
reverse: true,
batch: 500,
interval: 100,
});
if (uidsToNotify.length) {
const { displayname } = messageObj.fromUser; const { displayname } = messageObj.fromUser;
const isGroupChat = await Messaging.isGroupChat(roomId); const isGroupChat = await Messaging.isGroupChat(roomId);
const notification = await notifications.create({ const notification = await notifications.create({
@@ -76,16 +118,7 @@ module.exports = function (Messaging) {
from: fromUid, from: fromUid,
path: `/chats/${messageObj.roomId}`, path: `/chats/${messageObj.roomId}`,
}); });
await notifications.push(notification, uidsToNotify);
await batch.processSortedSet(`chat:room:${roomId}:uids:online`, async (uids) => { }
const hasRead = await Messaging.hasRead(uids, roomId);
uids = uids.filter((uid, index) => !hasRead[index] && parseInt(fromUid, 10) !== parseInt(uid, 10));
notifications.push(notification, uids);
}, {
reverse: true,
batch: 500,
interval: 1000,
});
} }
}; };

View File

@@ -54,6 +54,15 @@ module.exports = function (Messaging) {
data.groupChat = parseInt(data.groupChat, 10) === 1; 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')) { if (data.hasOwnProperty('groups') || !fields.length || fields.includes('groups')) {
try { try {
data.groups = JSON.parse(data.groups || '[]'); data.groups = JSON.parse(data.groups || '[]');
@@ -76,6 +85,7 @@ module.exports = function (Messaging) {
const room = { const room = {
roomId: roomId, roomId: roomId,
timestamp: now, timestamp: now,
notificationSetting: data.notificationSetting,
}; };
if (data.hasOwnProperty('roomName') && data.roomName) { 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}:uids`),
...roomIds.map(id => `chat:room:${id}:owners`), ...roomIds.map(id => `chat:room:${id}:owners`),
...roomIds.map(id => `chat:room:${id}:uids:online`), ...roomIds.map(id => `chat:room:${id}:uids:online`),
...roomIds.map(id => `chat:room:${id}:notification:settings`),
]), ]),
db.sortedSetRemove('chat:rooms', roomIds), db.sortedSetRemove([
db.sortedSetRemove('chat:rooms:public', roomIds), 'chat:rooms',
db.sortedSetRemove('chat:rooms:public:order', roomIds), 'chat:rooms:public',
'chat:rooms:public:order',
'chat:rooms:public:lastpost',
], roomIds),
]); ]);
cache.del([ cache.del([
'chat:rooms:public:all', 'chat:rooms:public:all',
@@ -448,7 +462,36 @@ module.exports = function (Messaging) {
await db.sortedSetAdd(`chat:room:${roomId}:uids:online`, Date.now(), uid); 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.canReply(roomId, uid),
Messaging.getUsersInRoomFromSet(`chat:room:${roomId}:uids:online`, roomId, 0, 39, true), Messaging.getUsersInRoomFromSet(`chat:room:${roomId}:uids:online`, roomId, 0, 39, true),
Messaging.getMessages({ Messaging.getMessages({
@@ -460,6 +503,7 @@ module.exports = function (Messaging) {
user.getSettings(uid), user.getSettings(uid),
Messaging.isRoomOwner(uid, roomId), Messaging.isRoomOwner(uid, roomId),
io.getUidsInRoom(`chat_room_${roomId}`), io.getUidsInRoom(`chat_room_${roomId}`),
getNotificationOptions(),
]); ]);
users.forEach((user) => { users.forEach((user) => {
@@ -481,6 +525,8 @@ module.exports = function (Messaging) {
room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2;
room.isAdminOrGlobalMod = isAdmin || isGlobalMod; room.isAdminOrGlobalMod = isAdmin || isGlobalMod;
room.isAdmin = isAdmin; room.isAdmin = isAdmin;
room.notificationOptions = notifOptions.options;
room.notificationOptionsIcon = notifOptions.selectedIcon;
const payload = await plugins.hooks.fire('filter:messaging.loadRoom', { uid, data, room }); const payload = await plugins.hooks.fire('filter:messaging.loadRoom', { uid, data, room });
return payload.room; return payload.room;

View File

@@ -33,6 +33,25 @@ module.exports = function (Messaging) {
}; };
Messaging.hasRead = async (uids, roomId) => { 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( const isMembers = await db.isMemberOfSortedSets(
uids.map(uid => `uid:${uid}:chat:rooms:unread`), uids.map(uid => `uid:${uid}:chat:rooms:unread`),
roomId roomId

View File

@@ -159,7 +159,7 @@ Notifications.push = async function (notification, uids) {
winston.error(err.stack); winston.error(err.stack);
} }
}); });
}, 1000); }, 500);
}; };
async function pushToUids(uids, notification) { async function pushToUids(uids, notification) {

View File

@@ -200,4 +200,17 @@ SocketModules.chats.toggleOwner = async (socket, data) => {
await Messaging.toggleOwner(data.uid, data.roomId); 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); require('../promisify')(SocketModules);

View File

@@ -1,10 +1,11 @@
<div class="mb-3"> <div class="mb-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">[[modules:chat.room-name-optional]]</label> <label class="form-label text-nowrap">[[modules:chat.room-name-optional]]</label>
<input component="chat/room/name" class="form-control" /> <input component="chat/room/name" class="form-control" />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="dropdown mb-3"> <div class="dropdown">
<label class="form-label">[[modules:chat.add-user]]</label> <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"/> <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;"> <ul component="chat/search/list" class="dropdown-menu p-1 overflow-auto" style="max-height: 400px;">
@@ -15,7 +16,7 @@
{{{ end }}} {{{ end }}}
</ul> </ul>
</div> </div>
<ul component="chat/room/users" class="list-group"> <ul component="chat/room/users" class="list-group mt-2">
{{{ each selectedUsers }}} {{{ each selectedUsers }}}
<li class="list-group-item d-flex gap-2 align-items-center justify-content-between" component="chat/user" data-uid="{./uid}"> <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> <a href="#" class="text-reset text-decoration-none">{buildAvatar(@value, "24px", true)} {./username}</a>

View File

@@ -1,4 +1,4 @@
<div class="mb-3"> <div class="">
<label class="form-label">[[modules:chat.add-user]]</label> <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]]" /> <input component="chat/manage/user/add/search" class="form-control" type="text" placeholder="[[global:user-search-prompt]]" />
<p class="text-danger"></p> <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> <li class="list-group-item"><i class="fa fa-spinner fa-spin"></i> [[modules:chat.retrieving-users]]</li>
</ul> </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> <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 }}} {{{ each groups }}}
<option value="{./displayName}" {{{ if ./selected }}}selected{{{ end }}}>{./displayName}</option> <option value="{./displayName}" {{{ if ./selected }}}selected{{{ end }}}>{./displayName}</option>
{{{ end }}} {{{ end }}}
</select> </select>
{{{ end }}}
<div class="d-flex justify-content-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> </div>
{{{ end }}} {{{ end }}}
</div> </div>