diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue index 16a21a69b3e..e1fc7db3351 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/MessageBody.vue @@ -307,7 +307,12 @@ export default { }, isThreadStarterMessage() { - return !this.threadId && this.message.isThread && this.message.id === this.message.threadId + if (this.threadId || !this.message.isThread) { + return false + } + + return this.message.id === this.message.threadId + || (this.message.threadTitle && this.message.id.toString().startsWith('temp-')) }, threadInfo() { @@ -315,12 +320,13 @@ export default { }, threadTitle() { - return this.threadInfo?.thread.title ?? this.message.message + return this.threadInfo?.thread.title ?? this.message.threadTitle }, threadNumReplies() { - return this.threadInfo?.thread.numReplies - ? n('spreed', '%n reply', '%n replies', this.threadInfo.thread.numReplies) + const numReplies = this.threadInfo?.thread.numReplies ?? this.message.threadReplies + return numReplies + ? n('spreed', '%n reply', '%n replies', numReplies) : t('spreed', 'Reply') }, diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index bb50fc222f3..a65c94c4f3a 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -757,17 +757,27 @@ export default { this.chatExtrasStore.removeChatInput(this.token) if (this.hasText || (this.dialog && this.upload)) { - const message = this.text.trim() - // Substitute thread title with message text, if missing - const threadTitle = this.threadCreating - ? this.threadTitle.trim() - : undefined - - const temporaryMessage = this.createTemporaryMessage({ - message, + const temporaryMessagePayload = { + message: this.text.trim(), token: this.token, silent: this.silentChat, - }) + } + + if (this.threadId) { + temporaryMessagePayload.threadId = this.threadId + temporaryMessagePayload.isThread = true + } + if (this.parentMessage) { + temporaryMessagePayload.parent = this.parentMessage + } + if (this.threadCreating) { + // Substitute thread title with message text, if missing + temporaryMessagePayload.threadTitle = this.threadTitle.trim() + temporaryMessagePayload.threadReplies = 0 + temporaryMessagePayload.isThread = true + } + + const temporaryMessage = this.createTemporaryMessage(temporaryMessagePayload) this.text = '' this.chatExtrasStore.removeThreadTitle(this.token) @@ -779,24 +789,24 @@ export default { this.chatExtrasStore.removeParentIdToReply(this.token) this.dialog - ? await this.submitMessage(this.token, temporaryMessage, threadTitle) - : await this.postMessage(this.token, temporaryMessage, threadTitle) + ? await this.submitMessage(this.token, temporaryMessage) + : await this.postMessage(this.token, temporaryMessage) this.resetTypingIndicator() } }, // Post message to conversation - async postMessage(token, temporaryMessage, threadTitle) { + async postMessage(token, temporaryMessage) { try { - await this.$store.dispatch('postNewMessage', { token, temporaryMessage, threadTitle }) + await this.$store.dispatch('postNewMessage', { token, temporaryMessage }) } catch (e) { console.error(e) } }, // Broadcast message to all breakout rooms - async submitMessage(token, temporaryMessage, threadTitle) { - this.$emit('submit', { token, temporaryMessage, threadTitle }) + async submitMessage(token, temporaryMessage) { + this.$emit('submit', { token, temporaryMessage }) }, async handleSubmitSpam(numberOfMessages) { diff --git a/src/components/NewMessage/NewMessageUploadEditor.vue b/src/components/NewMessage/NewMessageUploadEditor.vue index 58cf2017821..ef81d21c91b 100644 --- a/src/components/NewMessage/NewMessageUploadEditor.vue +++ b/src/components/NewMessage/NewMessageUploadEditor.vue @@ -199,7 +199,7 @@ export default { }) }, - async handleUpload({ token, temporaryMessage, threadTitle }) { + async handleUpload({ token, temporaryMessage }) { if (this.files.length) { // Create a share with optional caption await this.$store.dispatch('uploadFiles', { @@ -207,7 +207,7 @@ export default { uploadId: this.currentUploadId, caption: temporaryMessage.message, options: { - threadTitle, + threadTitle: temporaryMessage.threadTitle, silent: temporaryMessage.silent, }, }) @@ -216,7 +216,7 @@ export default { if (temporaryMessage.message.trim()) { // Proceed as a normal message try { - await this.$store.dispatch('postNewMessage', { token, temporaryMessage, threadTitle }) + await this.$store.dispatch('postNewMessage', { token, temporaryMessage }) } catch (e) { console.error(e) } diff --git a/src/components/RightSidebar/BreakoutRooms/BreakoutRoomItem.vue b/src/components/RightSidebar/BreakoutRooms/BreakoutRoomItem.vue index 4fcae50b9f2..94c26c4bbaa 100644 --- a/src/components/RightSidebar/BreakoutRooms/BreakoutRoomItem.vue +++ b/src/components/RightSidebar/BreakoutRooms/BreakoutRoomItem.vue @@ -234,9 +234,9 @@ export default { this.showParticipants = !this.showParticipants }, - async sentMessageToRoom({ token, temporaryMessage, threadTitle, options }) { + async sentMessageToRoom({ token, temporaryMessage, options }) { try { - await this.$store.dispatch('postNewMessage', { token, temporaryMessage, threadTitle, options }) + await this.$store.dispatch('postNewMessage', { token, temporaryMessage, options }) showSuccess(t('spreed', 'The message was sent to "{roomName}"', { roomName: this.roomName })) this.isDialogOpened = false } catch (e) { diff --git a/src/composables/useTemporaryMessage.ts b/src/composables/useTemporaryMessage.ts index 3c757bc9d6b..2fd476c339f 100644 --- a/src/composables/useTemporaryMessage.ts +++ b/src/composables/useTemporaryMessage.ts @@ -3,43 +3,26 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Store } from 'vuex' import type { PrepareTemporaryMessagePayload } from '../utils/prepareTemporaryMessage.ts' -import { useStore } from 'vuex' import { useActorStore } from '../stores/actor.ts' -import { useChatExtrasStore } from '../stores/chatExtras.ts' import { prepareTemporaryMessage } from '../utils/prepareTemporaryMessage.ts' -import { useGetThreadId } from './useGetThreadId.ts' /** * Composable to generate temporary messages using defined in store information - * - * @param context Vuex Store (to be used inside Vuex modules) */ -export function useTemporaryMessage(context: Store) { - const store = context ?? useStore() - const chatExtrasStore = useChatExtrasStore() +export function useTemporaryMessage() { const actorStore = useActorStore() - const threadId = useGetThreadId() /** * @param payload payload for generating a temporary message */ function createTemporaryMessage(payload: PrepareTemporaryMessagePayload) { - const parentId = chatExtrasStore.getParentIdToReply(payload.token) - const parent = parentId - ? store.getters.message(payload.token, parentId) - : (threadId.value ? chatExtrasStore.getThread(payload.token, threadId.value)?.first : undefined) - return prepareTemporaryMessage({ ...payload, actorId: actorStore.actorId ?? '', actorType: actorStore.actorType ?? '', actorDisplayName: actorStore.displayName, - parent, - threadId: threadId.value ? threadId.value : undefined, - isThread: threadId.value ? true : undefined, }) } diff --git a/src/services/messagesService.ts b/src/services/messagesService.ts index af029c20fb4..1a0b9360cad 100644 --- a/src/services/messagesService.ts +++ b/src/services/messagesService.ts @@ -139,7 +139,8 @@ async function getMessageContext({ token, messageId, threadId, limit = 50 }: Get * @param payload.referenceId A reference id to identify the message later again * @param payload.replyTo The message id to be replied to * @param payload.silent whether the message should trigger a notifications - * @param payload.threadTitle + * @param payload.threadId The thread id to post the message in + * @param payload.threadTitle The thread title to set when creating a new thread * @param [options] Axios request options */ async function postNewMessage({ @@ -149,6 +150,7 @@ async function postNewMessage({ referenceId, replyTo, silent, + threadId, threadTitle, }: postNewMessageParams & { token: string }, options?: AxiosRequestConfig): postNewMessageResponse { return axios.post(generateOcsUrl('apps/spreed/api/v1/chat/{token}', { token }), { @@ -157,6 +159,7 @@ async function postNewMessage({ referenceId, replyTo, silent, + threadId, threadTitle, } as postNewMessageParams, options) } diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index 410064e6592..ee3bc06a771 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -521,12 +521,6 @@ const actions = { } if (message.systemMessage === MESSAGE.SYSTEM_TYPE.THREAD_CREATED) { - // Check existing messages for having a threadId flag, and update them - context.getters.messagesList(token) - .filter((storedMessage) => storedMessage.threadId === message.threadId) - .forEach((storedMessage) => { - context.commit('addMessage', { token, message: { ...storedMessage, isThread: true } }) - }) // Fetch thread data in case it doesn't exist in the store yet if (!chatExtrasStore.getThread(token, message.threadId)) { chatExtrasStore.fetchSingleThread(token, message.threadId) @@ -534,7 +528,7 @@ const actions = { } if (message.systemMessage === MESSAGE.SYSTEM_TYPE.THREAD_RENAMED) { - chatExtrasStore.updateThreadTitle(token, message.threadId, message.messageParameters.title.name) + chatExtrasStore.updateThreadTitle(token, message.threadId, message.threadTitle) } // Quit processing @@ -588,16 +582,25 @@ const actions = { // Update threads if (message.isThread) { const thread = chatExtrasStore.getThread(token, message.threadId) - if (thread && thread.thread.lastMessageId < message.id) { - chatExtrasStore.updateThread(message.token, message.threadId, { + + if (!thread) { + chatExtrasStore.fetchSingleThread(token, message.threadId) + } else if (thread.thread.title !== message.threadTitle + || thread.thread.numReplies !== message.threadReplies + || thread.thread.lastMessageId < message.id) { + const updatePayload = { thread: { ...thread.thread, - lastMessageId: message.id, - lastActivity: message.timestamp, - numReplies: thread.thread.numReplies + 1, + title: message.threadTitle, + numReplies: message.threadReplies, }, - last: message, - }) + } + if (thread && thread.thread.lastMessageId < message.id) { + updatePayload.thread.lastMessageId = message.id + updatePayload.thread.lastActivity = message.timestamp + updatePayload.last = message + } + chatExtrasStore.updateThread(message.token, message.threadId, updatePayload) } } @@ -1193,10 +1196,9 @@ const actions = { * @param {object} data Passed in parameters * @param {string} data.token token of the conversation * @param {object} data.temporaryMessage temporary message, must already have been added to messages list. - * @param {object} data.threadTitle if given, creates a thread with that title * @param {object} data.options post request options. */ - async postNewMessage(context, { token, temporaryMessage, threadTitle, options }) { + async postNewMessage(context, { token, temporaryMessage, options }) { context.dispatch('addTemporaryMessage', { token, message: temporaryMessage }) const { request, cancel } = CancelableRequest(postNewMessage) @@ -1225,9 +1227,9 @@ const actions = { actorDisplayName: temporaryMessage.actorDisplayName, referenceId: temporaryMessage.referenceId, replyTo: temporaryMessage.parent?.id, - // FIXME threadId: temporaryMessage.threadId, PR #15645 + threadId: temporaryMessage.threadId, silent: temporaryMessage.silent, - threadTitle, + threadTitle: temporaryMessage.threadTitle, }, options) clearTimeout(timeout) context.commit('setCancelPostNewMessage', { messageId: temporaryMessage.id, cancelFunction: null }) diff --git a/src/store/messagesStore.spec.js b/src/store/messagesStore.spec.js index cbaea3cefb9..c7833c716ba 100644 --- a/src/store/messagesStore.spec.js +++ b/src/store/messagesStore.spec.js @@ -22,6 +22,7 @@ import { editMessage, fetchMessages, getMessageContext, + getSingleThreadForConversation, pollNewMessages, postNewMessage, postRichObjectToConversation, @@ -41,6 +42,7 @@ vi.mock('../services/messagesService', () => ({ editMessage: vi.fn(), updateLastReadMessage: vi.fn(), fetchMessages: vi.fn(), + getSingleThreadForConversation: vi.fn(), getMessageContext: vi.fn(), pollNewMessages: vi.fn(), postNewMessage: vi.fn(), @@ -1042,7 +1044,15 @@ describe('messagesStore', () => { }, payload: messagesContext, }) + const getThreadResponse = generateOCSResponse({ + payload: { + thread: { id: 3 }, + first: messagesContext[0], + last: messagesContext[1], + }, + }) getMessageContext.mockResolvedValueOnce(responseContext) + getSingleThreadForConversation.mockResolvedValueOnce(getThreadResponse) const responseFetch = generateOCSResponse({ headers: { diff --git a/src/stores/__tests__/chat.spec.js b/src/stores/__tests__/chat.spec.js index ea6a6733570..c5ddf2b3255 100644 --- a/src/stores/__tests__/chat.spec.js +++ b/src/stores/__tests__/chat.spec.js @@ -7,6 +7,8 @@ import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createStore, useStore } from 'vuex' import storeConfig from '../../store/storeConfig.js' +import { generateOCSResponse } from '../../test-helpers.js' +import { convertToUnix } from '../../utils/formattedTime.ts' import { useChatStore } from '../chat.ts' vi.mock('vuex', async () => { @@ -17,6 +19,26 @@ vi.mock('vuex', async () => { } }) +vi.mock('../../services/messagesService', () => ({ + getSingleThreadForConversation: vi.fn((roomToken, id) => { + return generateOCSResponse({ + payload: { + thread: { + id, + roomToken, + title: 'title', + lastMessageId: id, + lastActivity: convertToUnix(Date.now()), + numReplies: 0, + }, + attendee: { notificationLevel: 0 }, + first: null, + last: null, + }, + }) + }), +})) + describe('chatStore', () => { const TOKEN = 'XXTOKENXX' let chatStore diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 6d02ba3ca21..20add63a0a4 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -115,7 +115,7 @@ export const useChatStore = defineStore('chat', () => { if (message && !isHiddenSystemMessage(message) && (threadId ? threadId === message.threadId - : (!message.isThread || message.id === message.threadId) + : (!message.isThread || message.id === message.threadId || message.id.toString().startsWith('temp-')) ) ) { acc.push(message) diff --git a/src/stores/chatExtras.ts b/src/stores/chatExtras.ts index 704e4f6c78d..5403cbcda45 100644 --- a/src/stores/chatExtras.ts +++ b/src/stores/chatExtras.ts @@ -40,6 +40,8 @@ type State = { chatSummary: Record> } +const pendingFetchSingleThreadRequests = new Set() + /** * Store for conversation extra chat features apart from messages */ @@ -138,11 +140,19 @@ export const useChatExtrasStore = defineStore('chatExtras', { * @param threadId - thread id to fetch */ async fetchSingleThread(token: string, threadId: number) { + if (pendingFetchSingleThreadRequests.has(threadId)) { + // A request for this thread is already pending + return + } + try { + pendingFetchSingleThreadRequests.add(threadId) const response = await getSingleThreadForConversation(token, threadId) this.addThread(token, response.data.ocs.data) } catch (error) { console.error('Error fetching thread:', error) + } finally { + pendingFetchSingleThreadRequests.delete(threadId) } }, diff --git a/src/utils/__tests__/prepareTemporaryMessage.spec.js b/src/utils/__tests__/prepareTemporaryMessage.spec.js index 87282e1629f..ca4fda19025 100644 --- a/src/utils/__tests__/prepareTemporaryMessage.spec.js +++ b/src/utils/__tests__/prepareTemporaryMessage.spec.js @@ -45,6 +45,8 @@ describe('prepareTemporaryMessage', () => { silent: false, threadId: undefined, isThread: undefined, + threadReplies: undefined, + threadTitle: undefined, } const parent = { diff --git a/src/utils/prepareTemporaryMessage.ts b/src/utils/prepareTemporaryMessage.ts index 6ae9e3e2649..497bf31374a 100644 --- a/src/utils/prepareTemporaryMessage.ts +++ b/src/utils/prepareTemporaryMessage.ts @@ -19,6 +19,8 @@ export type PrepareTemporaryMessagePayload = Pick & { uploadId: string index: number @@ -62,6 +64,8 @@ export function prepareTemporaryMessage({ parent, silent = false, threadId, + threadTitle, + threadReplies, isThread, }: PrepareTemporaryMessagePayload): ChatMessage { const date = new Date() @@ -102,7 +106,9 @@ export function prepareTemporaryMessage({ actorType, actorDisplayName, silent, - threadId: threadId || undefined, + threadId, + threadTitle, + threadReplies, isThread, } }