feat: typing user list in chat

isekai-main
Barış Soner Uşaklı 1 year ago
parent 7e4b4d3a33
commit 600357444d

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

Loading…
Cancel
Save