refactored chat into separate chat_view and chat_message_list

This commit is contained in:
Henry Jameson 2026-07-07 20:44:13 +03:00
commit 55917125a8
12 changed files with 127 additions and 96 deletions

View file

@ -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,

View file

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

View file

@ -15,7 +15,7 @@
class="avatar-wrapper"
>
<UserPopover
v-if="chatViewItem.isHead"
v-if="chatItem.isHead"
:user-id="author.id"
>
<UserAvatar
@ -31,7 +31,7 @@
>
<div
class="media status"
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
:class="{ 'without-attachment': !hasAttachment, 'pending': chatItem.data.pending, 'error': chatItem.data.error }"
style="position: relative;"
@mouseenter="hovered = true"
@mouseleave="hovered = false"
@ -93,7 +93,7 @@
v-else
class="chat-message-date-separator"
>
<ChatMessageDate :date="chatViewItem.date" />
<ChatMessageDate :date="chatItem.date" />
</div>
</template>

View file

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

View file

@ -0,0 +1,7 @@
.ChatMessageList {
padding: 0 0.8em;
height: 100%;
display: flex;
flex-direction: column;
justify-content: end;
}

View file

@ -0,0 +1,14 @@
<template>
<div class="ChatMessageList">
<ChatMessage
v-for="chatItem in chatItems"
:key="chatItem.id"
:chat-item="chatItem"
:hovered-message-chain="chatItem.messageChainId === hoveredMessageChainId"
@hover="onMessageHover"
/>
</div>
</template>
<script src="./chat_message_list.js"></script>
<style src="./chat_message_list.scss" lang="scss" />

View file

@ -2,8 +2,8 @@ import { throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
import { mapGetters, mapState } from 'vuex'
import ChatMessage from 'src/components/chat_message/chat_message.vue'
import ChatTitle from 'src/components/chat_title/chat_title.vue'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import chatService from '../../services/chat_service/chat_service.js'
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
@ -17,6 +17,7 @@ import {
import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import {
chatMessages,
@ -38,10 +39,13 @@ const MAX_RETRIES = 10
const Chat = {
components: {
ChatMessage,
ChatMessageList,
ChatTitle,
PostStatusForm,
},
props: {
messages: Array,
},
data() {
return {
jumpToBottomButtonVisible: false,
@ -97,12 +101,11 @@ const Chat = {
return ''
}
},
chatViewItems() {
return chatService.getView(this.currentChatMessageService?.messages)
chatMessages() {
return this.currentChatMessageService?.messages
},
newMessageCount() {
return this.currentChatMessageService?.newMessageCount
},
streamingEnabled() {
return (
@ -114,18 +117,18 @@ const Chat = {
'currentChat',
'currentChatMessageService',
'findOpenedChatByRecipientId',
'mergedConfig',
]),
...mapPiniaState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
}),
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
...mapState({
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
currentUser: (state) => state.users.currentUser,
}),
},
watch: {
chatViewItems() {
chatMessages() {
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)

View file

@ -1,6 +1,6 @@
export default {
name: 'Chat',
selector: '.chat-message-list',
selector: '.ChatMessageList',
validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
defaultRules: [
{

View file

@ -25,29 +25,7 @@
/>
</div>
</div>
<div
class="chat-message-list message-list"
:style="{ height: scrollableContainerHeight }"
>
<template v-if="!errorLoadingChat">
<ChatMessage
v-for="chatViewItem in chatViewItems"
:key="chatViewItem.id"
:author="recipient"
:chat-view-item="chatViewItem"
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
@hover="onMessageHover"
/>
</template>
<div
v-else
class="chat-loading-error"
>
<div class="alert error">
{{ $t('chats.error_loading_chat') }}
</div>
</div>
</div>
<ChatMessageList :messages="chatMessages" />
<div
ref="footer"
class="panel-body footer"
@ -95,5 +73,5 @@
</div>
</template>
<script src="./chat.js"></script>
<style src="./chat.scss" lang="scss" />
<script src="./chat_view.js"></script>
<style src="./chat_view.scss" lang="scss" />

View file

@ -159,60 +159,9 @@ const resetNewMessageCount = (storage) => {
storage.lastSeenMessageId = storage.maxId
}
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
const getView = (items = []) => {
const messages = orderBy(items, ['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()
}
const ChatService = {
add,
empty,
getView,
deleteMessage,
cullOlderMessages,
resetNewMessageCount,