Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -307,20 +307,26 @@ 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() {
return this.chatExtrasStore.getThread(this.message.token, this.message.threadId)
},

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')
},

Expand Down
40 changes: 25 additions & 15 deletions src/components/NewMessage/NewMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/NewMessage/NewMessageUploadEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ 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', {
token,
uploadId: this.currentUploadId,
caption: temporaryMessage.message,
options: {
threadTitle,
threadTitle: temporaryMessage.threadTitle,
silent: temporaryMessage.silent,
},
})
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 1 addition & 18 deletions src/composables/useTemporaryMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>) {
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,
})
}

Expand Down
5 changes: 4 additions & 1 deletion src/services/messagesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 }), {
Expand All @@ -157,6 +159,7 @@ async function postNewMessage({
referenceId,
replyTo,
silent,
threadId,
threadTitle,
} as postNewMessageParams, options)
}
Expand Down
38 changes: 20 additions & 18 deletions src/store/messagesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,20 +521,14 @@ 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)
}
}

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
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 })
Expand Down
10 changes: 10 additions & 0 deletions src/store/messagesStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
editMessage,
fetchMessages,
getMessageContext,
getSingleThreadForConversation,
pollNewMessages,
postNewMessage,
postRichObjectToConversation,
Expand All @@ -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(),
Expand Down Expand Up @@ -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: {
Expand Down
22 changes: 22 additions & 0 deletions src/stores/__tests__/chat.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions src/stores/chatExtras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type State = {
chatSummary: Record<string, Record<number, ChatTask>>
}

const pendingFetchSingleThreadRequests = new Set<number>()

/**
* Store for conversation extra chat features apart from messages
*/
Expand Down Expand Up @@ -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)
}
},

Expand Down
Loading