From 13bf64c95675d770c9b9ae853591bf7e10f431a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 20 Jan 2026 20:14:40 -0500 Subject: [PATCH] fix: closes #12458, on socket.io reconnect load messages after last data-index --- public/src/client/chats.js | 44 ++------ public/src/client/chats/events.js | 113 ++++++++++++++++++++ public/src/client/chats/messages.js | 113 ++++++++++++++------ src/views/chat.tpl | 2 +- src/views/partials/chats/message-window.tpl | 2 +- 5 files changed, 203 insertions(+), 71 deletions(-) create mode 100644 public/src/client/chats/events.js diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 4181bd79cb..316a6a4007 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -297,7 +297,7 @@ define('forum/chats', [ let loading = false; let previousScrollTop = el.scrollTop(); let currentScrollTop = previousScrollTop; - el.off('scroll').on('scroll', utils.debounce(function () { + el.off('scroll').on('scroll', utils.debounce(async function () { if (parseInt(el.attr('data-ignore-next-scroll'), 10) === 1) { el.removeAttr('data-ignore-next-scroll'); previousScrollTop = el.scrollTop(); @@ -323,41 +323,13 @@ define('forum/chats', [ if ((scrollPercent < top && direction === -1) || (scrollPercent > bottom && direction === 1)) { loading = true; - const msgEls = el.children('[data-mid]').not('.new'); - const afterEl = direction > 0 ? msgEls.last() : msgEls.first(); - const start = parseInt(afterEl.attr('data-index'), 10) || 0; - - api.get(`/chats/${roomId}/messages`, { uid, start, direction }).then((data) => { - let messageData = data.messages; - if (!messageData) { - loading = false; - return; - } - messageData = messageData.filter(function (chatMsg) { - const msgOnDom = el.find('[component="chat/message"][data-mid="' + chatMsg.messageId + '"]'); - msgOnDom.removeClass('new'); - return !msgOnDom.length; - }); - if (!messageData.length) { - loading = false; - return; - } - messages.parseMessage(messageData, function (html) { - el.attr('data-ignore-next-scroll', 1); - if (direction > 0) { - html.insertAfter(afterEl); - messages.onMessagesAddedToDom(html); - } else { - const currentScrollTop = el.scrollTop(); - const previousHeight = el[0].scrollHeight; - el.prepend(html); - messages.onMessagesAddedToDom(html); - el.scrollTop((el[0].scrollHeight - previousHeight) + currentScrollTop); - } - - loading = false; - }); - }).catch(alerts.error); + try { + await messages.loadMoreMessages(el, uid, roomId, direction); + } catch (err) { + alerts.error(err); + } finally { + loading = false; + } } }, 100)); }; diff --git a/public/src/client/chats/events.js b/public/src/client/chats/events.js new file mode 100644 index 0000000000..119ba0106d --- /dev/null +++ b/public/src/client/chats/events.js @@ -0,0 +1,113 @@ + +'use strict'; + +define('forum/chats/events', [ + 'forum/chats/messages', + 'chat', + 'components', +], function (messages, chatModule, components) { + const Events = {}; + + const events = { + 'event:chats.receive': chatsReceive, + 'event:chats.public.unread': publicChatUnread, + 'event:user_status_change': onUserStatusChange, + 'event:chats.roomRename': onRoomRename, + 'event:chats.mark': markChatState, + 'event:chats.typing': onChatTyping, + }; + let chatNavWrapper = null; + let Chats = null; + Events.init = async function () { + Chats = await require('forum/chats'); + chatNavWrapper = $('[component="chat/nav-wrapper"]'); + Events.removeListeners(); + for (const [eventName, handler] of Object.entries(events)) { + socket.on(eventName, handler); + } + }; + + Events.removeListeners = function () { + for (const [eventName, handler] of Object.entries(events)) { + socket.removeListener(eventName, handler); + } + }; + + function chatsReceive(data) { + if (chatModule.isFromBlockedUser(data.fromUid)) { + return; + } + if (parseInt(data.roomId, 10) === parseInt(ajaxify.data.roomId, 10)) { + data.self = parseInt(app.user.uid, 10) === parseInt(data.fromUid, 10) ? 1 : 0; + if (!Chats.newMessage) { + Chats.newMessage = data.self === 0; + } + data.message.self = data.self; + data.message.timestamp = Math.min(Date.now(), data.message.timestamp); + data.message.timestampISO = utils.toISOString(data.message.timestamp); + messages.appendChatMessage($('[component="chat/message/content"]'), data.message); + + Chats.updateTeaser(data.roomId, { + content: utils.stripHTMLTags(utils.decodeHTMLEntities(data.message.content)), + user: data.message.fromUser, + timestampISO: data.message.timestampISO, + }); + } + } + + function publicChatUnread(data) { + if ( + !ajaxify.data.template.chats || + chatModule.isFromBlockedUser(data.fromUid) || + chatModule.isLookingAtRoom(data.roomId) || + app.user.uid === parseInt(data.fromUid, 10) + ) { + return; + } + Chats.markChatPageElUnread(data); + Chats.increasePublicRoomUnreadCount(chatNavWrapper.find('[data-roomid=' + data.roomId + ']')); + } + + function onUserStatusChange(data) { + app.updateUserStatus( + $(`.chats-list [data-uid="${data.uid}"] [component="user/status"]`), data.status + ); + } + + function onRoomRename(data) { + const roomEl = components.get('chat/recent/room', data.roomId); + if (roomEl.length) { + const titleEl = roomEl.find('[component="chat/room/title"]'); + ajaxify.data.roomName = data.newName; + titleEl.translateText(data.newName ? data.newName : ajaxify.data.usernames); + } + const titleEl = $(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"] [component="chat/header/title"]`); + if (titleEl.length) { + titleEl.html( + data.newName ? + ` ${data.newName}` : + ajaxify.data.chatWithMessage + ); + } + } + + function markChatState({ roomId, state }) { + const roomEls = $(`[component="chat/recent"] [data-roomid="${roomId}"], [component="chat/list"] [data-roomid="${roomId}"], [component="chat/public"] [data-roomid="${roomId}"]`); + roomEls.each((idx, el) => { + const roomEl = $(el); + chatModule.markChatElUnread(roomEl, state === 1); + if (state === 0) { + Chats.updatePublicRoomUnreadCount(roomEl, 0); + } + }); + } + + function onChatTyping(data) { + if (data.uid === app.user.uid || chatModule.isFromBlockedUser(data.uid)) { + return; + } + chatModule.updateTypingUserList($(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"]`), data); + } + + return Events; +}); diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index d351a263f9..062e19ef2f 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -79,7 +79,7 @@ define('forum/chats/messages', [ }); } - messages.appendChatMessage = function (chatContentEl, data) { + messages.appendChatMessage = async function (chatContentEl, data) { const lastMsgEl = chatContentEl.find('.chat-message').last(); const lastSpeaker = parseInt(lastMsgEl.attr('data-uid'), 10); const lasttimestamp = parseInt(lastMsgEl.attr('data-timestamp'), 10); @@ -89,9 +89,8 @@ define('forum/chats/messages', [ data.index = parseInt(lastMsgEl.attr('data-index'), 10) + 1; } - messages.parseMessage(data, function (html) { - onMessagesParsed(chatContentEl, html, data); - }); + const html = await messages.parseMessage(data); + onMessagesParsed(chatContentEl, html, data); }; function onMessagesParsed(chatContentEl, html, msgData) { @@ -125,16 +124,15 @@ define('forum/chats/messages', [ hooks.fire('action:chat.onMessagesAddedToDom', { messageEls }); }; - messages.parseMessage = function (data, callback) { + messages.parseMessage = function (data) { const tplData = { messages: data, isAdminOrGlobalMod: app.user.isAdmin || app.user.isGlobalMod, }; if (Array.isArray(data)) { - app.parseAndTranslate('partials/chats/messages', tplData).then(callback); - } else { - app.parseAndTranslate('partials/chats/' + (data.system ? 'system-message' : 'message'), tplData).then(callback); + return app.parseAndTranslate('partials/chats/messages', tplData); } + return app.parseAndTranslate('partials/chats/' + (data.system ? 'system-message' : 'message'), tplData); }; messages.isAtBottom = function (containerEl, threshold) { @@ -273,35 +271,37 @@ define('forum/chats/messages', [ socket.removeListener('event:chats.restore', onChatMessageRestored); socket.on('event:chats.restore', onChatMessageRestored); + + socket.removeListener('connect', onChatReconnect); + socket.on('connect', onChatReconnect); }; - function onChatMessageEdited(data) { - data.messages.forEach(function (message) { + async function onChatMessageEdited(data) { + await Promise.all(data.messages.map(async (message) => { const self = parseInt(message.fromuid, 10) === parseInt(app.user.uid, 10); message.self = self ? 1 : 0; - messages.parseMessage(message, function (html) { - const msgEl = components.get('chat/message', message.mid); - if (msgEl.length) { - const componentsToReplace = [ - '[component="chat/message/body"]', - '[component="chat/message/edited"]', - ]; - componentsToReplace.forEach((cmp) => { - msgEl.find(cmp).replaceWith(html.find(cmp)); - }); - messages.onMessagesAddedToDom(components.get('chat/message', message.mid)); - } - const parentEl = $(`[component="chat/message/parent"][data-parent-mid="${message.mid}"]`); - if (parentEl.length) { - parentEl.find('[component="chat/message/parent/content"]').html( - html.find('[component="chat/message/body"]').html() - ); - messages.onMessagesAddedToDom( - $(`[component="chat/message/parent"][data-parent-mid="${message.mid}"]`) - ); - } - }); - }); + const html = await messages.parseMessage(message); + const msgEl = components.get('chat/message', message.mid); + if (msgEl.length) { + const componentsToReplace = [ + '[component="chat/message/body"]', + '[component="chat/message/edited"]', + ]; + componentsToReplace.forEach((cmp) => { + msgEl.find(cmp).replaceWith(html.find(cmp)); + }); + messages.onMessagesAddedToDom(components.get('chat/message', message.mid)); + } + const parentEl = $(`[component="chat/message/parent"][data-parent-mid="${message.mid}"]`); + if (parentEl.length) { + parentEl.find('[component="chat/message/parent/content"]').html( + html.find('[component="chat/message/body"]').html() + ); + messages.onMessagesAddedToDom( + $(`[component="chat/message/parent"][data-parent-mid="${message.mid}"]`) + ); + } + })); } function onChatMessageDeleted(messageId) { @@ -341,6 +341,53 @@ define('forum/chats/messages', [ } } + function onChatReconnect() { + $('[component="chat/message/content"]').each(function () { + const chatContentEl = $(this); + const roomId = chatContentEl.attr('data-roomid'); + const uid = (ajaxify.template.chats && ajaxify.data.uid) || app.user.uid; + const isAtBottom = messages.isAtBottom(chatContentEl); + messages.loadMoreMessages(chatContentEl, uid, roomId, 1) + .then(() => { + if (isAtBottom) { + messages.scrollToBottomAfterImageLoad(chatContentEl); + } + }).catch(alerts.error); + }); + } + + messages.loadMoreMessages = async function (chatContentEl, uid, roomId, direction) { + const msgEls = chatContentEl.children('[data-mid]').not('.new'); + const afterEl = direction > 0 ? msgEls.last() : msgEls.first(); + const start = parseInt(afterEl.attr('data-index'), 10) || 0; + + const data = await api.get(`/chats/${roomId}/messages`, { uid, start, direction }); + let messageData = data.messages; + if (!messageData) { + return; + } + messageData = messageData.filter(function (chatMsg) { + const msgOnDom = chatContentEl.find(`[component="chat/message"][data-mid="${chatMsg.messageId}"]`); + msgOnDom.removeClass('new'); + return !msgOnDom.length; + }); + if (!messageData.length) { + return; + } + const html = await messages.parseMessage(messageData); + chatContentEl.attr('data-ignore-next-scroll', 1); + if (direction > 0) { + html.insertAfter(afterEl); + messages.onMessagesAddedToDom(html); + } else { + const currentScrollTop = chatContentEl.scrollTop(); + const previousHeight = chatContentEl[0].scrollHeight; + chatContentEl.prepend(html); + messages.onMessagesAddedToDom(html); + chatContentEl.scrollTop((chatContentEl[0].scrollHeight - previousHeight) + currentScrollTop); + } + }; + messages.delete = function (messageId, roomId) { bootbox.confirm('[[modules:chat.delete-message-confirm]]', function (ok) { if (!ok) { diff --git a/src/views/chat.tpl b/src/views/chat.tpl index f51ba8fa86..12dc87e1b7 100644 --- a/src/views/chat.tpl +++ b/src/views/chat.tpl @@ -18,7 +18,7 @@
-
    +