mirror of
https://github.com/nextcloud/spreed.git
synced 2025-07-22 06:48:21 +00:00
fix(useGetMessages): extract shared methods
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
This commit is contained in:
@ -66,7 +66,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Axios from '@nextcloud/axios'
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import moment from '@nextcloud/moment'
|
||||
@ -85,10 +84,9 @@ import { useDocumentVisibility } from '../../composables/useDocumentVisibility.t
|
||||
import { useGetMessages } from '../../composables/useGetMessages.ts'
|
||||
import { useGetThreadId } from '../../composables/useGetThreadId.ts'
|
||||
import { useIsInCall } from '../../composables/useIsInCall.js'
|
||||
import { ATTENDEE, CHAT, CONVERSATION, MESSAGE } from '../../constants.ts'
|
||||
import { ATTENDEE, CONVERSATION } from '../../constants.ts'
|
||||
import { EventBus } from '../../services/EventBus.ts'
|
||||
import { useChatExtrasStore } from '../../stores/chatExtras.ts'
|
||||
import { debugTimer } from '../../utils/debugTimer.ts'
|
||||
import { convertToUnix, ONE_DAY_IN_MS } from '../../utils/formattedTime.ts'
|
||||
|
||||
const SCROLL_TOLERANCE = 10
|
||||
@ -135,15 +133,16 @@ export default {
|
||||
|
||||
setup(props) {
|
||||
const {
|
||||
pollingErrorTimeout,
|
||||
loadingOldMessages,
|
||||
isInitialisingMessages,
|
||||
destroying,
|
||||
stopFetchingOldMessages,
|
||||
isParticipant,
|
||||
isInLobby,
|
||||
chatIdentifier,
|
||||
isChatBeginningReached,
|
||||
|
||||
handleStartGettingMessagesPreconditions,
|
||||
getMessageContext,
|
||||
getOldMessages,
|
||||
pollNewMessages,
|
||||
} = useGetMessages()
|
||||
|
||||
const isDocumentVisible = useDocumentVisibility()
|
||||
@ -156,15 +155,16 @@ export default {
|
||||
isChatVisible,
|
||||
threadId,
|
||||
|
||||
pollingErrorTimeout,
|
||||
loadingOldMessages,
|
||||
isInitialisingMessages,
|
||||
destroying,
|
||||
stopFetchingOldMessages,
|
||||
isParticipant,
|
||||
isInLobby,
|
||||
chatIdentifier,
|
||||
isChatBeginningReached,
|
||||
|
||||
handleStartGettingMessagesPreconditions,
|
||||
getMessageContext,
|
||||
getOldMessages,
|
||||
pollNewMessages,
|
||||
}
|
||||
},
|
||||
|
||||
@ -271,6 +271,16 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
isInitialisingMessages(newValue, oldValue) {
|
||||
if (oldValue && !newValue) { // switching true -> false
|
||||
this.$nextTick(() => {
|
||||
// basically scrolling to either the last read message or the message in the URL anchor
|
||||
// and there is a fallback to scroll to the bottom if the message is not found
|
||||
this.scrollToFocusedMessage(this.getMessageIdFromHash())
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
chatIdentifier: {
|
||||
immediate: true,
|
||||
handler(newValue, oldValue) {
|
||||
@ -660,187 +670,6 @@ export default {
|
||||
this.debounceUpdateReadMarkerPosition()
|
||||
},
|
||||
|
||||
async handleStartGettingMessagesPreconditions(token) {
|
||||
if (token && this.isParticipant && !this.isInLobby) {
|
||||
// prevent sticky mode before we have loaded anything
|
||||
this.isInitialisingMessages = true
|
||||
const focusMessageId = this.getMessageIdFromHash()
|
||||
|
||||
this.$store.dispatch('setVisualLastReadMessageId', { token, id: this.conversation.lastReadMessage })
|
||||
|
||||
if (!this.$store.getters.getFirstKnownMessageId(token)) {
|
||||
try {
|
||||
// Start from message hash or unread marker
|
||||
let startingMessageId = focusMessageId !== null ? focusMessageId : this.conversation.lastReadMessage
|
||||
// Check if thread is initially opened
|
||||
if (this.threadId) {
|
||||
// FIXME temporary get thread messages from the start
|
||||
startingMessageId = this.threadId
|
||||
}
|
||||
|
||||
// First time load, initialize important properties
|
||||
if (!startingMessageId) {
|
||||
throw new Error(`[DEBUG] spreed: context message ID is ${startingMessageId}`)
|
||||
}
|
||||
this.$store.dispatch('setFirstKnownMessageId', { token, id: startingMessageId })
|
||||
this.$store.dispatch('setLastKnownMessageId', { token, id: startingMessageId })
|
||||
// If MESSAGE.CHAT_BEGIN_ID we need to get the context from the beginning
|
||||
// using 0 as the API does not support negative values
|
||||
// Get chat messages before last read message and after it
|
||||
await this.getMessageContext(token, startingMessageId !== MESSAGE.CHAT_BEGIN_ID ? startingMessageId : 0)
|
||||
} catch (exception) {
|
||||
console.debug(exception)
|
||||
// Request was cancelled, stop getting preconditions and restore initial state
|
||||
this.$store.dispatch('setFirstKnownMessageId', { token, id: null })
|
||||
this.$store.dispatch('setLastKnownMessageId', { token, id: null })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
// basically scrolling to either the last read message or the message in the URL anchor
|
||||
// and there is a fallback to scroll to the bottom if the message is not found
|
||||
this.scrollToFocusedMessage(focusMessageId)
|
||||
})
|
||||
|
||||
this.isInitialisingMessages = false
|
||||
|
||||
// Once the history is received, starts looking for new messages.
|
||||
await this.pollNewMessages(token)
|
||||
} else {
|
||||
this.$store.dispatch('cancelPollNewMessages', { requestId: this.chatIdentifier })
|
||||
}
|
||||
},
|
||||
|
||||
async getMessageContext(token, messageId) {
|
||||
// Make the request
|
||||
this.loadingOldMessages = true
|
||||
try {
|
||||
debugTimer.start(`${token} | get context`)
|
||||
await this.$store.dispatch('getMessageContext', {
|
||||
token,
|
||||
messageId,
|
||||
minimumVisible: CHAT.MINIMUM_VISIBLE,
|
||||
})
|
||||
debugTimer.end(`${token} | get context`, 'status 200')
|
||||
this.loadingOldMessages = false
|
||||
} catch (exception) {
|
||||
if (Axios.isCancel(exception)) {
|
||||
console.debug('The request has been canceled', exception)
|
||||
debugTimer.end(`${token} | get context`, 'cancelled')
|
||||
this.loadingOldMessages = false
|
||||
throw exception
|
||||
}
|
||||
|
||||
if (exception?.response?.status === 304 && exception?.response?.data === '') {
|
||||
// 304 - Not modified
|
||||
// Empty chat, no messages to load
|
||||
debugTimer.end(`${token} | get context`, 'status 304')
|
||||
this.$store.dispatch('loadedMessagesOfConversation', { token: this.token })
|
||||
this.stopFetchingOldMessages = true
|
||||
}
|
||||
}
|
||||
this.loadingOldMessages = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Get messages history.
|
||||
*
|
||||
* @param {boolean} includeLastKnown Include or exclude the last known message in the response
|
||||
*/
|
||||
async getOldMessages(includeLastKnown) {
|
||||
if (this.isChatBeginningReached) {
|
||||
// Beginning of the chat reached, no more messages to load
|
||||
return
|
||||
}
|
||||
// Make the request
|
||||
this.loadingOldMessages = true
|
||||
try {
|
||||
debugTimer.start(`${this.token} | fetch history`)
|
||||
await this.$store.dispatch('fetchMessages', {
|
||||
token: this.token,
|
||||
lastKnownMessageId: this.$store.getters.getFirstKnownMessageId(this.token),
|
||||
includeLastKnown,
|
||||
minimumVisible: CHAT.MINIMUM_VISIBLE,
|
||||
})
|
||||
debugTimer.end(`${this.token} | fetch history`, 'status 200')
|
||||
} catch (exception) {
|
||||
if (Axios.isCancel(exception)) {
|
||||
debugTimer.end(`${this.token} | fetch history`, 'cancelled')
|
||||
console.debug('The request has been canceled', exception)
|
||||
}
|
||||
if (exception?.response?.status === 304) {
|
||||
// 304 - Not modified
|
||||
debugTimer.end(`${this.token} | fetch history`, 'status 304')
|
||||
this.stopFetchingOldMessages = true
|
||||
}
|
||||
}
|
||||
this.loadingOldMessages = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the messages of a conversation given the conversation token.
|
||||
* Creates a long polling request for new messages.
|
||||
* @param token token of conversation where a method was called
|
||||
*/
|
||||
async pollNewMessages(token) {
|
||||
if (this.destroying) {
|
||||
console.debug('Prevent polling new messages on MessagesList being destroyed')
|
||||
return
|
||||
}
|
||||
// Check that the token has not changed
|
||||
if (this.token !== token) {
|
||||
console.debug(`token has changed to ${this.token}, breaking the loop for ${token}`)
|
||||
return
|
||||
}
|
||||
// Make the request
|
||||
try {
|
||||
debugTimer.start(`${token} | long polling`)
|
||||
// TODO: move polling logic to the store and also cancel timers on cancel
|
||||
this.pollingErrorTimeout = 1
|
||||
await this.$store.dispatch('pollNewMessages', {
|
||||
token,
|
||||
lastKnownMessageId: this.$store.getters.getLastKnownMessageId(token),
|
||||
requestId: this.chatIdentifier,
|
||||
})
|
||||
debugTimer.end(`${token} | long polling`, 'status 200')
|
||||
} catch (exception) {
|
||||
if (Axios.isCancel(exception)) {
|
||||
debugTimer.end(`${token} | long polling`, 'cancelled')
|
||||
console.debug('The request has been canceled', exception)
|
||||
return
|
||||
}
|
||||
|
||||
if (exception?.response?.status === 304) {
|
||||
debugTimer.end(`${token} | long polling`, 'status 304')
|
||||
// 304 - Not modified
|
||||
// This is not an error, so reset error timeout and poll again
|
||||
this.pollingErrorTimeout = 1
|
||||
setTimeout(() => {
|
||||
this.pollNewMessages(token)
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.pollingErrorTimeout < 30) {
|
||||
// Delay longer after each error
|
||||
this.pollingErrorTimeout += 5
|
||||
}
|
||||
|
||||
debugTimer.end(`${token} | long polling`, `status ${exception?.response?.status}`)
|
||||
console.debug('Error happened while getting chat messages. Trying again in ', this.pollingErrorTimeout, exception)
|
||||
|
||||
setTimeout(() => {
|
||||
this.pollNewMessages(token)
|
||||
}, this.pollingErrorTimeout * 1000)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.pollNewMessages(token)
|
||||
}, 500)
|
||||
},
|
||||
|
||||
checkSticky() {
|
||||
const ulElements = this.$refs['dateGroup-' + this.token]
|
||||
if (!ulElements) {
|
||||
|
@ -3,27 +3,43 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { AxiosError } from '@nextcloud/axios'
|
||||
import type {
|
||||
ChatMessage,
|
||||
Conversation,
|
||||
} from '../types/index.ts'
|
||||
|
||||
import Axios from '@nextcloud/axios'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { MESSAGE } from '../constants.ts'
|
||||
import { CHAT, MESSAGE } from '../constants.ts'
|
||||
import { debugTimer } from '../utils/debugTimer.ts'
|
||||
import { useGetThreadId } from './useGetThreadId.ts'
|
||||
import { useGetToken } from './useGetToken.ts'
|
||||
|
||||
/**
|
||||
* Check whether caught error is from OCS API
|
||||
*/
|
||||
function isAxiosErrorResponse(exception: unknown): exception is AxiosError<string> {
|
||||
return exception !== null && typeof exception === 'object' && 'response' in exception
|
||||
}
|
||||
|
||||
let isUnmounting = false
|
||||
|
||||
/**
|
||||
* Composable to provide control logic for fetching messages list
|
||||
*/
|
||||
export function useGetMessagesProvider() {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
|
||||
const currentToken = useGetToken()
|
||||
const threadId = useGetThreadId()
|
||||
const conversation = computed<Conversation | undefined>(() => store.getters.conversation(currentToken.value))
|
||||
const isInLobby = computed<boolean>(() => store.getters.isInLobby)
|
||||
|
||||
const pollingErrorTimeout = ref(1)
|
||||
const destroying = ref(false)
|
||||
|
||||
const loadingOldMessages = ref(false)
|
||||
const isInitialisingMessages = ref(false)
|
||||
@ -51,16 +67,205 @@ export function useGetMessagesProvider() {
|
||||
&& ['conversation_created', 'history_cleared'].includes(firstKnownMessage.value.systemMessage))
|
||||
})
|
||||
|
||||
/**
|
||||
* Initialize chat context borders and start fetching messages
|
||||
* @param token token of conversation where a method was called
|
||||
*/
|
||||
async function handleStartGettingMessagesPreconditions(token: string) {
|
||||
if (token && isParticipant.value && !isInLobby.value) {
|
||||
// prevent sticky mode before we have loaded anything
|
||||
isInitialisingMessages.value = true
|
||||
const focusMessageId = route?.hash?.startsWith('#message_') ? parseInt(route.hash.slice(9), 10) : null
|
||||
|
||||
store.dispatch('setVisualLastReadMessageId', { token, id: conversation.value!.lastReadMessage })
|
||||
|
||||
if (!store.getters.getFirstKnownMessageId(token)) {
|
||||
try {
|
||||
// Start from message hash or unread marker
|
||||
let startingMessageId = focusMessageId !== null ? focusMessageId : conversation.value!.lastReadMessage
|
||||
// Check if thread is initially opened
|
||||
if (threadId.value) {
|
||||
// FIXME temporary get thread messages from the start
|
||||
startingMessageId = threadId.value
|
||||
}
|
||||
|
||||
// First time load, initialize important properties
|
||||
if (!startingMessageId) {
|
||||
throw new Error(`[DEBUG] spreed: context message ID is ${startingMessageId}`)
|
||||
}
|
||||
store.dispatch('setFirstKnownMessageId', { token, id: startingMessageId })
|
||||
store.dispatch('setLastKnownMessageId', { token, id: startingMessageId })
|
||||
// If MESSAGE.CHAT_BEGIN_ID we need to get the context from the beginning
|
||||
// using 0 as the API does not support negative values
|
||||
// Get chat messages before last read message and after it
|
||||
await getMessageContext(token, startingMessageId !== MESSAGE.CHAT_BEGIN_ID ? startingMessageId : 0)
|
||||
} catch (exception) {
|
||||
console.debug(exception)
|
||||
// Request was cancelled, stop getting preconditions and restore initial state
|
||||
store.dispatch('setFirstKnownMessageId', { token, id: null })
|
||||
store.dispatch('setLastKnownMessageId', { token, id: null })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isInitialisingMessages.value = false
|
||||
|
||||
// Once the history is received, starts looking for new messages.
|
||||
await pollNewMessages(token)
|
||||
} else {
|
||||
store.dispatch('cancelPollNewMessages', { requestId: chatIdentifier.value })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the messages of a conversation given the conversation token.
|
||||
* Creates a long polling request for new messages.
|
||||
* @param token token of conversation where a method was called
|
||||
* @param messageId messageId
|
||||
*/
|
||||
async function getMessageContext(token: string, messageId: number) {
|
||||
// Make the request
|
||||
loadingOldMessages.value = true
|
||||
try {
|
||||
debugTimer.start(`${token} | get context`)
|
||||
await store.dispatch('getMessageContext', {
|
||||
token,
|
||||
messageId,
|
||||
minimumVisible: CHAT.MINIMUM_VISIBLE,
|
||||
})
|
||||
debugTimer.end(`${token} | get context`, 'status 200')
|
||||
loadingOldMessages.value = false
|
||||
} catch (exception) {
|
||||
if (Axios.isCancel(exception)) {
|
||||
console.debug('The request has been canceled', exception)
|
||||
debugTimer.end(`${token} | get context`, 'cancelled')
|
||||
loadingOldMessages.value = false
|
||||
throw exception
|
||||
}
|
||||
|
||||
if (isAxiosErrorResponse(exception) && exception.response?.status === 304) {
|
||||
// 304 - Not modified
|
||||
// Empty chat, no messages to load
|
||||
debugTimer.end(`${token} | get context`, 'status 304')
|
||||
store.dispatch('loadedMessagesOfConversation', { token })
|
||||
|
||||
stopFetchingOldMessages.value = true
|
||||
}
|
||||
}
|
||||
loadingOldMessages.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages history.
|
||||
*
|
||||
* @param includeLastKnown Include or exclude the last known message in the response
|
||||
*/
|
||||
async function getOldMessages(includeLastKnown: boolean) {
|
||||
if (isChatBeginningReached.value) {
|
||||
// Beginning of the chat reached, no more messages to load
|
||||
return
|
||||
}
|
||||
// Make the request
|
||||
loadingOldMessages.value = true
|
||||
try {
|
||||
debugTimer.start(`${currentToken.value} | fetch history`)
|
||||
await store.dispatch('fetchMessages', {
|
||||
token: currentToken.value,
|
||||
lastKnownMessageId: store.getters.getFirstKnownMessageId(currentToken.value),
|
||||
includeLastKnown,
|
||||
minimumVisible: CHAT.MINIMUM_VISIBLE,
|
||||
})
|
||||
debugTimer.end(`${currentToken.value} | fetch history`, 'status 200')
|
||||
} catch (exception) {
|
||||
if (Axios.isCancel(exception)) {
|
||||
debugTimer.end(`${currentToken.value} | fetch history`, 'cancelled')
|
||||
console.debug('The request has been canceled', exception)
|
||||
}
|
||||
if (isAxiosErrorResponse(exception) && exception?.response?.status === 304) {
|
||||
// 304 - Not modified
|
||||
debugTimer.end(`${currentToken.value} | fetch history`, 'status 304')
|
||||
stopFetchingOldMessages.value = true
|
||||
}
|
||||
}
|
||||
loadingOldMessages.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the messages of a conversation given the conversation token.
|
||||
* Creates a long polling request for new messages.
|
||||
* @param token token of conversation where a method was called
|
||||
*/
|
||||
async function pollNewMessages(token: string) {
|
||||
if (isUnmounting) {
|
||||
console.debug('Prevent polling new messages on MessagesList being destroyed')
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the token has not changed
|
||||
if (currentToken.value !== token) {
|
||||
console.debug(`token has changed to ${currentToken.value}, breaking the loop for ${token}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Make the request
|
||||
try {
|
||||
debugTimer.start(`${token} | long polling`)
|
||||
// TODO: move polling logic to the store and also cancel timers on cancel
|
||||
pollingErrorTimeout.value = 1
|
||||
await store.dispatch('pollNewMessages', {
|
||||
token,
|
||||
lastKnownMessageId: store.getters.getLastKnownMessageId(token),
|
||||
requestId: chatIdentifier.value,
|
||||
})
|
||||
debugTimer.end(`${token} | long polling`, 'status 200')
|
||||
} catch (exception) {
|
||||
if (Axios.isCancel(exception)) {
|
||||
debugTimer.end(`${token} | long polling`, 'cancelled')
|
||||
console.debug('The request has been canceled', exception)
|
||||
return
|
||||
}
|
||||
|
||||
if (isAxiosErrorResponse(exception) && exception?.response?.status === 304) {
|
||||
debugTimer.end(`${token} | long polling`, 'status 304')
|
||||
// 304 - Not modified
|
||||
// This is not an error, so reset error timeout and poll again
|
||||
pollingErrorTimeout.value = 1
|
||||
setTimeout(() => {
|
||||
pollNewMessages(token)
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
if (pollingErrorTimeout.value < 30) {
|
||||
// Delay longer after each error
|
||||
pollingErrorTimeout.value += 5
|
||||
}
|
||||
|
||||
debugTimer.end(`${token} | long polling`, `status ${isAxiosErrorResponse(exception) ? exception?.response?.status : 'unknown'}`)
|
||||
console.debug('Error happened while getting chat messages. Trying again in ', pollingErrorTimeout.value, exception)
|
||||
|
||||
setTimeout(() => {
|
||||
pollNewMessages(token)
|
||||
}, pollingErrorTimeout.value * 1000)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
pollNewMessages(token)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
return {
|
||||
pollingErrorTimeout,
|
||||
loadingOldMessages,
|
||||
isInitialisingMessages,
|
||||
destroying,
|
||||
stopFetchingOldMessages,
|
||||
isParticipant,
|
||||
isInLobby,
|
||||
chatIdentifier,
|
||||
isChatBeginningReached,
|
||||
|
||||
handleStartGettingMessagesPreconditions,
|
||||
getMessageContext,
|
||||
getOldMessages,
|
||||
pollNewMessages,
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user