refactored chat handling into chat view component
This commit is contained in:
parent
a58cdd954d
commit
26a2b4fca1
13 changed files with 315 additions and 455 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { paramsString, promisedRequest } from './helpers.js'
|
import { paramsString, promisedRequest } from './helpers.js'
|
||||||
|
|
||||||
import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
import { parseChat, parseChatMessage } from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
|
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
|
||||||
const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}`
|
const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}`
|
||||||
|
|
@ -15,7 +15,7 @@ export const chats = ({ credentials }) =>
|
||||||
url: PLEROMA_CHATS_URL,
|
url: PLEROMA_CHATS_URL,
|
||||||
credentials,
|
credentials,
|
||||||
}).then(({ data }) => ({
|
}).then(({ data }) => ({
|
||||||
chatList: data.map(parseChat).filter((c) => c),
|
data: data.map(parseChat).filter((c) => c),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const getOrCreateChat = ({ accountId, credentials }) =>
|
export const getOrCreateChat = ({ accountId, credentials }) =>
|
||||||
|
|
@ -23,7 +23,7 @@ export const getOrCreateChat = ({ accountId, credentials }) =>
|
||||||
url: PLEROMA_CHAT_URL(accountId),
|
url: PLEROMA_CHAT_URL(accountId),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials,
|
credentials,
|
||||||
})
|
}).then(({ data }) => ({ data: parseChat(data) }))
|
||||||
|
|
||||||
export const chatMessages = ({
|
export const chatMessages = ({
|
||||||
id,
|
id,
|
||||||
|
|
@ -36,7 +36,9 @@ export const chatMessages = ({
|
||||||
url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }),
|
url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }),
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials,
|
credentials,
|
||||||
})
|
}).then(({ data }) => ({
|
||||||
|
data: data.map(parseChatMessage).filter((c) => c),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendChatMessage = ({
|
export const sendChatMessage = ({
|
||||||
|
|
@ -66,7 +68,9 @@ export const sendChatMessage = ({
|
||||||
payload,
|
payload,
|
||||||
credentials,
|
credentials,
|
||||||
headers,
|
headers,
|
||||||
})
|
}).then(({ data }) => ({
|
||||||
|
data: parseChatMessage(data),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const readChat = ({ id, lastReadId, credentials }) =>
|
export const readChat = ({ id, lastReadId, credentials }) =>
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,13 @@ import { useInterfaceStore } from 'src/stores/interface'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
|
import { faEllipsisH, faTimes, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faTimes, faEllipsisH)
|
library.add(faTimes, faEllipsisH, faCircleNotch)
|
||||||
|
|
||||||
const ChatMessage = {
|
const ChatMessage = {
|
||||||
name: 'ChatMessage',
|
name: 'ChatMessage',
|
||||||
props: [
|
props: ['edited', 'noHeading', 'chatItem', 'hoveredMessageChain'],
|
||||||
'edited',
|
|
||||||
'noHeading',
|
|
||||||
'chatItem',
|
|
||||||
'hoveredMessageChain',
|
|
||||||
],
|
|
||||||
emits: ['hover'],
|
emits: ['hover'],
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outgoing {
|
.-outgoing {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
place-content: end flex-end;
|
place-content: end flex-end;
|
||||||
|
|
@ -118,12 +118,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.incoming {
|
.-incoming {
|
||||||
.chat-message-menu {
|
.chat-message-menu {
|
||||||
left: 0.4rem;
|
left: 0.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.-pending {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-message-inner.with-media {
|
.chat-message-inner.with-media {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="chat-message"
|
class="chat-message"
|
||||||
:class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]"
|
:class="[{ '-outgoing': isCurrentUser, '-incoming': !isCurrentUser, '-pending': message.pending }]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isCurrentUser"
|
v-if="!isCurrentUser"
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
class="message-content"
|
class="message-content"
|
||||||
|
:class="{ faint: message.pending }"
|
||||||
:status="messageForStatusContent"
|
:status="messageForStatusContent"
|
||||||
:full-content="true"
|
:full-content="true"
|
||||||
>
|
>
|
||||||
|
|
@ -80,6 +81,16 @@
|
||||||
<span
|
<span
|
||||||
class="created-at"
|
class="created-at"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="message.pending"
|
||||||
|
class="loading-spinner"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-old-padding"
|
||||||
|
spin
|
||||||
|
icon="circle-notch"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
{{ createdAt }}
|
{{ createdAt }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import localeService from 'src/services/locale/locale.service.js'
|
|
||||||
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
|
import localeService from 'src/services/locale/locale.service.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Timeago',
|
name: 'Timeago',
|
||||||
props: ['date', 'showTime'],
|
props: ['date', 'showTime'],
|
||||||
|
|
@ -26,7 +26,7 @@ export default {
|
||||||
if (this.showTime) {
|
if (this.showTime) {
|
||||||
return this.date.toLocaleTimeString(
|
return this.date.toLocaleTimeString(
|
||||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||||
{ hour12: this.time12hFormat, hour: 'numeric', minute: 'numeric' }
|
{ hour12: this.time12hFormat, hour: 'numeric', minute: 'numeric' },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return this.date.toLocaleDateString(
|
return this.date.toLocaleDateString(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { throttle, orderBy, uniqueId } from 'lodash'
|
import { orderBy, throttle, uniqueId } from 'lodash'
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
|
|
@ -10,6 +10,11 @@ const ChatMessageList = {
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
messages: Array,
|
messages: Array,
|
||||||
|
pendingMessages: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
headerDate: Boolean,
|
headerDate: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -19,76 +24,87 @@ const ChatMessageList = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
chatItems() {
|
chatItems() {
|
||||||
const messages = orderBy(this.messages, ['pending', 'id'], ['asc', 'asc'])
|
const messages = [
|
||||||
return messages.reduceRight((acc, message, index) => {
|
...orderBy(this.messages, ['pending', 'id'], ['asc', 'asc']),
|
||||||
const date = new Date(message.created_at)
|
...this.pendingMessages.map((m) => ({ ...m, pending: true })),
|
||||||
|
]
|
||||||
|
return messages
|
||||||
|
.reduceRight((acc, message, index) => {
|
||||||
|
const date = new Date(message.created_at)
|
||||||
|
|
||||||
const olderMessage = messages[index - 1]
|
const olderMessage = messages[index - 1]
|
||||||
const newerMessage = messages[index + 1]
|
const newerMessage = messages[index + 1]
|
||||||
const newerItem = acc[acc.length - 1]
|
const newerItem = acc[acc.length - 1]
|
||||||
|
|
||||||
const diff = olderMessage ? message.created_at - olderMessage.created_at : null
|
const diff = olderMessage
|
||||||
|
? message.created_at - olderMessage.created_at
|
||||||
|
: null
|
||||||
|
|
||||||
const MAX_DIFF = 1000 * 60 * 5 // 5 minutes
|
const MAX_DIFF = 1000 * 60 * 5 // 5 minutes
|
||||||
|
|
||||||
const dateDiffs = (() => {
|
const dateDiffs = (() => {
|
||||||
if (olderMessage) {
|
if (olderMessage) {
|
||||||
const newerDate = new Date(message.created_at)
|
const newerDate = new Date(message.created_at)
|
||||||
const olderDate = new Date(olderMessage.created_at)
|
const olderDate = new Date(olderMessage.created_at)
|
||||||
|
|
||||||
newerDate.setHours(0, 0, 0, 0)
|
newerDate.setHours(0, 0, 0, 0)
|
||||||
olderDate.setHours(0, 0, 0, 0)
|
olderDate.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
return newerDate.toISOString() !== olderDate.toISOString()
|
return newerDate.toISOString() !== olderDate.toISOString()
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
chatItem.messageChainId = newerItem.messageChainId
|
return true
|
||||||
chatItem.isTail = false
|
}
|
||||||
newerItem.isHead = false
|
})()
|
||||||
|
|
||||||
|
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 && this.headerDate)) {
|
if (diff > MAX_DIFF || (!olderMessage && this.headerDate)) {
|
||||||
return [...acc, chatItem, {
|
return [
|
||||||
type: 'date',
|
...acc,
|
||||||
date,
|
chatItem,
|
||||||
isDate: dateDiffs,
|
{
|
||||||
isTime: diff > MAX_DIFF && !dateDiffs,
|
type: 'date',
|
||||||
id: date.getTime().toString(),
|
date,
|
||||||
}]
|
isDate: dateDiffs,
|
||||||
} else {
|
isTime: diff > MAX_DIFF && !dateDiffs,
|
||||||
return [...acc, chatItem]
|
id: date.getTime().toString(),
|
||||||
}
|
},
|
||||||
}, []).reverse()
|
]
|
||||||
}
|
} else {
|
||||||
|
return [...acc, chatItem]
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
.reverse()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onMessageHover({ isHovered, messageChainId }) {
|
onMessageHover({ isHovered, messageChainId }) {
|
||||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatMessageList
|
export default ChatMessageList
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
|
||||||
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
|
||||||
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
||||||
|
import ChatTitle from 'src/components/chat_title/chat_title.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'
|
||||||
|
|
@ -16,12 +17,13 @@ import {
|
||||||
} from './chat_layout_utils.js'
|
} from './chat_layout_utils.js'
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatMessages,
|
chatMessages,
|
||||||
getOrCreateChat,
|
getOrCreateChat,
|
||||||
|
readChat,
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
} from 'src/api/chats.js'
|
} from 'src/api/chats.js'
|
||||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||||
|
|
@ -37,23 +39,39 @@ const SAFE_RESIZE_TIME_OFFSET = 100
|
||||||
const MARK_AS_READ_DELAY = 1500
|
const MARK_AS_READ_DELAY = 1500
|
||||||
const MAX_RETRIES = 10
|
const MAX_RETRIES = 10
|
||||||
|
|
||||||
|
const isConfirmation = (storage, message) => {
|
||||||
|
if (!message.idempotency_key) return
|
||||||
|
return storage.idempotencyKeyIndex[message.idempotency_key]
|
||||||
|
}
|
||||||
|
|
||||||
const Chat = {
|
const Chat = {
|
||||||
components: {
|
components: {
|
||||||
ChatMessageList,
|
ChatMessageList,
|
||||||
ChatTitle,
|
ChatTitle,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
messages: Array,
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jumpToBottomButtonVisible: false,
|
// Main info
|
||||||
hoveredMessageChainId: undefined,
|
chat: null,
|
||||||
|
messages: [],
|
||||||
|
messagesIndex: {},
|
||||||
|
pendingMessages: [],
|
||||||
|
pendingMessagesIndex: {},
|
||||||
|
minId: undefined,
|
||||||
|
maxId: undefined,
|
||||||
|
|
||||||
|
// Unread stuff
|
||||||
|
newMessageCount: 0,
|
||||||
|
lastReadMessageId: null,
|
||||||
lastScrollPosition: {},
|
lastScrollPosition: {},
|
||||||
scrollableContainerHeight: '100%',
|
jumpToBottomButtonVisible: false,
|
||||||
|
|
||||||
|
// Internal network stuff
|
||||||
|
fetcher: null,
|
||||||
errorLoadingChat: false,
|
errorLoadingChat: false,
|
||||||
messageRetriers: {},
|
messageRetriers: {},
|
||||||
|
idempotencyKeyIndex: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
@ -83,11 +101,10 @@ const Chat = {
|
||||||
this.handleVisibilityChange,
|
this.handleVisibilityChange,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
this.$store.dispatch('clearCurrentChat')
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
recipient() {
|
recipient() {
|
||||||
return this.currentChat && this.currentChat.account
|
return this.chat?.account
|
||||||
},
|
},
|
||||||
recipientId() {
|
recipientId() {
|
||||||
return this.$route.params.recipient_id
|
return this.$route.params.recipient_id
|
||||||
|
|
@ -101,23 +118,12 @@ const Chat = {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chatMessages() {
|
|
||||||
return this.currentChatMessageService?.messages
|
|
||||||
},
|
|
||||||
newMessageCount() {
|
|
||||||
return this.currentChatMessageService?.newMessageCount
|
|
||||||
},
|
|
||||||
streamingEnabled() {
|
streamingEnabled() {
|
||||||
return (
|
return (
|
||||||
this.mergedConfig.useStreamingApi &&
|
this.mergedConfig.useStreamingApi &&
|
||||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...mapGetters([
|
|
||||||
'currentChat',
|
|
||||||
'currentChatMessageService',
|
|
||||||
'findOpenedChatByRecipientId',
|
|
||||||
]),
|
|
||||||
...mapPiniaState(useInterfaceStore, {
|
...mapPiniaState(useInterfaceStore, {
|
||||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||||
}),
|
}),
|
||||||
|
|
@ -128,7 +134,7 @@ const Chat = {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
chatMessages() {
|
messages() {
|
||||||
// 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)
|
||||||
|
|
@ -148,10 +154,6 @@ const Chat = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
|
||||||
onMessageHover({ isHovered, messageChainId }) {
|
|
||||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
|
||||||
},
|
|
||||||
onFilesDropped() {
|
onFilesDropped() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
|
|
@ -198,22 +200,22 @@ const Chat = {
|
||||||
this.readChat()
|
this.readChat()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
readChat() {
|
async readChat() {
|
||||||
if (
|
if (!this.maxId || document.hidden) {
|
||||||
!(
|
|
||||||
this.currentChatMessageService && this.currentChatMessageService.maxId
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (document.hidden) {
|
const lastReadId = this.maxId
|
||||||
return
|
const isNewMessage = this.lastReadMessageId !== lastReadId
|
||||||
}
|
|
||||||
const lastReadId = this.currentChatMessageService.maxId
|
if (!isNewMessage) return
|
||||||
this.$store.dispatch('readChat', {
|
|
||||||
id: this.currentChat.id,
|
await readChat({
|
||||||
|
id: this.chat.id,
|
||||||
lastReadId,
|
lastReadId,
|
||||||
|
credentials: useOAuthStore().token,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$store.commit('readChat', { id: this.chat.id, lastId: lastReadId })
|
||||||
},
|
},
|
||||||
bottomedOut(offset) {
|
bottomedOut(offset) {
|
||||||
return isBottomedOut(offset)
|
return isBottomedOut(offset)
|
||||||
|
|
@ -224,21 +226,32 @@ const Chat = {
|
||||||
cullOlderCheck() {
|
cullOlderCheck() {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||||
this.$store.dispatch(
|
const maxIndex = this.messages.length
|
||||||
'cullOlderMessages',
|
const minIndex = maxIndex - 50
|
||||||
this.currentChatMessageService.chatId,
|
if (maxIndex <= 50) return
|
||||||
)
|
|
||||||
|
this.messages = sortBy(this.messages, ['id'])
|
||||||
|
this.minId = this.messages[minIndex].id
|
||||||
|
|
||||||
|
for (const message of this.messages) {
|
||||||
|
if (message.id < this.minId) {
|
||||||
|
delete this.messagesIndex[message.id]
|
||||||
|
delete this.idempotencyKeyIndex[message.idempotency_key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages = this.messages.slice(minIndex, maxIndex)
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
},
|
},
|
||||||
handleScroll: throttle(function () {
|
handleScroll: throttle(function () {
|
||||||
this.lastScrollPosition = getScrollPosition()
|
if (!this.chat) {
|
||||||
if (!this.currentChat) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.lastScrollPosition = getScrollPosition()
|
||||||
|
|
||||||
if (this.reachedTop()) {
|
if (this.reachedTop()) {
|
||||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
this.fetchChat({ maxId: this.minId })
|
||||||
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||||
this.jumpToBottomButtonVisible = false
|
this.jumpToBottomButtonVisible = false
|
||||||
this.cullOlderCheck()
|
this.cullOlderCheck()
|
||||||
|
|
@ -257,85 +270,124 @@ const Chat = {
|
||||||
}, 200),
|
}, 200),
|
||||||
handleScrollUp(positionBeforeLoading) {
|
handleScrollUp(positionBeforeLoading) {
|
||||||
const positionAfterLoading = getScrollPosition()
|
const positionAfterLoading = getScrollPosition()
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
clear() {
|
||||||
const chatMessageService = this.currentChatMessageService
|
this.messages = this.messages.filter((m) => m.error)
|
||||||
if (!chatMessageService) {
|
this.messagesIndex = this.messages.reduce(
|
||||||
return
|
(acc, m) => ({
|
||||||
}
|
...acc,
|
||||||
|
[m.id]: m,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
this.newMessageCount = 0
|
||||||
|
this.lastReadMessageId = null
|
||||||
|
this.minId = undefined
|
||||||
|
this.maxId = undefined
|
||||||
|
},
|
||||||
|
async fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||||
if (fetchLatest && this.streamingEnabled) {
|
if (fetchLatest && this.streamingEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = chatMessageService.chatId
|
const { data: messages } = await chatMessages({
|
||||||
const fetchOlderMessages = !!maxId
|
id: this.chat.id,
|
||||||
const sinceId = fetchLatest && chatMessageService.maxId
|
|
||||||
|
|
||||||
return chatMessages({
|
|
||||||
id: chatId,
|
|
||||||
maxId,
|
maxId,
|
||||||
sinceId,
|
sinceId: fetchLatest ? this.maxId : null,
|
||||||
credentials: useOAuthStore().token,
|
credentials: useOAuthStore().token,
|
||||||
}).then(({ data: messages }) => {
|
|
||||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
|
||||||
if (isFirstFetch) {
|
|
||||||
chatService.clear(chatMessageService)
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionBeforeUpdate = getScrollPosition()
|
|
||||||
this.$store
|
|
||||||
.dispatch('addChatMessages', { chatId, messages })
|
|
||||||
.then(() => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (fetchOlderMessages) {
|
|
||||||
this.handleScrollUp(positionBeforeUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In vertical screens, the first batch of fetched messages may not always take the
|
|
||||||
// full height of the scrollable container.
|
|
||||||
// If this is the case, we want to fetch the messages until the scrollable container
|
|
||||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
|
||||||
if (!isScrollable() && messages.length > 0) {
|
|
||||||
this.fetchChat({
|
|
||||||
maxId: this.currentChatMessageService.minId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||||
|
if (isFirstFetch) {
|
||||||
|
this.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionBeforeUpdate = getScrollPosition()
|
||||||
|
this.addMessages({ messages })
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const fetchOlderMessages = !!maxId
|
||||||
|
if (fetchOlderMessages) {
|
||||||
|
this.handleScrollUp(positionBeforeUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In vertical screens, the first batch of fetched messages may not always take the
|
||||||
|
// full height of the scrollable container.
|
||||||
|
// If this is the case, we want to fetch the messages until the scrollable container
|
||||||
|
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||||
|
if (!isScrollable() && messages.length > 0) {
|
||||||
|
this.fetchChat({
|
||||||
|
maxId: this.minId,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async startFetching() {
|
async startFetching() {
|
||||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
try {
|
||||||
if (!chat) {
|
const { data } = await getOrCreateChat({
|
||||||
try {
|
accountId: this.recipientId,
|
||||||
const { data } = await getOrCreateChat({
|
credentials: useOAuthStore().token,
|
||||||
accountId: this.recipientId,
|
})
|
||||||
credentials: useOAuthStore().token,
|
this.chat = data
|
||||||
})
|
} catch (e) {
|
||||||
chat = data
|
console.error('Error creating or getting a chat', e)
|
||||||
} catch (e) {
|
this.errorLoadingChat = true
|
||||||
console.error('Error creating or getting a chat', e)
|
|
||||||
this.errorLoadingChat = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (chat) {
|
|
||||||
|
if (this.chat) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollDown({ forceRead: true })
|
this.scrollDown({ forceRead: true })
|
||||||
})
|
})
|
||||||
this.$store.dispatch('addOpenedChat', { chat })
|
|
||||||
this.doStartFetching()
|
this.doStartFetching()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doStartFetching() {
|
doStartFetching() {
|
||||||
this.$store.dispatch('startFetchingCurrentChat', {
|
this.fetcher = promiseInterval(
|
||||||
fetcher: () =>
|
() => this.fetchChat({ fetchLatest: true }),
|
||||||
promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000),
|
5000,
|
||||||
})
|
)
|
||||||
this.fetchChat({ isFirstFetch: true })
|
this.fetchChat({ isFirstFetch: true })
|
||||||
},
|
},
|
||||||
|
addMessages({ messages: newMessages }) {
|
||||||
|
for (let i = 0; i < newMessages.length; i++) {
|
||||||
|
const message = newMessages[i]
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (message.chat_id !== this.chat.id) {
|
||||||
|
console.warn(`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any known pending messages
|
||||||
|
if (message.idempotency_key) {
|
||||||
|
if (this.pendingMessagesIndex[message.idempotencyKeyIndex]) {
|
||||||
|
delete this.pendingMessagesIndex[message.idempotencyKeyIndex]
|
||||||
|
this.pendingMessages = this.pendingMessages.filter(({ idempotency_key }) => idempotency_key !== message.idempotency_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.minId || (!message.pending && message.id < this.minId)) {
|
||||||
|
this.minId = message.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.maxId || message.id > this.maxId) {
|
||||||
|
this.maxId = message.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.messagesIndex[message.id] && !isConfirmation(this, message)) {
|
||||||
|
if (this.lastSeenMessageId < message.id) {
|
||||||
|
this.newMessageCount++
|
||||||
|
}
|
||||||
|
this.messagesIndex[message.id] = message
|
||||||
|
this.messages.push(this.messagesIndex[message.id])
|
||||||
|
this.idempotencyKeyIndex[message.idempotency_key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
handleAttachmentPosting() {
|
handleAttachmentPosting() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.handleResize()
|
this.handleResize()
|
||||||
|
|
@ -344,9 +396,9 @@ const Chat = {
|
||||||
this.scrollDown({ forceRead: true })
|
this.scrollDown({ forceRead: true })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sendMessage({ status, media, idempotencyKey }) {
|
async sendMessage({ status, media, idempotencyKey }) {
|
||||||
const params = {
|
const params = {
|
||||||
id: this.currentChat.id,
|
id: this.chat.id,
|
||||||
content: status,
|
content: status,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
}
|
}
|
||||||
|
|
@ -357,69 +409,59 @@ const Chat = {
|
||||||
|
|
||||||
const fakeMessage = buildFakeMessage({
|
const fakeMessage = buildFakeMessage({
|
||||||
attachments: media,
|
attachments: media,
|
||||||
chatId: this.currentChat.id,
|
chatId: this.chat.id,
|
||||||
content: status,
|
content: status,
|
||||||
userId: this.currentUser.id,
|
userId: this.currentUser.id,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$store
|
this.pendingMessages.push(fakeMessage)
|
||||||
.dispatch('addChatMessages', {
|
this.pendingMessagesIndex[idempotencyKey] = fakeMessage
|
||||||
chatId: this.currentChat.id,
|
|
||||||
messages: [fakeMessage],
|
this.handleAttachmentPosting()
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.handleAttachmentPosting()
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.doSendMessage({
|
return this.doSendMessage({
|
||||||
params,
|
params,
|
||||||
fakeMessage,
|
|
||||||
retriesLeft: MAX_RETRIES,
|
retriesLeft: MAX_RETRIES,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
async doSendMessage({ params, pendingId, retriesLeft = MAX_RETRIES }) {
|
||||||
if (retriesLeft <= 0) return
|
if (retriesLeft <= 0) return
|
||||||
|
|
||||||
sendChatMessage({
|
try {
|
||||||
params,
|
const { data } = await sendChatMessage({
|
||||||
credentials: useOAuthStore().token,
|
...params,
|
||||||
})
|
credentials: useOAuthStore().token,
|
||||||
.then(({ data }) => {
|
|
||||||
this.$store.dispatch('addChatMessages', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
updateMaxId: false,
|
|
||||||
messages: [{ ...data, fakeId: fakeMessage.id }],
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error sending message', error)
|
|
||||||
this.$store.dispatch('handleMessageError', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
fakeId: fakeMessage.id,
|
|
||||||
isRetry: retriesLeft !== MAX_RETRIES,
|
|
||||||
})
|
|
||||||
if (
|
|
||||||
(error.statusCode >= 500 && error.statusCode < 600) ||
|
|
||||||
error.message === 'Failed to fetch'
|
|
||||||
) {
|
|
||||||
this.messageRetriers[fakeMessage.id] = setTimeout(
|
|
||||||
() => {
|
|
||||||
this.doSendMessage({
|
|
||||||
params,
|
|
||||||
fakeMessage,
|
|
||||||
retriesLeft: retriesLeft - 1,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
1000 * 2 ** (MAX_RETRIES - retriesLeft),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.resolve(fakeMessage)
|
this.addMessages({
|
||||||
|
messages: [{ ...data }],
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== 'StatusCodeError') throw error
|
||||||
|
console.error('Error sending message', error)
|
||||||
|
|
||||||
|
this.handleMessageError({
|
||||||
|
chatId: this.chat.id,
|
||||||
|
isRetry: retriesLeft !== MAX_RETRIES,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
(error.statusCode >= 500 && error.statusCode < 600) ||
|
||||||
|
error.message === 'Failed to fetch'
|
||||||
|
) {
|
||||||
|
this.messageRetriers[fakeMessage.id] = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.doSendMessage({
|
||||||
|
params,
|
||||||
|
fakeMessage,
|
||||||
|
retriesLeft: retriesLeft - 1,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
1000 * 2 ** (MAX_RETRIES - retriesLeft),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@
|
||||||
</div>
|
</div>
|
||||||
<ChatMessageList
|
<ChatMessageList
|
||||||
header-date
|
header-date
|
||||||
:messages="chatMessages"
|
:messages="messages"
|
||||||
|
:pending-messages="pendingMessages"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="footer"
|
ref="footer"
|
||||||
|
|
@ -56,7 +57,7 @@
|
||||||
:disable-polls="true"
|
:disable-polls="true"
|
||||||
:disable-quotes="true"
|
:disable-quotes="true"
|
||||||
:disable-sensitivity-checkbox="true"
|
:disable-sensitivity-checkbox="true"
|
||||||
:disable-submit="errorLoadingChat || !currentChat"
|
:disable-submit="errorLoadingChat || !chat"
|
||||||
:disable-preview="true"
|
:disable-preview="true"
|
||||||
:disable-draft="true"
|
:disable-draft="true"
|
||||||
:optimistic-posting="true"
|
:optimistic-posting="true"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { clone, filter, findIndex, get, reduce } from 'lodash'
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
||||||
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
|
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
|
||||||
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
|
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
|
||||||
import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
|
import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
|
||||||
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
|
||||||
|
|
||||||
import { useInterfaceStore } from 'src/stores/interface'
|
import { useInterfaceStore } from 'src/stores/interface'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
|
||||||
|
|
@ -596,16 +596,18 @@ const PostStatusForm = {
|
||||||
? this.postHandler
|
? this.postHandler
|
||||||
: statusPoster.postStatus
|
: statusPoster.postStatus
|
||||||
|
|
||||||
postHandler(postingOptions).then((data) => {
|
postHandler(postingOptions)
|
||||||
if (!data.error) {
|
.then((data) => {
|
||||||
this.abandonDraft()
|
this.abandonDraft()
|
||||||
this.clearStatus()
|
this.clearStatus()
|
||||||
this.$emit('posted', data)
|
this.$emit('posted', data)
|
||||||
} else {
|
})
|
||||||
|
.catch((error) => {
|
||||||
this.error = data.error
|
this.error = data.error
|
||||||
}
|
})
|
||||||
this.posting = false
|
.finally(() => {
|
||||||
})
|
this.posting = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
previewStatus() {
|
previewStatus() {
|
||||||
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import {
|
||||||
faBars,
|
faBars,
|
||||||
faFolderTree,
|
faFolderTree,
|
||||||
faList,
|
faList,
|
||||||
faWrench,
|
|
||||||
faMessage,
|
faMessage,
|
||||||
|
faWrench,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faList, faFolderTree, faBars, faWrench, faMessage)
|
library.add(faList, faFolderTree, faBars, faWrench, faMessage)
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,11 @@ import { reactive } from 'vue'
|
||||||
|
|
||||||
import chatService from '../services/chat_service/chat_service.js'
|
import chatService from '../services/chat_service/chat_service.js'
|
||||||
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
||||||
import {
|
|
||||||
parseChat,
|
|
||||||
parseChatMessage,
|
|
||||||
} from '../services/entity_normalizer/entity_normalizer.service.js'
|
|
||||||
import { promiseInterval } from '../services/promise_interval/promise_interval.js'
|
import { promiseInterval } from '../services/promise_interval/promise_interval.js'
|
||||||
|
|
||||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
|
|
||||||
import { chats, deleteChatMessage, readChat } from 'src/api/chats.js'
|
import { chats, deleteChatMessage } from 'src/api/chats.js'
|
||||||
|
|
||||||
const emptyChatList = () => ({
|
const emptyChatList = () => ({
|
||||||
data: [],
|
data: [],
|
||||||
|
|
@ -21,8 +17,6 @@ const emptyChatList = () => ({
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
chatList: emptyChatList(),
|
chatList: emptyChatList(),
|
||||||
chatListFetcher: null,
|
chatListFetcher: null,
|
||||||
openedChats: reactive({}),
|
|
||||||
openedChatMessageServices: reactive({}),
|
|
||||||
fetcher: undefined,
|
fetcher: undefined,
|
||||||
currentChatId: null,
|
currentChatId: null,
|
||||||
lastReadMessageId: null,
|
lastReadMessageId: null,
|
||||||
|
|
@ -43,11 +37,6 @@ const unreadChatCount = (state) => {
|
||||||
const chatsModule = {
|
const chatsModule = {
|
||||||
state: { ...defaultState },
|
state: { ...defaultState },
|
||||||
getters: {
|
getters: {
|
||||||
currentChat: (state) => state.openedChats[state.currentChatId],
|
|
||||||
currentChatMessageService: (state) =>
|
|
||||||
state.openedChatMessageServices[state.currentChatId],
|
|
||||||
findOpenedChatByRecipientId: (state) => (recipientId) =>
|
|
||||||
find(state.openedChats, (c) => c.account.id === recipientId),
|
|
||||||
sortedChatList,
|
sortedChatList,
|
||||||
unreadChatCount,
|
unreadChatCount,
|
||||||
},
|
},
|
||||||
|
|
@ -65,8 +54,8 @@ const chatsModule = {
|
||||||
fetchChats({ dispatch, rootState }) {
|
fetchChats({ dispatch, rootState }) {
|
||||||
return chats({
|
return chats({
|
||||||
credentials: useOAuthStore().token,
|
credentials: useOAuthStore().token,
|
||||||
}).then(({ chatList }) => {
|
}).then(({ data }) => {
|
||||||
dispatch('addNewChats', { chats: chatList })
|
dispatch('addNewChats', { chats: data })
|
||||||
return chats
|
return chats
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -86,45 +75,15 @@ const chatsModule = {
|
||||||
newChatMessageSideEffects,
|
newChatMessageSideEffects,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateChat({ commit }, { chat }) {
|
|
||||||
commit('updateChat', { chat })
|
|
||||||
},
|
|
||||||
|
|
||||||
// Opened Chats
|
// Opened Chats
|
||||||
startFetchingCurrentChat({ dispatch }, { fetcher }) {
|
|
||||||
dispatch('setCurrentChatFetcher', { fetcher })
|
|
||||||
},
|
|
||||||
setCurrentChatFetcher({ commit }, { fetcher }) {
|
|
||||||
commit('setCurrentChatFetcher', { fetcher })
|
|
||||||
},
|
|
||||||
addOpenedChat({ commit, dispatch }, { chat }) {
|
addOpenedChat({ commit, dispatch }, { chat }) {
|
||||||
commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
|
commit('addOpenedChat', { dispatch, chat })
|
||||||
dispatch('addNewUsers', [chat.account])
|
dispatch('addNewUsers', [chat.account])
|
||||||
},
|
},
|
||||||
addChatMessages({ commit }, value) {
|
addChatMessages({ commit }, value) {
|
||||||
commit('addChatMessages', { commit, ...value })
|
commit('addChatMessages', { commit, ...value })
|
||||||
},
|
},
|
||||||
resetChatNewMessageCount({ commit }, value) {
|
|
||||||
commit('resetChatNewMessageCount', value)
|
|
||||||
},
|
|
||||||
clearCurrentChat({ commit }) {
|
|
||||||
commit('setCurrentChatId', { chatId: undefined })
|
|
||||||
commit('setCurrentChatFetcher', { fetcher: undefined })
|
|
||||||
},
|
|
||||||
readChat({ rootState, commit, dispatch }, { id, lastReadId }) {
|
|
||||||
const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
|
|
||||||
|
|
||||||
dispatch('resetChatNewMessageCount')
|
|
||||||
commit('readChat', { id, lastReadId })
|
|
||||||
|
|
||||||
if (isNewMessage) {
|
|
||||||
readChat({
|
|
||||||
id,
|
|
||||||
lastReadId,
|
|
||||||
credentials: useOAuthStore().token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteChatMessage({ rootState, commit }, value) {
|
deleteChatMessage({ rootState, commit }, value) {
|
||||||
deleteChatMessage({
|
deleteChatMessage({
|
||||||
...value,
|
...value,
|
||||||
|
|
@ -133,7 +92,6 @@ const chatsModule = {
|
||||||
commit('deleteChatMessage', { commit, ...value })
|
commit('deleteChatMessage', { commit, ...value })
|
||||||
},
|
},
|
||||||
resetChats({ commit, dispatch }) {
|
resetChats({ commit, dispatch }) {
|
||||||
dispatch('clearCurrentChat')
|
|
||||||
commit('resetChats', { commit })
|
commit('resetChats', { commit })
|
||||||
},
|
},
|
||||||
clearOpenedChats({ commit }) {
|
clearOpenedChats({ commit }) {
|
||||||
|
|
@ -142,9 +100,6 @@ const chatsModule = {
|
||||||
handleMessageError({ commit }, value) {
|
handleMessageError({ commit }, value) {
|
||||||
commit('handleMessageError', { commit, ...value })
|
commit('handleMessageError', { commit, ...value })
|
||||||
},
|
},
|
||||||
cullOlderMessages({ commit }, chatId) {
|
|
||||||
commit('cullOlderMessages', chatId)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setChatListFetcher(state, { fetcher }) {
|
setChatListFetcher(state, { fetcher }) {
|
||||||
|
|
@ -154,21 +109,6 @@ const chatsModule = {
|
||||||
}
|
}
|
||||||
state.chatListFetcher = fetcher && fetcher()
|
state.chatListFetcher = fetcher && fetcher()
|
||||||
},
|
},
|
||||||
setCurrentChatFetcher(state, { fetcher }) {
|
|
||||||
const prevFetcher = state.fetcher
|
|
||||||
if (prevFetcher) {
|
|
||||||
prevFetcher.stop()
|
|
||||||
}
|
|
||||||
state.fetcher = fetcher && fetcher()
|
|
||||||
},
|
|
||||||
addOpenedChat(state, { chat }) {
|
|
||||||
state.currentChatId = chat.id
|
|
||||||
state.openedChats[chat.id] = chat
|
|
||||||
|
|
||||||
if (!state.openedChatMessageServices[chat.id]) {
|
|
||||||
state.openedChatMessageServices[chat.id] = chatService.empty(chat.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setCurrentChatId(state, { chatId }) {
|
setCurrentChatId(state, { chatId }) {
|
||||||
state.currentChatId = chatId
|
state.currentChatId = chatId
|
||||||
},
|
},
|
||||||
|
|
@ -217,60 +157,16 @@ const chatsModule = {
|
||||||
state.chatList = emptyChatList()
|
state.chatList = emptyChatList()
|
||||||
state.currentChatId = null
|
state.currentChatId = null
|
||||||
commit('setChatListFetcher', { fetcher: undefined })
|
commit('setChatListFetcher', { fetcher: undefined })
|
||||||
for (const chatId in state.openedChats) {
|
|
||||||
chatService.clear(state.openedChatMessageServices[chatId])
|
|
||||||
delete state.openedChats[chatId]
|
|
||||||
delete state.openedChatMessageServices[chatId]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setChatsLoading(state, { value }) {
|
setChatsLoading(state, { value }) {
|
||||||
state.chats.loading = value
|
state.chats.loading = value
|
||||||
},
|
},
|
||||||
addChatMessages(state, { chatId, messages, updateMaxId }) {
|
|
||||||
const chatMessageService = state.openedChatMessageServices[chatId]
|
|
||||||
if (chatMessageService) {
|
|
||||||
chatService.add(chatMessageService, {
|
|
||||||
messages: messages.map(parseChatMessage),
|
|
||||||
updateMaxId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteChatMessage(state, { chatId, messageId }) {
|
|
||||||
const chatMessageService = state.openedChatMessageServices[chatId]
|
|
||||||
if (chatMessageService) {
|
|
||||||
chatService.deleteMessage(chatMessageService, messageId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetChatNewMessageCount(state) {
|
|
||||||
const chatMessageService =
|
|
||||||
state.openedChatMessageServices[state.currentChatId]
|
|
||||||
chatService.resetNewMessageCount(chatMessageService)
|
|
||||||
},
|
|
||||||
// Used when a connection loss occurs
|
|
||||||
clearOpenedChats(state) {
|
|
||||||
const currentChatId = state.currentChatId
|
|
||||||
for (const chatId in state.openedChats) {
|
|
||||||
if (currentChatId !== chatId) {
|
|
||||||
chatService.clear(state.openedChatMessageServices[chatId])
|
|
||||||
delete state.openedChats[chatId]
|
|
||||||
delete state.openedChatMessageServices[chatId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
readChat(state, { id, lastReadId }) {
|
readChat(state, { id, lastReadId }) {
|
||||||
state.lastReadMessageId = lastReadId
|
|
||||||
const chat = getChatById(state, id)
|
const chat = getChatById(state, id)
|
||||||
if (chat) {
|
if (chat) {
|
||||||
chat.unread = 0
|
chat.unread = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleMessageError(state, { chatId, fakeId, isRetry }) {
|
|
||||||
const chatMessageService = state.openedChatMessageServices[chatId]
|
|
||||||
chatService.handleMessageError(chatMessageService, fakeId, isRetry)
|
|
||||||
},
|
|
||||||
cullOlderMessages(state, chatId) {
|
|
||||||
chatService.cullOlderMessages(state.openedChatMessageServices[chatId])
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,5 @@
|
||||||
import { maxBy, minBy, orderBy, sortBy, uniqueId } from 'lodash'
|
import { maxBy, minBy, orderBy, sortBy, uniqueId } from 'lodash'
|
||||||
|
|
||||||
const empty = (chatId) => {
|
|
||||||
return {
|
|
||||||
idIndex: {},
|
|
||||||
idempotencyKeyIndex: {},
|
|
||||||
messages: [],
|
|
||||||
newMessageCount: 0,
|
|
||||||
lastSeenMessageId: '0',
|
|
||||||
chatId,
|
|
||||||
minId: undefined,
|
|
||||||
maxId: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clear = (storage) => {
|
|
||||||
const failedMessageIds = []
|
|
||||||
|
|
||||||
for (const message of storage.messages) {
|
|
||||||
if (message.error) {
|
|
||||||
failedMessageIds.push(message.id)
|
|
||||||
} else {
|
|
||||||
delete storage.idIndex[message.id]
|
|
||||||
delete storage.idempotencyKeyIndex[message.idempotency_key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.messages = storage.messages.filter((m) =>
|
|
||||||
failedMessageIds.includes(m.id),
|
|
||||||
)
|
|
||||||
storage.newMessageCount = 0
|
|
||||||
storage.lastSeenMessageId = '0'
|
|
||||||
storage.minId = undefined
|
|
||||||
storage.maxId = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMessage = (storage, messageId) => {
|
const deleteMessage = (storage, messageId) => {
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
return
|
return
|
||||||
|
|
@ -52,22 +18,6 @@ const deleteMessage = (storage, messageId) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cullOlderMessages = (storage) => {
|
|
||||||
const maxIndex = storage.messages.length
|
|
||||||
const minIndex = maxIndex - 50
|
|
||||||
if (maxIndex <= 50) return
|
|
||||||
|
|
||||||
storage.messages = sortBy(storage.messages, ['id'])
|
|
||||||
storage.minId = storage.messages[minIndex].id
|
|
||||||
for (const message of storage.messages) {
|
|
||||||
if (message.id < storage.minId) {
|
|
||||||
delete storage.idIndex[message.id]
|
|
||||||
delete storage.idempotencyKeyIndex[message.idempotency_key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storage.messages = storage.messages.slice(minIndex, maxIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMessageError = (storage, fakeId, isRetry) => {
|
const handleMessageError = (storage, fakeId, isRetry) => {
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
return
|
return
|
||||||
|
|
@ -93,63 +43,6 @@ const handleMessageError = (storage, fakeId, isRetry) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
|
||||||
if (!storage) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for (let i = 0; i < newMessages.length; i++) {
|
|
||||||
const message = newMessages[i]
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
if (message.chat_id !== storage.chatId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.fakeId) {
|
|
||||||
const fakeMessage = storage.idIndex[message.fakeId]
|
|
||||||
if (fakeMessage) {
|
|
||||||
// In case the same id exists (chat update before POST response)
|
|
||||||
// make sure to remove the older duplicate message.
|
|
||||||
if (storage.idIndex[message.id]) {
|
|
||||||
delete storage.idIndex[message.id]
|
|
||||||
storage.messages = storage.messages.filter(
|
|
||||||
(msg) => msg.id !== message.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Object.assign(fakeMessage, message, { error: false })
|
|
||||||
delete fakeMessage.fakeId
|
|
||||||
storage.idIndex[fakeMessage.id] = fakeMessage
|
|
||||||
delete storage.idIndex[message.fakeId]
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage.minId || (!message.pending && message.id < storage.minId)) {
|
|
||||||
storage.minId = message.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage.maxId || message.id > storage.maxId) {
|
|
||||||
if (updateMaxId) {
|
|
||||||
storage.maxId = message.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
|
|
||||||
if (storage.lastSeenMessageId < message.id) {
|
|
||||||
storage.newMessageCount++
|
|
||||||
}
|
|
||||||
storage.idIndex[message.id] = message
|
|
||||||
storage.messages.push(storage.idIndex[message.id])
|
|
||||||
storage.idempotencyKeyIndex[message.idempotency_key] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConfirmation = (storage, message) => {
|
|
||||||
if (!message.idempotency_key) return
|
|
||||||
return storage.idempotencyKeyIndex[message.idempotency_key]
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetNewMessageCount = (storage) => {
|
const resetNewMessageCount = (storage) => {
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
|
|
@ -160,12 +53,8 @@ const resetNewMessageCount = (storage) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatService = {
|
const ChatService = {
|
||||||
add,
|
|
||||||
empty,
|
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
cullOlderMessages,
|
|
||||||
resetNewMessageCount,
|
resetNewMessageCount,
|
||||||
clear,
|
|
||||||
handleMessageError,
|
handleMessageError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue