refactored chat into separate chat_view and chat_message_list
This commit is contained in:
parent
cab0e9a881
commit
55917125a8
12 changed files with 127 additions and 96 deletions
|
|
@ -294,7 +294,7 @@ export default (store) => {
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
path: '/users/:username/chats/:recipient_id',
|
path: '/users/:username/chats/:recipient_id',
|
||||||
component: defineAsyncComponent(
|
component: defineAsyncComponent(
|
||||||
() => import('src/components/chat/chat.vue'),
|
() => import('src/components/chat_view/chat_view.vue'),
|
||||||
),
|
),
|
||||||
meta: { dontScroll: false },
|
meta: { dontScroll: false },
|
||||||
beforeEnter: validateAuthenticatedRoute,
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,9 @@ library.add(faTimes, faEllipsisH)
|
||||||
const ChatMessage = {
|
const ChatMessage = {
|
||||||
name: 'ChatMessage',
|
name: 'ChatMessage',
|
||||||
props: [
|
props: [
|
||||||
'author',
|
|
||||||
'edited',
|
'edited',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'chatViewItem',
|
'chatItem',
|
||||||
'hoveredMessageChain',
|
'hoveredMessageChain',
|
||||||
],
|
],
|
||||||
emits: ['hover'],
|
emits: ['hover'],
|
||||||
|
|
@ -42,21 +41,26 @@ const ChatMessage = {
|
||||||
computed: {
|
computed: {
|
||||||
// Returns HH:MM (hours and minutes) in local time.
|
// Returns HH:MM (hours and minutes) in local time.
|
||||||
createdAt() {
|
createdAt() {
|
||||||
const time = this.chatViewItem.data.created_at
|
const time = this.chatItem.data.created_at
|
||||||
return time.toLocaleTimeString('en', {
|
return time.toLocaleTimeString('en', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: false,
|
hour12: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
author() {
|
||||||
|
return this.$store.getters.findUser(
|
||||||
|
this.chatItem.data.account_id
|
||||||
|
)
|
||||||
|
},
|
||||||
isCurrentUser() {
|
isCurrentUser() {
|
||||||
return this.message.account_id === this.currentUser.id
|
return this.message.account_id === this.currentUser.id
|
||||||
},
|
},
|
||||||
message() {
|
message() {
|
||||||
return this.chatViewItem.data
|
return this.chatItem.data
|
||||||
},
|
},
|
||||||
isMessage() {
|
isMessage() {
|
||||||
return this.chatViewItem.type === 'message'
|
return this.chatItem.type === 'message'
|
||||||
},
|
},
|
||||||
messageForStatusContent() {
|
messageForStatusContent() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -96,15 +100,15 @@ const ChatMessage = {
|
||||||
onHover(bool) {
|
onHover(bool) {
|
||||||
this.$emit('hover', {
|
this.$emit('hover', {
|
||||||
isHovered: bool,
|
isHovered: bool,
|
||||||
messageChainId: this.chatViewItem.messageChainId,
|
messageChainId: this.chatItem.messageChainId,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async deleteMessage() {
|
async deleteMessage() {
|
||||||
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
|
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await this.$store.dispatch('deleteChatMessage', {
|
await this.$store.dispatch('deleteChatMessage', {
|
||||||
messageId: this.chatViewItem.data.id,
|
messageId: this.chatItem.data.id,
|
||||||
chatId: this.chatViewItem.data.chat_id,
|
chatId: this.chatItem.data.chat_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.hovered = false
|
this.hovered = false
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
class="avatar-wrapper"
|
class="avatar-wrapper"
|
||||||
>
|
>
|
||||||
<UserPopover
|
<UserPopover
|
||||||
v-if="chatViewItem.isHead"
|
v-if="chatItem.isHead"
|
||||||
:user-id="author.id"
|
:user-id="author.id"
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="media status"
|
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;"
|
style="position: relative;"
|
||||||
@mouseenter="hovered = true"
|
@mouseenter="hovered = true"
|
||||||
@mouseleave="hovered = false"
|
@mouseleave="hovered = false"
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
v-else
|
v-else
|
||||||
class="chat-message-date-separator"
|
class="chat-message-date-separator"
|
||||||
>
|
>
|
||||||
<ChatMessageDate :date="chatViewItem.date" />
|
<ChatMessageDate :date="chatItem.date" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
76
src/components/chat_message_list/chat_message_list.js
Normal file
76
src/components/chat_message_list/chat_message_list.js
Normal 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
|
||||||
7
src/components/chat_message_list/chat_message_list.scss
Normal file
7
src/components/chat_message_list/chat_message_list.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.ChatMessageList {
|
||||||
|
padding: 0 0.8em;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
14
src/components/chat_message_list/chat_message_list.vue
Normal file
14
src/components/chat_message_list/chat_message_list.vue
Normal 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" />
|
||||||
|
|
@ -2,8 +2,8 @@ import { throttle } from 'lodash'
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
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 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 PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
import chatService from '../../services/chat_service/chat_service.js'
|
import chatService from '../../services/chat_service/chat_service.js'
|
||||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatMessages,
|
chatMessages,
|
||||||
|
|
@ -38,10 +39,13 @@ const MAX_RETRIES = 10
|
||||||
|
|
||||||
const Chat = {
|
const Chat = {
|
||||||
components: {
|
components: {
|
||||||
ChatMessage,
|
ChatMessageList,
|
||||||
ChatTitle,
|
ChatTitle,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
messages: Array,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jumpToBottomButtonVisible: false,
|
jumpToBottomButtonVisible: false,
|
||||||
|
|
@ -97,12 +101,11 @@ const Chat = {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chatViewItems() {
|
chatMessages() {
|
||||||
return chatService.getView(this.currentChatMessageService?.messages)
|
return this.currentChatMessageService?.messages
|
||||||
},
|
},
|
||||||
newMessageCount() {
|
newMessageCount() {
|
||||||
return this.currentChatMessageService?.newMessageCount
|
return this.currentChatMessageService?.newMessageCount
|
||||||
|
|
||||||
},
|
},
|
||||||
streamingEnabled() {
|
streamingEnabled() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -114,18 +117,18 @@ const Chat = {
|
||||||
'currentChat',
|
'currentChat',
|
||||||
'currentChatMessageService',
|
'currentChatMessageService',
|
||||||
'findOpenedChatByRecipientId',
|
'findOpenedChatByRecipientId',
|
||||||
'mergedConfig',
|
|
||||||
]),
|
]),
|
||||||
...mapPiniaState(useInterfaceStore, {
|
...mapPiniaState(useInterfaceStore, {
|
||||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||||
}),
|
}),
|
||||||
|
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||||
currentUser: (state) => state.users.currentUser,
|
currentUser: (state) => state.users.currentUser,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
chatViewItems() {
|
chatMessages() {
|
||||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
// 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.
|
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
selector: '.chat-message-list',
|
selector: '.ChatMessageList',
|
||||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
|
validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
|
||||||
defaultRules: [
|
defaultRules: [
|
||||||
{
|
{
|
||||||
|
|
@ -25,29 +25,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<ChatMessageList :messages="chatMessages" />
|
||||||
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>
|
|
||||||
<div
|
<div
|
||||||
ref="footer"
|
ref="footer"
|
||||||
class="panel-body footer"
|
class="panel-body footer"
|
||||||
|
|
@ -95,5 +73,5 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./chat.js"></script>
|
<script src="./chat_view.js"></script>
|
||||||
<style src="./chat.scss" lang="scss" />
|
<style src="./chat_view.scss" lang="scss" />
|
||||||
|
|
@ -159,60 +159,9 @@ const resetNewMessageCount = (storage) => {
|
||||||
storage.lastSeenMessageId = storage.maxId
|
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 = {
|
const ChatService = {
|
||||||
add,
|
add,
|
||||||
empty,
|
empty,
|
||||||
getView,
|
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
cullOlderMessages,
|
cullOlderMessages,
|
||||||
resetNewMessageCount,
|
resetNewMessageCount,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue