From cab0e9a8819a13835560f889ce31295c4fa466a6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 7 Jul 2026 20:07:17 +0300 Subject: [PATCH 01/30] a bit more functional way to group/separate chat list --- src/components/chat/chat.js | 8 +- src/services/chat_service/chat_service.js | 96 ++++++++--------------- 2 files changed, 37 insertions(+), 67 deletions(-) diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 16a03ab1d..22b138036 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -98,13 +98,11 @@ const Chat = { } }, chatViewItems() { - return chatService.getView(this.currentChatMessageService) + return chatService.getView(this.currentChatMessageService?.messages) }, newMessageCount() { - return ( - this.currentChatMessageService && - this.currentChatMessageService.newMessageCount - ) + return this.currentChatMessageService?.newMessageCount + }, streamingEnabled() { return ( diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index eec267dde..bf95da197 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -160,81 +160,53 @@ const resetNewMessageCount = (storage) => { } // Inserts date separators and marks the head and tail if it's the chain of messages made by the same user -const getView = (storage) => { - if (!storage) { - return [] - } - - const result = [] - const messages = orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc']) - const firstMessage = messages[0] - let previousMessage = messages[messages.length - 1] - let currentMessageChainId - - if (firstMessage) { - const date = new Date(firstMessage.created_at) - date.setHours(0, 0, 0, 0) - result.push({ - type: 'date', - date, - id: date.getTime().toString(), - }) - } - - let afterDate = false - - for (let i = 0; i < messages.length; i++) { - const message = messages[i] - const nextMessage = messages[i + 1] - +const getView = (items = []) => { + const messages = orderBy(items, ['pending', 'id'], ['asc', 'asc']) + return messages.reduceRight((acc, message, index) => { const date = new Date(message.created_at) - date.setHours(0, 0, 0, 0) - // insert date separator and start a new message chain - if (previousMessage && previousMessage.date < date) { - result.push({ - type: 'date', - date, - id: date.getTime().toString(), - }) + const olderMessage = messages[index - 1] + const newerMessage = messages[index + 1] + const newerItem = acc[acc.length - 1] - previousMessage.isTail = true - currentMessageChainId = undefined - afterDate = true - } + const diff = message.created_at - (olderMessage?.created_at || 0) + const MAX_DIFF = 1000 * 60 // 5 minutes - const object = { + const chatItem = { type: 'message', data: message, date, id: message.id, - messageChainId: currentMessageChainId, + isTail: true, + isHead: true, } - // end a message chian - if ((nextMessage && nextMessage.account_id) !== message.account_id) { - object.isTail = true - currentMessageChainId = undefined + if (newerItem == null) { + chatItem.messageChainId = uniqueId() + } else { + if (newerItem.type === 'date') { + chatItem.messageChainId = uniqueId() + } else if (newerItem.type === 'message') { + if (newerItem.data.account_id !== message.account_id) { + chatItem.messageChainId = uniqueId() + } else { + chatItem.messageChainId = newerItem.messageChainId + chatItem.isTail = false + newerItem.isHead = false + } + } } - // start a new message chain - if ( - (previousMessage && - previousMessage.data && - previousMessage.data.account_id) !== message.account_id || - afterDate - ) { - currentMessageChainId = uniqueId() - object.isHead = true - object.messageChainId = currentMessageChainId + if (diff > MAX_DIFF || !olderMessage) { + return [...acc, chatItem, { + type: 'date', + date, + id: date.getTime().toString(), + }] + } else { + return [...acc, chatItem] } - - result.push(object) - previousMessage = object - afterDate = false - } - - return result + }, []).reverse() } const ChatService = { From 55917125a89e4647fd942ed735015f4f8c9a32ed Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 7 Jul 2026 20:44:13 +0300 Subject: [PATCH 02/30] refactored chat into separate chat_view and chat_message_list --- src/boot/routes.js | 2 +- src/components/chat_message/chat_message.js | 20 +++-- src/components/chat_message/chat_message.vue | 6 +- .../chat_message_list/chat_message_list.js | 76 +++++++++++++++++++ .../chat_message_list/chat_message_list.scss | 7 ++ .../chat_message_list/chat_message_list.vue | 14 ++++ .../{chat => chat_view}/chat_layout_utils.js | 0 .../{chat/chat.js => chat_view/chat_view.js} | 17 +++-- .../chat.scss => chat_view/chat_view.scss} | 0 .../chat_view.style.js} | 2 +- .../chat.vue => chat_view/chat_view.vue} | 28 +------ src/services/chat_service/chat_service.js | 51 ------------- 12 files changed, 127 insertions(+), 96 deletions(-) create mode 100644 src/components/chat_message_list/chat_message_list.js create mode 100644 src/components/chat_message_list/chat_message_list.scss create mode 100644 src/components/chat_message_list/chat_message_list.vue rename src/components/{chat => chat_view}/chat_layout_utils.js (100%) rename src/components/{chat/chat.js => chat_view/chat_view.js} (97%) rename src/components/{chat/chat.scss => chat_view/chat_view.scss} (100%) rename src/components/{chat/chat.style.js => chat_view/chat_view.style.js} (86%) rename src/components/{chat/chat.vue => chat_view/chat_view.vue} (71%) diff --git a/src/boot/routes.js b/src/boot/routes.js index 9b00a8004..4f21246e5 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -294,7 +294,7 @@ export default (store) => { name: 'chat', path: '/users/:username/chats/:recipient_id', component: defineAsyncComponent( - () => import('src/components/chat/chat.vue'), + () => import('src/components/chat_view/chat_view.vue'), ), meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute, diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index 2066a8194..e5a798fef 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -22,10 +22,9 @@ library.add(faTimes, faEllipsisH) const ChatMessage = { name: 'ChatMessage', props: [ - 'author', 'edited', 'noHeading', - 'chatViewItem', + 'chatItem', 'hoveredMessageChain', ], emits: ['hover'], @@ -42,21 +41,26 @@ const ChatMessage = { computed: { // Returns HH:MM (hours and minutes) in local time. createdAt() { - const time = this.chatViewItem.data.created_at + const time = this.chatItem.data.created_at return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false, }) }, + author() { + return this.$store.getters.findUser( + this.chatItem.data.account_id + ) + }, isCurrentUser() { return this.message.account_id === this.currentUser.id }, message() { - return this.chatViewItem.data + return this.chatItem.data }, isMessage() { - return this.chatViewItem.type === 'message' + return this.chatItem.type === 'message' }, messageForStatusContent() { return { @@ -96,15 +100,15 @@ const ChatMessage = { onHover(bool) { this.$emit('hover', { isHovered: bool, - messageChainId: this.chatViewItem.messageChainId, + messageChainId: this.chatItem.messageChainId, }) }, async deleteMessage() { const confirmed = window.confirm(this.$t('chats.delete_confirm')) if (confirmed) { await this.$store.dispatch('deleteChatMessage', { - messageId: this.chatViewItem.data.id, - chatId: this.chatViewItem.data.chat_id, + messageId: this.chatItem.data.id, + chatId: this.chatItem.data.chat_id, }) } this.hovered = false diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 22e2e8eb8..ee3cbdc2c 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -15,7 +15,7 @@ class="avatar-wrapper" >
- +
diff --git a/src/components/chat_message_list/chat_message_list.js b/src/components/chat_message_list/chat_message_list.js new file mode 100644 index 000000000..ffbedf7c0 --- /dev/null +++ b/src/components/chat_message_list/chat_message_list.js @@ -0,0 +1,76 @@ +import { throttle, orderBy, uniqueId } from 'lodash' +import { mapState as mapPiniaState } from 'pinia' +import { mapGetters, mapState } from 'vuex' + +import ChatMessage from 'src/components/chat_message/chat_message.vue' + +const ChatMessageList = { + components: { + ChatMessage, + }, + props: { + messages: Array, + }, + data() { + return { + hoveredMessageChainId: undefined, + } + }, + computed: { + chatItems() { + const messages = orderBy(this.messages, ['pending', 'id'], ['asc', 'asc']) + return messages.reduceRight((acc, message, index) => { + const date = new Date(message.created_at) + + const olderMessage = messages[index - 1] + const newerMessage = messages[index + 1] + const newerItem = acc[acc.length - 1] + + const diff = message.created_at - (olderMessage?.created_at || 0) + const MAX_DIFF = 1000 * 60 // 5 minutes + + const chatItem = { + type: 'message', + data: message, + date, + id: message.id, + isTail: true, + isHead: true, + } + + if (newerItem == null) { + chatItem.messageChainId = uniqueId() + } else { + if (newerItem.type === 'date') { + chatItem.messageChainId = uniqueId() + } else if (newerItem.type === 'message') { + if (newerItem.data.account_id !== message.account_id) { + chatItem.messageChainId = uniqueId() + } else { + chatItem.messageChainId = newerItem.messageChainId + chatItem.isTail = false + newerItem.isHead = false + } + } + } + + if (diff > MAX_DIFF || !olderMessage) { + return [...acc, chatItem, { + type: 'date', + date, + id: date.getTime().toString(), + }] + } else { + return [...acc, chatItem] + } + }, []).reverse() + } + }, + methods: { + onMessageHover({ isHovered, messageChainId }) { + this.hoveredMessageChainId = isHovered ? messageChainId : undefined + }, + } +} + +export default ChatMessageList diff --git a/src/components/chat_message_list/chat_message_list.scss b/src/components/chat_message_list/chat_message_list.scss new file mode 100644 index 000000000..aa2313f8c --- /dev/null +++ b/src/components/chat_message_list/chat_message_list.scss @@ -0,0 +1,7 @@ +.ChatMessageList { + padding: 0 0.8em; + height: 100%; + display: flex; + flex-direction: column; + justify-content: end; +} diff --git a/src/components/chat_message_list/chat_message_list.vue b/src/components/chat_message_list/chat_message_list.vue new file mode 100644 index 000000000..b1fcd6dd6 --- /dev/null +++ b/src/components/chat_message_list/chat_message_list.vue @@ -0,0 +1,14 @@ + + + + +