mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-27 09:06:15 +01:00
feat: typing user list in chat
This commit is contained in:
@@ -102,10 +102,10 @@
|
|||||||
"nodebb-plugin-ntfy": "1.5.0",
|
"nodebb-plugin-ntfy": "1.5.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.50",
|
"nodebb-theme-harmony": "1.1.51",
|
||||||
"nodebb-theme-lavender": "7.1.3",
|
"nodebb-theme-lavender": "7.1.3",
|
||||||
"nodebb-theme-peace": "2.1.18",
|
"nodebb-theme-peace": "2.1.18",
|
||||||
"nodebb-theme-persona": "13.2.25",
|
"nodebb-theme-persona": "13.2.26",
|
||||||
"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",
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
"chat.chat-with-usernames-and-x-others": "Chat with %1 & %2 others",
|
"chat.chat-with-usernames-and-x-others": "Chat with %1 & %2 others",
|
||||||
"chat.send": "Send",
|
"chat.send": "Send",
|
||||||
"chat.no_active": "You have no active chats.",
|
"chat.no_active": "You have no active chats.",
|
||||||
"chat.user_typing": "%1 is typing ...",
|
"chat.user_typing_1": "<strong>%1</strong> is typing ...",
|
||||||
|
"chat.user_typing_2": "<strong>%1</strong> and <strong>%2</strong> are typing ...",
|
||||||
|
"chat.user_typing_3": "<strong>%1</strong>, <strong>%2</strong> and <strong>%3</strong> are typing ...",
|
||||||
|
"chat.user_typing_n": "<strong>%1</strong>, <strong>%2</strong> and <strong>%3</strong> others are typing ...",
|
||||||
"chat.user_has_messaged_you": "%1 has messaged you.",
|
"chat.user_has_messaged_you": "%1 has messaged you.",
|
||||||
"chat.replying-to": "Replying to %1",
|
"chat.replying-to": "Replying to %1",
|
||||||
"chat.see_all": "All chats",
|
"chat.see_all": "All chats",
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ define('forum/chats', [
|
|||||||
'chat',
|
'chat',
|
||||||
'api',
|
'api',
|
||||||
'uploadHelpers',
|
'uploadHelpers',
|
||||||
|
'translator',
|
||||||
], function (
|
], function (
|
||||||
components, mousetrap, recentChats, create,
|
components, mousetrap, recentChats, create,
|
||||||
manage, messages, userList, messageSearch, pinnedMessages,
|
manage, messages, userList, messageSearch, pinnedMessages,
|
||||||
autocomplete, hooks, bootbox, alerts, chatModule, api,
|
autocomplete, hooks, bootbox, alerts, chatModule, api,
|
||||||
uploadHelpers
|
uploadHelpers, translator
|
||||||
) {
|
) {
|
||||||
const Chats = {
|
const Chats = {
|
||||||
initialised: false,
|
initialised: false,
|
||||||
@@ -89,6 +90,7 @@ define('forum/chats', [
|
|||||||
Chats.addParentHandler(mainWrapper);
|
Chats.addParentHandler(mainWrapper);
|
||||||
Chats.addCharactersLeftHandler(mainWrapper);
|
Chats.addCharactersLeftHandler(mainWrapper);
|
||||||
Chats.addTextareaResizeHandler(mainWrapper);
|
Chats.addTextareaResizeHandler(mainWrapper);
|
||||||
|
Chats.addTypingHandler(mainWrapper, roomId);
|
||||||
Chats.addIPHandler(mainWrapper);
|
Chats.addIPHandler(mainWrapper);
|
||||||
Chats.createAutoComplete(roomId, $('[component="chat/input"]'));
|
Chats.createAutoComplete(roomId, $('[component="chat/input"]'));
|
||||||
Chats.addUploadHandler({
|
Chats.addUploadHandler({
|
||||||
@@ -313,6 +315,23 @@ define('forum/chats', [
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Chats.addTypingHandler = function (parent, roomId) {
|
||||||
|
const textarea = parent.find('[component="chat/input"]');
|
||||||
|
function emitTyping(typing) {
|
||||||
|
socket.emit('modules.chats.typing', {
|
||||||
|
roomId: roomId,
|
||||||
|
typing: typing,
|
||||||
|
username: app.user.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.on('focus', () => emitTyping(!!textarea.val()));
|
||||||
|
textarea.on('blur', () => emitTyping(false));
|
||||||
|
textarea.on('input', utils.throttle(function () {
|
||||||
|
emitTyping(!!textarea.val());
|
||||||
|
}, 2500, true));
|
||||||
|
};
|
||||||
|
|
||||||
Chats.addActionHandlers = function (element, roomId) {
|
Chats.addActionHandlers = function (element, roomId) {
|
||||||
element.on('click', '[data-mid] [data-action]', function () {
|
element.on('click', '[data-mid] [data-action]', function () {
|
||||||
const msgEl = $(this).parents('[data-mid]');
|
const msgEl = $(this).parents('[data-mid]');
|
||||||
@@ -544,6 +563,7 @@ define('forum/chats', [
|
|||||||
const html = await app.parseAndTranslate('partials/chats/message-window', payload);
|
const html = await app.parseAndTranslate('partials/chats/message-window', payload);
|
||||||
const mainWrapper = components.get('chat/main-wrapper');
|
const mainWrapper = components.get('chat/main-wrapper');
|
||||||
mainWrapper.html(html);
|
mainWrapper.html(html);
|
||||||
|
mainWrapper.attr('data-roomid', roomId);
|
||||||
chatNavWrapper = $('[component="chat/nav-wrapper"]');
|
chatNavWrapper = $('[component="chat/nav-wrapper"]');
|
||||||
html.find('.timeago').timeago();
|
html.find('.timeago').timeago();
|
||||||
ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId };
|
ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId };
|
||||||
@@ -636,6 +656,13 @@ define('forum/chats', [
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('event:chats.typing', async (data) => {
|
||||||
|
if (chatModule.isFromBlockedUser(data.uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatModule.updateTypingUserList($(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"]`), data);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Chats.markChatPageElUnread = function (data) {
|
Chats.markChatPageElUnread = function (data) {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ define('forum/header/chat', [
|
|||||||
socket.removeListener('event:chats.receive', onChatMessageReceived);
|
socket.removeListener('event:chats.receive', onChatMessageReceived);
|
||||||
socket.on('event:chats.receive', onChatMessageReceived);
|
socket.on('event:chats.receive', onChatMessageReceived);
|
||||||
|
|
||||||
socket.removeListener('event:user_status_change', onUserStatusChange);
|
socket.removeListener('event:chats.typing', onUserTyping);
|
||||||
socket.on('event:user_status_change', onUserStatusChange);
|
socket.on('event:chats.typing', onUserTyping);
|
||||||
|
|
||||||
socket.removeListener('event:chats.roomRename', onRoomRename);
|
socket.removeListener('event:chats.roomRename', onRoomRename);
|
||||||
socket.on('event:chats.roomRename', onRoomRename);
|
socket.on('event:chats.roomRename', onRoomRename);
|
||||||
@@ -63,8 +63,8 @@ define('forum/header/chat', [
|
|||||||
requireAndCall('onChatMessageReceived', data);
|
requireAndCall('onChatMessageReceived', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserStatusChange(data) {
|
function onUserTyping(data) {
|
||||||
requireAndCall('onUserStatusChange', data);
|
requireAndCall('onUserTyping', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRoomRename(data) {
|
function onRoomRename(data) {
|
||||||
|
|||||||
@@ -226,11 +226,6 @@ define('chat', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.onUserStatusChange = function (data) {
|
|
||||||
const modal = module.getModal(data.uid);
|
|
||||||
app.updateUserStatus(modal.find('[component="user/status"]'), data.status);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.onRoomRename = function (data) {
|
module.onRoomRename = function (data) {
|
||||||
const modal = module.getModal(data.roomId);
|
const modal = module.getModal(data.roomId);
|
||||||
const titleEl = modal.find('[component="chat/room/name"]');
|
const titleEl = modal.find('[component="chat/room/name"]');
|
||||||
@@ -252,6 +247,44 @@ define('chat', [
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.onUserTyping = function (data) {
|
||||||
|
if (module.isFromBlockedUser(data.uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modal = module.getModal(data.roomId);
|
||||||
|
if (modal.length) {
|
||||||
|
module.updateTypingUserList(modal, data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.updateTypingUserList = async function (container, { uid, username, typing }) {
|
||||||
|
const typingEl = container.find(`[component="chat/composer/typing"]`);
|
||||||
|
const typingUsersList = typingEl.find('[component="chat/composer/typing/users"]');
|
||||||
|
const userEl = typingUsersList.find(`[data-uid="${uid}"]`);
|
||||||
|
|
||||||
|
if (typing && !userEl.length) {
|
||||||
|
$(`<div/>`).attr('data-uid', uid)
|
||||||
|
.text(username)
|
||||||
|
.appendTo(typingUsersList);
|
||||||
|
} else if (!typing && userEl.length) {
|
||||||
|
userEl.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernames = [];
|
||||||
|
typingUsersList.children().each((i, el) => {
|
||||||
|
usernames.push($(el).text());
|
||||||
|
});
|
||||||
|
|
||||||
|
const typingTextEl = typingEl.find('[component="chat/composer/typing/text"]');
|
||||||
|
const count = usernames.length > 3 ? 'n' : usernames.length;
|
||||||
|
if (count) {
|
||||||
|
const key = `modules:chat.user_typing_${count}`;
|
||||||
|
const compiled = translator.compile.apply(null, [key, ...usernames]);
|
||||||
|
typingTextEl.html(await translator.translate(compiled));
|
||||||
|
}
|
||||||
|
typingTextEl.toggleClass('hidden', !usernames.length);
|
||||||
|
};
|
||||||
|
|
||||||
module.getModal = function (roomId) {
|
module.getModal = function (roomId) {
|
||||||
return $('#chat-modal-' + roomId);
|
return $('#chat-modal-' + roomId);
|
||||||
};
|
};
|
||||||
@@ -370,6 +403,7 @@ define('chat', [
|
|||||||
Chats.addParentHandler(chatModal.find('[component="chat/message/content"]'));
|
Chats.addParentHandler(chatModal.find('[component="chat/message/content"]'));
|
||||||
Chats.addCharactersLeftHandler(chatModal);
|
Chats.addCharactersLeftHandler(chatModal);
|
||||||
Chats.addTextareaResizeHandler(chatModal);
|
Chats.addTextareaResizeHandler(chatModal);
|
||||||
|
Chats.addTypingHandler(chatModal, roomId);
|
||||||
Chats.addIPHandler(chatModal);
|
Chats.addIPHandler(chatModal);
|
||||||
Chats.addTooltipHandler(chatModal);
|
Chats.addTooltipHandler(chatModal);
|
||||||
Chats.addUploadHandler({
|
Chats.addUploadHandler({
|
||||||
|
|||||||
@@ -707,9 +707,7 @@ const utils = {
|
|||||||
const args = arguments;
|
const args = arguments;
|
||||||
const later = function () {
|
const later = function () {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
if (!immediate) {
|
|
||||||
func.apply(context, args);
|
func.apply(context, args);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const callNow = immediate && !timeout;
|
const callNow = immediate && !timeout;
|
||||||
if (!timeout) {
|
if (!timeout) {
|
||||||
|
|||||||
@@ -97,6 +97,19 @@ function onConnection(socket) {
|
|||||||
}, onMessage, socket, payload);
|
}, onMessage, socket, payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('disconnecting', () => {
|
||||||
|
for (const room of socket.rooms) {
|
||||||
|
if (room && room.match(/^chat_room_\d+$/)) {
|
||||||
|
Sockets.server.in(room).emit('event:chats.typing', {
|
||||||
|
roomId: room.split('_').pop(),
|
||||||
|
uid: socket.uid,
|
||||||
|
username: '',
|
||||||
|
typing: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
onDisconnect(socket);
|
onDisconnect(socket);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const validator = require('validator');
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const Messaging = require('../messaging');
|
const Messaging = require('../messaging');
|
||||||
@@ -264,5 +265,21 @@ SocketModules.chats.loadPinnedMessages = async (socket, data) => {
|
|||||||
return pinnedMsgs;
|
return pinnedMsgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SocketModules.chats.typing = async (socket, data) => {
|
||||||
|
if (!data || !utils.isNumber(data.roomId) || typeof data.typing !== 'boolean') {
|
||||||
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
|
const isInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId);
|
||||||
|
if (!isInRoom) {
|
||||||
|
throw new Error('[[error:no-privileges]]');
|
||||||
|
}
|
||||||
|
socket.to(`chat_room_${data.roomId}`).emit('event:chats.typing', {
|
||||||
|
uid: socket.uid,
|
||||||
|
roomId: data.roomId,
|
||||||
|
typing: data.typing,
|
||||||
|
username: validator.escape(String(data.username)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
require('../promisify')(SocketModules);
|
require('../promisify')(SocketModules);
|
||||||
|
|||||||
Reference in New Issue
Block a user