mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +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-spam-be-gone": "2.1.1",
|
||||
"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-peace": "2.1.18",
|
||||
"nodebb-theme-persona": "13.2.25",
|
||||
"nodebb-theme-persona": "13.2.26",
|
||||
"nodebb-widget-essentials": "7.0.13",
|
||||
"nodemailer": "6.9.4",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
"chat.chat-with-usernames-and-x-others": "Chat with %1 & %2 others",
|
||||
"chat.send": "Send",
|
||||
"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.replying-to": "Replying to %1",
|
||||
"chat.see_all": "All chats",
|
||||
|
||||
@@ -18,11 +18,12 @@ define('forum/chats', [
|
||||
'chat',
|
||||
'api',
|
||||
'uploadHelpers',
|
||||
'translator',
|
||||
], function (
|
||||
components, mousetrap, recentChats, create,
|
||||
manage, messages, userList, messageSearch, pinnedMessages,
|
||||
autocomplete, hooks, bootbox, alerts, chatModule, api,
|
||||
uploadHelpers
|
||||
uploadHelpers, translator
|
||||
) {
|
||||
const Chats = {
|
||||
initialised: false,
|
||||
@@ -89,6 +90,7 @@ define('forum/chats', [
|
||||
Chats.addParentHandler(mainWrapper);
|
||||
Chats.addCharactersLeftHandler(mainWrapper);
|
||||
Chats.addTextareaResizeHandler(mainWrapper);
|
||||
Chats.addTypingHandler(mainWrapper, roomId);
|
||||
Chats.addIPHandler(mainWrapper);
|
||||
Chats.createAutoComplete(roomId, $('[component="chat/input"]'));
|
||||
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) {
|
||||
element.on('click', '[data-mid] [data-action]', function () {
|
||||
const msgEl = $(this).parents('[data-mid]');
|
||||
@@ -544,6 +563,7 @@ define('forum/chats', [
|
||||
const html = await app.parseAndTranslate('partials/chats/message-window', payload);
|
||||
const mainWrapper = components.get('chat/main-wrapper');
|
||||
mainWrapper.html(html);
|
||||
mainWrapper.attr('data-roomid', roomId);
|
||||
chatNavWrapper = $('[component="chat/nav-wrapper"]');
|
||||
html.find('.timeago').timeago();
|
||||
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) {
|
||||
|
||||
@@ -25,8 +25,8 @@ define('forum/header/chat', [
|
||||
socket.removeListener('event:chats.receive', onChatMessageReceived);
|
||||
socket.on('event:chats.receive', onChatMessageReceived);
|
||||
|
||||
socket.removeListener('event:user_status_change', onUserStatusChange);
|
||||
socket.on('event:user_status_change', onUserStatusChange);
|
||||
socket.removeListener('event:chats.typing', onUserTyping);
|
||||
socket.on('event:chats.typing', onUserTyping);
|
||||
|
||||
socket.removeListener('event:chats.roomRename', onRoomRename);
|
||||
socket.on('event:chats.roomRename', onRoomRename);
|
||||
@@ -63,8 +63,8 @@ define('forum/header/chat', [
|
||||
requireAndCall('onChatMessageReceived', data);
|
||||
}
|
||||
|
||||
function onUserStatusChange(data) {
|
||||
requireAndCall('onUserStatusChange', data);
|
||||
function onUserTyping(data) {
|
||||
requireAndCall('onUserTyping', 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) {
|
||||
const modal = module.getModal(data.roomId);
|
||||
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) {
|
||||
return $('#chat-modal-' + roomId);
|
||||
};
|
||||
@@ -370,6 +403,7 @@ define('chat', [
|
||||
Chats.addParentHandler(chatModal.find('[component="chat/message/content"]'));
|
||||
Chats.addCharactersLeftHandler(chatModal);
|
||||
Chats.addTextareaResizeHandler(chatModal);
|
||||
Chats.addTypingHandler(chatModal, roomId);
|
||||
Chats.addIPHandler(chatModal);
|
||||
Chats.addTooltipHandler(chatModal);
|
||||
Chats.addUploadHandler({
|
||||
|
||||
@@ -707,9 +707,7 @@ const utils = {
|
||||
const args = arguments;
|
||||
const later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
func.apply(context, args);
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
if (!timeout) {
|
||||
|
||||
@@ -97,6 +97,19 @@ function onConnection(socket) {
|
||||
}, 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', () => {
|
||||
onDisconnect(socket);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const validator = require('validator');
|
||||
|
||||
const db = require('../database');
|
||||
const Messaging = require('../messaging');
|
||||
@@ -264,5 +265,21 @@ SocketModules.chats.loadPinnedMessages = async (socket, data) => {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user