Merge pull request #15519 from nextcloud/fix/noid/thread-poll-message

fix(threads): add instant update for threads list
This commit is contained in:
Dorra
2025-07-21 11:16:15 +02:00
committed by GitHub
4 changed files with 130 additions and 19 deletions

View File

@ -117,6 +117,9 @@ function isExistingMessage(message: ChatMessage | DeletedParentMessage): message
return 'messageType' in message
}
/**
* Go to thread if it exists
*/
function goToThread() {
if (isExistingMessage(message) && message.threadId) {
threadId.value = message.threadId

View File

@ -17,6 +17,7 @@ import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcListItem from '@nextcloud/vue/components/NcListItem'
import IconArrowLeftTop from 'vue-material-design-icons/ArrowLeftTop.vue'
import IconBellOutline from 'vue-material-design-icons/BellOutline.vue'
import IconCommentAlertOutline from 'vue-material-design-icons/CommentAlertOutline.vue'
import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue'
import { getDisplayNameWithFallback } from '../../../utils/getDisplayName.ts'
import { parseToSimpleMessage } from '../../../utils/textParse.ts'
@ -26,9 +27,19 @@ const { thread } = defineProps<{ thread: ThreadInfo }>()
const router = useRouter()
const route = useRoute()
const threadAuthor = computed(() => getDisplayNameWithFallback(thread.first.actorDisplayName, thread.first.actorType, true))
const threadAuthor = computed(() => {
if (!thread.first) {
return
}
return getDisplayNameWithFallback(thread.first.actorDisplayName, thread.first.actorType, true)
})
const lastActivity = computed(() => thread.thread.lastActivity * 1000)
const name = computed(() => parseToSimpleMessage(thread.first.message, thread.first.messageParameters))
const name = computed(() => {
if (!thread.first) {
return t('spreed', 'Thread origin message expired')
}
return parseToSimpleMessage(thread.first.message, thread.first.messageParameters)
})
const subname = computed(() => {
if (!thread.last) {
return t('spreed', 'No messages')
@ -72,14 +83,22 @@ const timeFormat = computed<Intl.DateTimeFormatOptions>(() => {
force-menu>
<template #icon>
<AvatarWrapper
v-if="thread.first"
:id="thread.first.actorId"
:name="thread.first.actorDisplayName"
:source="thread.first.actorType"
disable-menu
:token="thread.thread.roomToken" />
<IconCommentAlertOutline
v-else
:size="20" />
</template>
<template #name>
<span class="thread__author">{{ threadAuthor }}</span>
<span
v-if="threadAuthor"
class="thread__author">
{{ threadAuthor }}
</span>
<span>{{ name }}</span>
</template>
<template #subname>

View File

@ -26,6 +26,7 @@ import {
} from '../services/messagesService.ts'
import { useActorStore } from '../stores/actor.ts'
import { useCallViewStore } from '../stores/callView.ts'
import { useChatExtrasStore } from '../stores/chatExtras.ts'
import { useGuestNameStore } from '../stores/guestName.js'
import { usePollsStore } from '../stores/polls.ts'
import { useReactionsStore } from '../stores/reactions.js'
@ -298,6 +299,10 @@ const mutations = {
const preparedMessage = !message.parent && storedMessage?.parent
? { ...message, parent: storedMessage.parent }
: message
if (preparedMessage.parent) {
preparedMessage.parent.isThread = preparedMessage.isThread
}
state.messages[token][message.id] = preparedMessage
},
/**
@ -475,21 +480,6 @@ const mutations = {
}
},
removeExpiredMessages(state, { token }) {
if (!state.messages[token]) {
return
}
const timestamp = convertToUnix(Date.now())
const messageIds = Object.keys(state.messages[token])
messageIds.forEach((messageId) => {
if (state.messages[token][messageId].expirationTimestamp
&& timestamp > state.messages[token][messageId].expirationTimestamp) {
delete state.messages[token][messageId]
}
})
},
easeMessageList(state, { token, lastReadMessage }) {
if (!state.messages[token]) {
return
@ -544,6 +534,7 @@ const actions = {
processMessage(context, { token, message }) {
const sharedItemsStore = useSharedItemsStore()
const actorStore = useActorStore()
const chatExtrasStore = useChatExtrasStore()
if (message.systemMessage === 'message_deleted'
|| message.systemMessage === 'reaction'
@ -574,6 +565,21 @@ const actions = {
if (message.parent.id === context.getters.conversation(token).lastMessage?.id) {
context.dispatch('updateConversationLastMessage', { token, lastMessage: message.parent })
}
const thread = chatExtrasStore.getThread(token, message.parent.threadId)
// update threads, if it is the first or the last message in the thread
if (thread && (thread.last?.id === message.parent.id || thread.first?.id === message.parent.id)) {
const updatedData = {
thread: {
...thread.thread,
lastActivity: message.parent.timestamp,
},
first: (thread.first?.id === message.parent.id) ? message.parent : undefined,
last: (thread.last?.id === message.parent.id) ? message.parent : undefined,
}
chatExtrasStore.updateThread(token, message.parent.threadId, updatedData)
}
// Check existing messages for having a deleted / edited message as parent, and update them
context.getters.messagesList(token)
.filter((storedMessage) => storedMessage.parent?.id === message.parent.id && JSON.stringify(storedMessage.parent) !== JSON.stringify(message.parent))
@ -589,6 +595,10 @@ const actions = {
.forEach((storedMessage) => {
context.commit('addMessage', { token, message: Object.assign({}, storedMessage, { isThread: true }) })
})
// Fetch thread data in case it doesn't exist in the store yet
if (!chatExtrasStore.getThread(token, message.threadId) && chatExtrasStore.threads[token] !== undefined) {
chatExtrasStore.fetchSingleThread(token, message.threadId)
}
}
// Quit processing
@ -639,6 +649,7 @@ const actions = {
if (message.systemMessage === 'history_cleared') {
sharedItemsStore.purgeSharedItemsStore(token, message.id)
chatExtrasStore.clearThreads(token, message.id)
context.commit('clearMessagesHistory', {
token,
id: message.id,
@ -647,6 +658,24 @@ const actions = {
context.commit('addMessage', { token, message })
// Update threads
if (message.isThread) {
const thread = chatExtrasStore.getThread(token, message.threadId)
if (thread) {
const updateNumReplies = thread.thread.lastMessageId < message.id
chatExtrasStore.updateThread(message.token, message.threadId, {
thread: {
id: message.threadId,
roomToken: message.token,
lastMessageId: message.id,
lastActivity: message.timestamp,
numReplies: thread.thread.numReplies + (updateNumReplies ? 1 : 0),
},
last: message,
})
}
}
if (message.messageParameters && [MESSAGE.TYPE.COMMENT, MESSAGE.TYPE.VOICE_MESSAGE, MESSAGE.TYPE.RECORD_AUDIO, MESSAGE.TYPE.RECORD_VIDEO].includes(message.messageType)) {
if (message.messageParameters?.object || message.messageParameters?.file) {
// Handle voice messages, shares with single file, polls, deck cards, e.t.c
@ -1416,7 +1445,20 @@ const actions = {
},
async removeExpiredMessages(context, { token }) {
context.commit('removeExpiredMessages', { token })
if (!context.state.messages[token]) {
return
}
const chatExtrasStore = useChatExtrasStore()
const timestamp = convertToUnix(Date.now())
context.getters.messagesList(token).forEach((message) => {
if (message.expirationTimestamp && timestamp > message.expirationTimestamp) {
if (message.isThread) {
chatExtrasStore.removeMessageFromThread(token, message.threadId, message.id)
}
context.commit('deleteMessage', { token, id: message.id })
}
})
},
async easeMessageList(context, { token }) {

View File

@ -177,6 +177,52 @@ export const useChatExtrasStore = defineStore('chatExtras', {
}
},
/**
* Remove a thread from the store
*
* @param token - conversation token
* @param messageId - message id to remove all preceding threads (remove all, if omitted)
*/
clearThreads(token: string, messageId?: number) {
if (messageId) {
// Clear threads that are older than the given messageId
for (const threadId of Object.keys(Object(this.threads[token]))) {
if (+threadId < messageId) {
delete this.threads[token][+threadId]
}
}
} else {
// Clear all threads for the conversation
delete this.threads[token]
}
},
/**
* Remove a message from a thread object
*
* @param token - conversation token
* @param threadId - thread id to remove message from
* @param messageId - message id to remove
*/
removeMessageFromThread(token: string, threadId: number, messageId: number) {
if (!this.threads[token]?.[threadId]) {
return
}
const thread = this.threads[token][threadId]
if (thread.first.id === messageId) {
// @ts-expect-error - missing null type in ThreadInfo
thread.first = null
} else {
this.threads[token][threadId].thread.numReplies -= 1
if (thread.last.id === messageId) {
// Last message was removed but there might be older messages in the thread
// that don't have expiration timestamp
this.fetchSingleThread(token, threadId)
}
}
},
/**
* Get chat input for current conversation (from store or BrowserStorage)
*
@ -318,6 +364,7 @@ export const useChatExtrasStore = defineStore('chatExtras', {
purgeChatExtras(token: string) {
this.removeParentIdToReply(token)
this.removeChatInput(token)
this.clearThreads(token)
},
setTasksCounters({ tasksCount, tasksDoneCount }: { tasksCount: number, tasksDoneCount: number }) {