2026-07-08 20:29:59 +03:00
|
|
|
import { maxBy, minBy, sortBy, throttle } from 'lodash'
|
2023-04-05 21:06:37 -06:00
|
|
|
import { mapState as mapPiniaState } from 'pinia'
|
2026-07-08 20:29:59 +03:00
|
|
|
import { nextTick } from 'vue'
|
|
|
|
|
import { mapState } from 'vuex'
|
2026-01-08 17:26:52 +02:00
|
|
|
|
2026-07-07 20:44:13 +03:00
|
|
|
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
2026-07-08 17:47:50 +03:00
|
|
|
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
2026-06-04 21:59:09 +03:00
|
|
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
2026-01-06 16:23:17 +02:00
|
|
|
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
2020-09-04 11:19:53 +03:00
|
|
|
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
2020-10-20 21:03:46 +03:00
|
|
|
import {
|
2026-01-06 16:22:52 +02:00
|
|
|
getNewTopPosition,
|
2026-01-06 16:23:17 +02:00
|
|
|
getScrollPosition,
|
2026-01-06 16:22:52 +02:00
|
|
|
isBottomedOut,
|
|
|
|
|
isScrollable,
|
|
|
|
|
} from './chat_layout_utils.js'
|
2020-10-20 21:03:46 +03:00
|
|
|
|
2026-07-08 20:29:59 +03:00
|
|
|
import { useChatsStore } from 'src/stores/chats.js'
|
2026-01-29 20:40:00 +02:00
|
|
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
2026-07-07 20:44:13 +03:00
|
|
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
2026-07-08 17:47:50 +03:00
|
|
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
2026-01-29 20:40:00 +02:00
|
|
|
|
2026-06-15 20:02:22 +03:00
|
|
|
import {
|
|
|
|
|
chatMessages,
|
2026-07-08 20:29:59 +03:00
|
|
|
deleteChatMessage,
|
2026-06-15 20:02:22 +03:00
|
|
|
getOrCreateChat,
|
2026-07-08 17:47:50 +03:00
|
|
|
readChat,
|
2026-06-15 20:02:22 +03:00
|
|
|
sendChatMessage,
|
2026-06-17 14:36:45 +03:00
|
|
|
} from 'src/api/chats.js'
|
2026-06-24 19:41:28 +03:00
|
|
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
2026-06-15 20:02:22 +03:00
|
|
|
|
2026-01-08 17:26:52 +02:00
|
|
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
|
|
|
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
|
|
|
|
|
2026-01-06 16:22:52 +02:00
|
|
|
library.add(faChevronDown, faChevronLeft)
|
2020-05-07 16:10:53 +03:00
|
|
|
|
|
|
|
|
const BOTTOMED_OUT_OFFSET = 10
|
2022-04-04 19:41:09 +03:00
|
|
|
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
|
2020-06-21 17:13:29 +03:00
|
|
|
const SAFE_RESIZE_TIME_OFFSET = 100
|
2020-10-27 10:03:04 +02:00
|
|
|
const MARK_AS_READ_DELAY = 1500
|
2020-10-29 13:33:06 +03:00
|
|
|
const MAX_RETRIES = 10
|
2020-05-07 16:10:53 +03:00
|
|
|
|
2026-07-08 17:47:50 +03:00
|
|
|
const isConfirmation = (storage, message) => {
|
|
|
|
|
if (!message.idempotency_key) return
|
|
|
|
|
return storage.idempotencyKeyIndex[message.idempotency_key]
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-07 16:10:53 +03:00
|
|
|
const Chat = {
|
|
|
|
|
components: {
|
2026-07-07 20:44:13 +03:00
|
|
|
ChatMessageList,
|
2020-05-07 16:10:53 +03:00
|
|
|
ChatTitle,
|
2026-01-06 16:22:52 +02:00
|
|
|
PostStatusForm,
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
data() {
|
2020-05-07 16:10:53 +03:00
|
|
|
return {
|
2026-07-08 17:47:50 +03:00
|
|
|
// Main info
|
|
|
|
|
chat: null,
|
|
|
|
|
messages: [],
|
|
|
|
|
messagesIndex: {},
|
|
|
|
|
pendingMessages: [],
|
|
|
|
|
pendingMessagesIndex: {},
|
|
|
|
|
minId: undefined,
|
|
|
|
|
maxId: undefined,
|
|
|
|
|
|
|
|
|
|
// Unread stuff
|
|
|
|
|
newMessageCount: 0,
|
|
|
|
|
lastReadMessageId: null,
|
2020-06-21 17:13:29 +03:00
|
|
|
lastScrollPosition: {},
|
2026-07-08 17:47:50 +03:00
|
|
|
jumpToBottomButtonVisible: false,
|
|
|
|
|
|
|
|
|
|
// Internal network stuff
|
|
|
|
|
fetcher: null,
|
2020-10-29 13:33:06 +03:00
|
|
|
errorLoadingChat: false,
|
2026-01-06 16:22:52 +02:00
|
|
|
messageRetriers: {},
|
2026-07-08 17:47:50 +03:00
|
|
|
idempotencyKeyIndex: {},
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
created() {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.startFetching()
|
2022-05-22 12:18:20 +03:00
|
|
|
window.addEventListener('resize', this.handleResize)
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
mounted() {
|
2022-04-10 19:28:26 +03:00
|
|
|
window.addEventListener('scroll', this.handleScroll)
|
2020-05-07 16:10:53 +03:00
|
|
|
if (typeof document.hidden !== 'undefined') {
|
2026-01-06 16:22:52 +02:00
|
|
|
document.addEventListener(
|
|
|
|
|
'visibilitychange',
|
|
|
|
|
this.handleVisibilityChange,
|
|
|
|
|
false,
|
|
|
|
|
)
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.handleResize()
|
|
|
|
|
})
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
unmounted() {
|
2022-04-10 19:28:26 +03:00
|
|
|
window.removeEventListener('scroll', this.handleScroll)
|
2022-08-09 01:44:44 +03:00
|
|
|
window.removeEventListener('resize', this.handleResize)
|
2026-01-06 16:22:52 +02:00
|
|
|
if (typeof document.hidden !== 'undefined')
|
|
|
|
|
document.removeEventListener(
|
|
|
|
|
'visibilitychange',
|
|
|
|
|
this.handleVisibilityChange,
|
|
|
|
|
false,
|
|
|
|
|
)
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
|
|
|
|
computed: {
|
2026-01-06 16:22:52 +02:00
|
|
|
recipient() {
|
2026-07-08 17:47:50 +03:00
|
|
|
return this.chat?.account
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
recipientId() {
|
2020-05-07 16:10:53 +03:00
|
|
|
return this.$route.params.recipient_id
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
formPlaceholder() {
|
2020-05-07 16:10:53 +03:00
|
|
|
if (this.recipient) {
|
2026-01-06 16:22:52 +02:00
|
|
|
return this.$t('chats.message_user', {
|
|
|
|
|
nickname: this.recipient.screen_name_ui,
|
|
|
|
|
})
|
2020-05-07 16:10:53 +03:00
|
|
|
} else {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
streamingEnabled() {
|
|
|
|
|
return (
|
|
|
|
|
this.mergedConfig.useStreamingApi &&
|
|
|
|
|
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
|
|
|
|
)
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2023-04-05 21:06:37 -06:00
|
|
|
...mapPiniaState(useInterfaceStore, {
|
2026-01-06 16:22:52 +02:00
|
|
|
mobileLayout: (store) => store.layoutType === 'mobile',
|
2023-04-05 21:06:37 -06:00
|
|
|
}),
|
2026-07-07 20:44:13 +03:00
|
|
|
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
2020-05-07 16:10:53 +03:00
|
|
|
...mapState({
|
2026-01-06 16:22:52 +02:00
|
|
|
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
|
|
|
|
currentUser: (state) => state.users.currentUser,
|
|
|
|
|
}),
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
|
|
|
|
watch: {
|
2026-07-08 17:47:50 +03:00
|
|
|
messages() {
|
2020-05-07 16:10:53 +03:00
|
|
|
// 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)
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
if (bottomedOutBeforeUpdate) {
|
2020-10-27 10:03:04 +02:00
|
|
|
this.scrollDown()
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
2022-07-31 12:35:48 +03:00
|
|
|
$route: function () {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.startFetching()
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
mastoUserSocketStatus(newValue) {
|
2020-05-07 16:10:53 +03:00
|
|
|
if (newValue === WSConnectionStatus.JOINED) {
|
|
|
|
|
this.fetchChat({ isFirstFetch: true })
|
|
|
|
|
}
|
2026-01-06 16:22:52 +02:00
|
|
|
},
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
|
|
|
|
methods: {
|
2026-01-06 16:22:52 +02:00
|
|
|
onFilesDropped() {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.$nextTick(() => {
|
2020-06-21 17:13:29 +03:00
|
|
|
this.handleResize()
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
handleVisibilityChange() {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.$nextTick(() => {
|
|
|
|
|
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
|
|
|
|
this.scrollDown({ forceRead: true })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
2022-05-22 12:18:20 +03:00
|
|
|
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
2026-01-06 16:22:52 +02:00
|
|
|
handleResize(opts = {}) {
|
2022-08-09 01:44:44 +03:00
|
|
|
const { delayed = false } = opts
|
2020-06-21 17:13:29 +03:00
|
|
|
|
|
|
|
|
if (delayed) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.handleResize({ ...opts, delayed: false })
|
|
|
|
|
}, SAFE_RESIZE_TIME_OFFSET)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-07 16:10:53 +03:00
|
|
|
this.$nextTick(() => {
|
2022-05-22 12:18:20 +03:00
|
|
|
const { offsetHeight = undefined } = getScrollPosition()
|
2022-08-09 01:44:44 +03:00
|
|
|
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
|
|
|
|
|
if (diff !== 0 && !this.bottomedOut()) {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.$nextTick(() => {
|
2022-08-09 01:44:44 +03:00
|
|
|
window.scrollBy({ top: -Math.trunc(diff) })
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
|
|
|
|
}
|
2022-05-22 12:18:20 +03:00
|
|
|
this.lastScrollPosition = getScrollPosition()
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
scrollDown(options = {}) {
|
2020-05-07 16:10:53 +03:00
|
|
|
const { behavior = 'auto', forceRead = false } = options
|
|
|
|
|
this.$nextTick(() => {
|
2026-01-06 16:22:52 +02:00
|
|
|
window.scrollTo({
|
|
|
|
|
top: document.documentElement.scrollHeight,
|
|
|
|
|
behavior,
|
|
|
|
|
})
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
2020-10-27 10:03:04 +02:00
|
|
|
if (forceRead) {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.readChat()
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-07-08 17:47:50 +03:00
|
|
|
async readChat() {
|
|
|
|
|
if (!this.maxId || document.hidden) {
|
2026-01-06 16:22:52 +02:00
|
|
|
return
|
|
|
|
|
}
|
2026-07-08 17:47:50 +03:00
|
|
|
const lastReadId = this.maxId
|
|
|
|
|
const isNewMessage = this.lastReadMessageId !== lastReadId
|
|
|
|
|
|
|
|
|
|
if (!isNewMessage) return
|
|
|
|
|
|
|
|
|
|
await readChat({
|
|
|
|
|
id: this.chat.id,
|
2026-01-06 16:22:52 +02:00
|
|
|
lastReadId,
|
2026-07-08 17:47:50 +03:00
|
|
|
credentials: useOAuthStore().token,
|
2020-10-29 13:33:06 +03:00
|
|
|
})
|
2026-07-08 17:47:50 +03:00
|
|
|
|
2026-07-08 20:29:59 +03:00
|
|
|
useChatsStore().readChat(this.chat.id)
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
bottomedOut(offset) {
|
2022-04-10 19:28:26 +03:00
|
|
|
return isBottomedOut(offset)
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
reachedTop() {
|
2022-04-10 19:28:26 +03:00
|
|
|
return window.scrollY <= 0
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
cullOlderCheck() {
|
2021-02-18 10:14:45 +02:00
|
|
|
window.setTimeout(() => {
|
|
|
|
|
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
2026-07-08 17:47:50 +03:00
|
|
|
const maxIndex = this.messages.length
|
|
|
|
|
const minIndex = maxIndex - 50
|
|
|
|
|
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)
|
2021-02-18 10:14:45 +02:00
|
|
|
}
|
|
|
|
|
}, 5000)
|
|
|
|
|
},
|
2026-06-03 01:56:18 +03:00
|
|
|
handleScroll: throttle(function () {
|
2026-07-08 17:47:50 +03:00
|
|
|
if (!this.chat) {
|
2026-01-06 16:22:52 +02:00
|
|
|
return
|
|
|
|
|
}
|
2026-07-08 17:47:50 +03:00
|
|
|
this.lastScrollPosition = getScrollPosition()
|
2020-05-07 16:10:53 +03:00
|
|
|
|
|
|
|
|
if (this.reachedTop()) {
|
2026-07-08 17:47:50 +03:00
|
|
|
this.fetchChat({ maxId: this.minId })
|
2020-05-07 16:10:53 +03:00
|
|
|
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
|
|
|
|
this.jumpToBottomButtonVisible = false
|
2021-02-18 10:14:45 +02:00
|
|
|
this.cullOlderCheck()
|
2020-05-07 16:10:53 +03:00
|
|
|
if (this.newMessageCount > 0) {
|
2020-10-27 10:03:04 +02:00
|
|
|
// Use a delay before marking as read to prevent situation where new messages
|
|
|
|
|
// arrive just as you're leaving the view and messages that you didn't actually
|
|
|
|
|
// get to see get marked as read.
|
|
|
|
|
window.setTimeout(() => {
|
2020-10-29 12:45:44 +02:00
|
|
|
// Don't mark as read if the element doesn't exist, user has left chat view
|
2020-10-27 10:03:04 +02:00
|
|
|
if (this.$el) this.readChat()
|
|
|
|
|
}, MARK_AS_READ_DELAY)
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.jumpToBottomButtonVisible = true
|
|
|
|
|
}
|
2020-10-27 10:03:04 +02:00
|
|
|
}, 200),
|
2026-01-06 16:22:52 +02:00
|
|
|
handleScrollUp(positionBeforeLoading) {
|
2022-04-10 19:28:26 +03:00
|
|
|
const positionAfterLoading = getScrollPosition()
|
2026-07-08 17:47:50 +03:00
|
|
|
|
2022-04-10 19:28:26 +03:00
|
|
|
window.scrollTo({
|
2026-01-06 16:22:52 +02:00
|
|
|
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
|
|
|
|
},
|
2026-07-08 17:47:50 +03:00
|
|
|
clear() {
|
|
|
|
|
this.messages = this.messages.filter((m) => m.error)
|
|
|
|
|
this.messagesIndex = this.messages.reduce(
|
|
|
|
|
(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 }) {
|
2026-01-06 16:22:52 +02:00
|
|
|
if (fetchLatest && this.streamingEnabled) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2020-05-07 16:10:53 +03:00
|
|
|
|
2026-07-08 17:47:50 +03:00
|
|
|
const { data: messages } = await chatMessages({
|
|
|
|
|
id: this.chat.id,
|
2026-06-15 20:02:22 +03:00
|
|
|
maxId,
|
2026-07-08 17:47:50 +03:00
|
|
|
sinceId: fetchLatest ? this.maxId : null,
|
2026-06-16 17:32:26 +03:00
|
|
|
credentials: useOAuthStore().token,
|
2026-06-15 20:02:22 +03:00
|
|
|
})
|
2026-07-08 17:47:50 +03:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
})
|
|
|
|
|
}
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
async startFetching() {
|
2026-07-08 17:47:50 +03:00
|
|
|
try {
|
|
|
|
|
const { data } = await getOrCreateChat({
|
|
|
|
|
accountId: this.recipientId,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
})
|
|
|
|
|
this.chat = data
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error creating or getting a chat', e)
|
|
|
|
|
this.errorLoadingChat = true
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
2026-07-08 17:47:50 +03:00
|
|
|
|
|
|
|
|
if (this.chat) {
|
2020-05-07 16:10:53 +03:00
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.scrollDown({ forceRead: true })
|
|
|
|
|
})
|
|
|
|
|
this.doStartFetching()
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
doStartFetching() {
|
2026-07-08 17:47:50 +03:00
|
|
|
this.fetcher = promiseInterval(
|
|
|
|
|
() => this.fetchChat({ fetchLatest: true }),
|
|
|
|
|
5000,
|
|
|
|
|
)
|
2020-05-07 16:10:53 +03:00
|
|
|
this.fetchChat({ isFirstFetch: true })
|
|
|
|
|
},
|
2026-07-08 19:34:09 +03:00
|
|
|
async deleteChatMessage({ chatId, messageId }) {
|
|
|
|
|
await deleteChatMessage({
|
|
|
|
|
chatId,
|
|
|
|
|
messageId,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.messages = this.messages.filter((m) => m.id !== messageId)
|
|
|
|
|
delete this.messagesIndex[messageId]
|
|
|
|
|
|
|
|
|
|
if (this.maxId === messageId) {
|
|
|
|
|
const lastMessage = maxBy(this.messages, 'id')
|
|
|
|
|
this.maxId = lastMessage.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.minId === messageId) {
|
|
|
|
|
const firstMessage = minBy(this.messages, 'id')
|
|
|
|
|
this.minId = firstMessage.id
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-07-08 17:47:50 +03:00
|
|
|
addMessages({ messages: newMessages }) {
|
|
|
|
|
for (let i = 0; i < newMessages.length; i++) {
|
|
|
|
|
const message = newMessages[i]
|
|
|
|
|
|
|
|
|
|
// Sanity check
|
|
|
|
|
if (message.chat_id !== this.chat.id) {
|
2026-07-08 20:29:59 +03:00
|
|
|
console.warn(
|
|
|
|
|
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
|
|
|
|
|
message,
|
|
|
|
|
)
|
2026-07-08 17:47:50 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear any known pending messages
|
|
|
|
|
if (message.idempotency_key) {
|
|
|
|
|
if (this.pendingMessagesIndex[message.idempotencyKeyIndex]) {
|
|
|
|
|
delete this.pendingMessagesIndex[message.idempotencyKeyIndex]
|
2026-07-08 20:29:59 +03:00
|
|
|
this.pendingMessages = this.pendingMessages.filter(
|
|
|
|
|
({ idempotency_key }) =>
|
|
|
|
|
idempotency_key !== message.idempotency_key,
|
|
|
|
|
)
|
2026-07-08 17:47:50 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
handleAttachmentPosting() {
|
2020-10-29 13:33:06 +03:00
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.handleResize()
|
|
|
|
|
// When the posting form size changes because of a media attachment, we need an extra resize
|
|
|
|
|
// to account for the potential delay in the DOM update.
|
|
|
|
|
this.scrollDown({ forceRead: true })
|
|
|
|
|
})
|
|
|
|
|
},
|
2026-07-08 17:47:50 +03:00
|
|
|
async sendMessage({ status, media, idempotencyKey }) {
|
2020-05-07 16:10:53 +03:00
|
|
|
const params = {
|
2026-07-08 17:47:50 +03:00
|
|
|
id: this.chat.id,
|
2020-10-29 13:33:06 +03:00
|
|
|
content: status,
|
2026-01-06 16:22:52 +02:00
|
|
|
idempotencyKey,
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (media[0]) {
|
|
|
|
|
params.mediaId = media[0].id
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-29 13:33:06 +03:00
|
|
|
const fakeMessage = buildFakeMessage({
|
|
|
|
|
attachments: media,
|
2026-07-08 17:47:50 +03:00
|
|
|
chatId: this.chat.id,
|
2020-10-29 13:33:06 +03:00
|
|
|
content: status,
|
|
|
|
|
userId: this.currentUser.id,
|
2026-01-06 16:22:52 +02:00
|
|
|
idempotencyKey,
|
2020-10-29 13:33:06 +03:00
|
|
|
})
|
|
|
|
|
|
2026-07-08 17:47:50 +03:00
|
|
|
this.pendingMessages.push(fakeMessage)
|
|
|
|
|
this.pendingMessagesIndex[idempotencyKey] = fakeMessage
|
|
|
|
|
|
|
|
|
|
this.handleAttachmentPosting()
|
2020-10-29 13:33:06 +03:00
|
|
|
|
2026-01-06 16:22:52 +02:00
|
|
|
return this.doSendMessage({
|
|
|
|
|
params,
|
|
|
|
|
retriesLeft: MAX_RETRIES,
|
|
|
|
|
})
|
2020-10-29 13:33:06 +03:00
|
|
|
},
|
2026-07-08 20:29:59 +03:00
|
|
|
async doSendMessage({ params, retriesLeft = MAX_RETRIES }) {
|
2020-10-29 13:33:06 +03:00
|
|
|
if (retriesLeft <= 0) return
|
|
|
|
|
|
2026-07-08 17:47:50 +03:00
|
|
|
try {
|
|
|
|
|
const { data } = await sendChatMessage({
|
|
|
|
|
...params,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
})
|
2020-05-07 16:10:53 +03:00
|
|
|
|
2026-07-08 17:47:50 +03:00
|
|
|
this.addMessages({
|
|
|
|
|
messages: [{ ...data }],
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
2026-07-08 17:47:50 +03:00
|
|
|
} catch (error) {
|
2026-07-08 20:29:59 +03:00
|
|
|
if (
|
|
|
|
|
error.name !== 'StatusCodeError' ||
|
|
|
|
|
error.message === 'Failed to fetch'
|
|
|
|
|
)
|
|
|
|
|
throw error
|
2026-07-08 17:47:50 +03:00
|
|
|
console.error('Error sending message', error)
|
|
|
|
|
|
|
|
|
|
this.handleMessageError({
|
|
|
|
|
chatId: this.chat.id,
|
2026-07-08 19:34:09 +03:00
|
|
|
idempotencyKey: params.idempotencyKey,
|
2026-07-08 17:47:50 +03:00
|
|
|
isRetry: retriesLeft !== MAX_RETRIES,
|
2020-05-07 16:10:53 +03:00
|
|
|
})
|
2020-10-29 13:33:06 +03:00
|
|
|
|
2026-07-08 17:47:50 +03:00
|
|
|
if (
|
|
|
|
|
(error.statusCode >= 500 && error.statusCode < 600) ||
|
|
|
|
|
error.message === 'Failed to fetch'
|
|
|
|
|
) {
|
2026-07-08 20:29:59 +03:00
|
|
|
this.messageRetriers[params.idempotencyKey] = setTimeout(
|
2026-07-08 17:47:50 +03:00
|
|
|
() => {
|
|
|
|
|
this.doSendMessage({
|
|
|
|
|
params,
|
|
|
|
|
retriesLeft: retriesLeft - 1,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
1000 * 2 ** (MAX_RETRIES - retriesLeft),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-07 16:10:53 +03:00
|
|
|
},
|
2026-07-08 20:29:59 +03:00
|
|
|
handleMessageError(idempotencyKey, isRetry) {
|
2026-07-08 19:34:09 +03:00
|
|
|
const fakeMessage = this.pendingMessagesIndex[idempotencyKey]
|
|
|
|
|
|
|
|
|
|
if (fakeMessage) {
|
|
|
|
|
fakeMessage.error = true
|
|
|
|
|
fakeMessage.pending = false
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-06 16:22:52 +02:00
|
|
|
goBack() {
|
|
|
|
|
this.$router.push({
|
|
|
|
|
name: 'chats',
|
|
|
|
|
params: { username: this.currentUser.screen_name },
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
2020-05-07 16:10:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Chat
|