Chat notifs (#11832)

* first part of chat notifs

* moved default notif to manage page

* spec

* notifs

* delete settings on room delete
isekai-main
Barış Soner Uşaklı 2 years ago committed by GitHub
parent f377650161
commit 61f036ce1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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",

@ -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?",

@ -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:

@ -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:

@ -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,

@ -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);
}); });
}); });

@ -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,
}); });

@ -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) => {

@ -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,
});
} }
}; };

@ -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;

@ -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

@ -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) {

@ -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);

@ -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>

@ -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>
Loading…
Cancel
Save