From ba07792402f0948ed15d66d950ad01d30a0a06e9 Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Sat, 28 Mar 2026 12:05:17 +0100 Subject: [PATCH 1/6] feat(reply_private): API level changes Signed-off-by: existentialcoder --- docs/capabilities.md | 1 + lib/AppInfo/Application.php | 2 + lib/Capabilities.php | 1 + lib/Chat/ChatManager.php | 7 +- lib/Chat/Parser/PrivateReply.php | 39 ++++++++ lib/Controller/ChatController.php | 142 ++++++++++++++++++++++++------ lib/Model/Message.php | 1 + lib/ResponseDefinitions.php | 8 ++ lib/Signaling/Listener.php | 11 ++- 9 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 lib/Chat/Parser/PrivateReply.php diff --git a/docs/capabilities.md b/docs/capabilities.md index 1a173953c60..b217e6adb7b 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -224,3 +224,4 @@ * `config => feature-hints => hidden` (local) - Number of the last hint the administration has hidden via the app config * `config => conversations => sort-order` (local) - User selected sort order for conversations (`activity` or `alphabetical`) * `config => conversations => group-mode` (local) - User selected grouping mode for conversations (`none`, `group-first` or `private-first`) +* `private-reply` - Whether clients can link the original message to a private reply in one-to-one conversations diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 41816ab9690..1692c20c773 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -20,6 +20,7 @@ use OCA\Talk\Chat\Changelog\Listener as ChangelogListener; use OCA\Talk\Chat\Listener as ChatListener; use OCA\Talk\Chat\Parser\Changelog; +use OCA\Talk\Chat\Parser\PrivateReply; use OCA\Talk\Chat\Parser\ReactionParser; use OCA\Talk\Chat\Parser\SystemMessage; use OCA\Talk\Chat\Parser\UserMention; @@ -235,6 +236,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(MessageParseEvent::class, Changelog::class, -75); $context->registerEventListener(MessageParseEvent::class, ReactionParser::class); $context->registerEventListener(MessageParseEvent::class, SystemMessage::class); + $context->registerEventListener(MessageParseEvent::class, PrivateReply::class); $context->registerEventListener(MessageParseEvent::class, SystemMessage::class, 9999); $context->registerEventListener(MessageParseEvent::class, UserMention::class, -100); diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 3641116913a..aed4ead872a 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -131,6 +131,7 @@ class Capabilities implements IPublicCapability { 'federated-shared-items', 'scheduled-messages', 'conversation-presets', + 'private-reply', ]; public const CONDITIONAL_FEATURES = [ diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index c6f244f665f..0e0b11e1b25 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -85,6 +85,7 @@ class ChatManager { public const VERB_VOICE_MESSAGE = 'voice-message'; public const VERB_RECORD_AUDIO = 'record-audio'; public const VERB_RECORD_VIDEO = 'record-video'; + public const VERB_PRIVATE_REPLY = 'private_reply'; /** * Last read message ID of -1 is set on the attendee table as default. @@ -393,6 +394,8 @@ public function sendMessage( int $threadId = 0, string $threadTitle = '', bool $fromScheduledMessage = false, + string $verb = self::VERB_MESSAGE, + array $extraMetaData = [], ): IComment { if ($chat->isFederatedConversation()) { $e = new MessagingNotAllowedException(); @@ -405,7 +408,7 @@ public function sendMessage( $comment->setCreationDateTime($creationDateTime); // A verb ('comment', 'like'...) must be provided to be able to save a // comment - $comment->setVerb(self::VERB_MESSAGE); + $comment->setVerb($verb); if ($replyTo instanceof IComment) { $comment->setParentId($replyTo->getId()); @@ -443,7 +446,7 @@ public function sendMessage( if ($threadId !== Thread::THREAD_NONE) { $metadata[Message::METADATA_THREAD_ID] = $threadId; } - $comment->setMetaData($metadata); + $comment->setMetaData(array_merge($metadata, $extraMetaData)); $event = new BeforeChatMessageSentEvent($chat, $comment, $participant, $silent, $replyTo); $this->dispatcher->dispatchTyped($event); diff --git a/lib/Chat/Parser/PrivateReply.php b/lib/Chat/Parser/PrivateReply.php new file mode 100644 index 00000000000..69d206cfe05 --- /dev/null +++ b/lib/Chat/Parser/PrivateReply.php @@ -0,0 +1,39 @@ + + */ +class PrivateReply implements IEventListener { + + #[\Override] + public function handle(Event $event): void { + if (!$event instanceof MessageParseEvent) { + return; + } + + $message = $event->getMessage(); + + if ($message->getMessageType() !== ChatManager::VERB_PRIVATE_REPLY) { + return; + } + + if ($message->getComment()->getParentId() === '0') { + $message->setVisibility(false); + $event->stopPropagation(); + return; + } + } +} diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index bac357d921d..76469321a45 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -143,6 +143,84 @@ public function __construct( parent::__construct($appName, $request); } + /** + * @return array{parent: IComment, parentMessage: Message}|DataResponse + */ + private function resolveReplyTo(int $replyTo, string $replyToToken, string $actorType, string $actorId, \DateTime $creationDateTime): array|DataResponse { + $isPrivateReplyFromAnotherConvo = $replyToToken != ''; + + try { + $targetParentRoom = $isPrivateReplyFromAnotherConvo ? $this->manager->getRoomByToken($replyToToken) : $this->room; + $parent = $this->chatManager->getParentComment($targetParentRoom, (string)$replyTo); + } catch (NotFoundException $e) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + + if ($isPrivateReplyFromAnotherConvo) { + $isOneToOneRoom = $this->room->getType() === Room::TYPE_ONE_TO_ONE; + if (!$isOneToOneRoom) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + + $parentActorId = $parent->getActorId(); + $parentActorType = $parent->getActorType(); + // Validate if the members are part of the convo + try { + $this->participantService->getParticipantByActor($targetParentRoom, $actorType, $actorId); + $this->participantService->getParticipantByActor($targetParentRoom, $parentActorType, $parentActorId); + } catch (ParticipantNotFoundException $e) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_FORBIDDEN); + } + + $originalParentMessage = $this->messageParser->createMessage($targetParentRoom, $this->participant, $parent, $this->l); + $this->messageParser->parseMessage($originalParentMessage); + + if (!$originalParentMessage->isReplyable()) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + + $originalMessageData = $originalParentMessage->toArray('json', null); + $originalMessageData['threadId'] = Thread::THREAD_NONE; + $originalMessageData['reactions'] = new \stdClass(); + $originalMessageData['messageType'] = $originalParentMessage->getMessageType(); + + $extraMetaData = [ + 'replyToMessageId' => $replyTo, + 'replyToConversationToken' => $replyToToken, + 'replyToConversationName' => $targetParentRoom->getName(), + 'replyToActorDisplayName' => $originalParentMessage->getActorDisplayName(), + ]; + $originalMessageData['metaData'] = array_merge($originalMessageData['metaData'] ?? [], $extraMetaData); + // Create a link message for the private reply + $copiedParent = $this->chatManager->sendMessage( + $this->room, + $this->participant, + $parentActorType, + $parentActorId, + ChatManager::VERB_PRIVATE_REPLY, + $creationDateTime, + null, + '', + true, + verb: ChatManager::VERB_PRIVATE_REPLY, + extraMetaData: [ + 'originalMessage' => $originalMessageData, + ], + ); + + $parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $copiedParent, $this->l); + $this->messageParser->parseMessage($parentMessage); + } else { + $parentMessage = $this->messageParser->createMessage($targetParentRoom, $this->participant, $parent, $this->l); + $this->messageParser->parseMessage($parentMessage); + + if (!$parentMessage->isReplyable()) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + } + + return ['parent' => $isPrivateReplyFromAnotherConvo ? $copiedParent : $parent, 'parentMessage' => $parentMessage]; + } /** * @return list{0: Attendee::ACTOR_*, 1: string} */ @@ -219,7 +297,12 @@ protected function parseCommentToResponse(IComment $comment, ?Message $parentMes } $data = $chatMessage->toArray($this->getResponseFormat(), $thread); if ($parentMessage instanceof Message) { - $data['parent'] = $parentMessage->toArray($this->getResponseFormat(), $thread); + $parentComment = $parentMessage->getComment(); + if ($parentComment->getVerb() === ChatManager::VERB_PRIVATE_REPLY && $parentComment->getParentId() === '0') { + $data['parent'] = $this->buildPrivateReplyParentSnapshot($parentComment); + } else { + $data['parent'] = $parentMessage->toArray($this->getResponseFormat(), $thread); + } } $headers = []; @@ -240,6 +323,7 @@ protected function parseCommentToResponse(IComment $comment, ?Message $parentMes * @param string $referenceId for the message to be able to later identify it again * @param int $replyTo Parent id which this message is a reply to * @psalm-param non-negative-int $replyTo + * @param string $replyToToken Parent token to which reply is initiated * @param bool $silent If sent silent the chat message will not create any notifications * @param string $threadTitle Only supported when not replying, when given will create a thread (requires `threads` capability) * @param int $threadId Thread id which this message is a reply to without quoting a specific message (ignored when $replyTo is given, also requires `threads` capability) @@ -262,7 +346,7 @@ protected function parseCommentToResponse(IComment $comment, ?Message $parentMes 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', ])] - public function sendMessage(string $message, string $actorDisplayName = '', string $referenceId = '', int $replyTo = 0, bool $silent = false, string $threadTitle = '', int $threadId = 0): DataResponse { + public function sendMessage(string $message, string $actorDisplayName = '', string $referenceId = '', int $replyTo = 0, string $replyToToken = '', bool $silent = false, string $threadTitle = '', int $threadId = 0): DataResponse { if ($this->room->isFederatedConversation()) { /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */ $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class); @@ -279,19 +363,15 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri } $parent = $parentMessage = null; - if ($replyTo !== 0) { - try { - $parent = $this->chatManager->getParentComment($this->room, (string)$replyTo); - } catch (NotFoundException $e) { - // Someone is trying to reply cross-rooms or to a non-existing message - return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); - } + $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC')); - $parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $parent, $this->l); - $this->messageParser->parseMessage($parentMessage); - if (!$parentMessage->isReplyable()) { - return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + if ($replyTo !== 0) { + $resolvedReplyTo = $this->resolveReplyTo($replyTo, $replyToToken, $actorType, $actorId, $creationDateTime); + if ($resolvedReplyTo instanceof DataResponse) { + return $resolvedReplyTo; } + $parent = $resolvedReplyTo['parent']; + $parentMessage = $resolvedReplyTo['parentMessage']; } elseif ($threadId !== 0) { if (!$this->threadService->validateThread($this->room->getId(), $threadId)) { return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); @@ -299,7 +379,6 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri } $this->participantService->ensureOneToOneRoomIsFilled($this->room); - $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC')); try { $createThread = $replyTo === 0 && $threadId === Thread::THREAD_NONE && $threadTitle !== ''; @@ -435,19 +514,16 @@ public function scheduleMessage( } $parent = $parentMessage = null; - if ($replyTo !== 0) { - try { - $parent = $this->chatManager->getParentComment($this->room, (string)$replyTo); - } catch (NotFoundException $e) { - // Someone is trying to reply cross-rooms or to a non-existing message - return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); - } + $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC')); - $parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $parent, $this->l); - $this->messageParser->parseMessage($parentMessage); - if (!$parentMessage->isReplyable()) { - return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + if ($replyTo !== 0) { + [$actorType, $actorId] = $this->getActorInfo(); + $resolvedReplyTo = $this->resolveReplyTo($replyTo, '', $actorType, $actorId, $creationDateTime); + if ($resolvedReplyTo instanceof DataResponse) { + return $resolvedReplyTo; } + $parent = $resolvedReplyTo['parent']; + $parentMessage = $resolvedReplyTo['parentMessage']; } if ($threadId !== 0 && !$this->threadService->validateThread($this->room->getId(), $threadId)) { @@ -1069,6 +1145,12 @@ protected function prepareCommentsAsDataResponse(array $comments, int $lastCommo continue; } + if ($comment->getVerb() === ChatManager::VERB_PRIVATE_REPLY && $comment->getParentId() === '0') { + $loadedParents[$parentId] = $this->buildPrivateReplyParentSnapshot($comment); + $messages[$commentKey]['parent'] = $loadedParents[$parentId]; + continue; + } + $expireDate = $message->getComment()->getExpireDate(); if ($expireDate instanceof \DateTime && $expireDate < $now) { $commentIdToIndex[$id] = null; @@ -1671,6 +1753,16 @@ public function getUpcomingReminders(): DataResponse { return new DataResponse($resultData, Http::STATUS_OK); } + private function buildPrivateReplyParentSnapshot(IComment $comment): array { + $metaData = $comment->getMetaData() ?? []; + $message = $metaData['originalMessage']; + $message['id'] = (int)$comment->getId(); + unset($metaData['originalMessage']); + $message['metaData'] = array_merge($metaData, $message['metaData'] ?? []); + + return $message; + } + /** * @throws DoesNotExistException * @throws CannotReachRemoteException diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 75085647bec..77d4f0bd7f0 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -168,6 +168,7 @@ public function isReplyable(): bool { && $this->getMessageType() !== ChatManager::VERB_COMMAND && $this->getMessageType() !== ChatManager::VERB_MESSAGE_DELETED && $this->getMessageType() !== ChatManager::VERB_REACTION + && $this->getMessageType() !== ChatManager::VERB_PRIVATE_REPLY && $this->getMessageType() !== ChatManager::VERB_REACTION_DELETED && \in_array($this->getActorType(), [ Attendee::ACTOR_USERS, diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 522dd0c5772..564210385d2 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -190,6 +190,14 @@ * threadId?: int, * // Set when a thread is created with this message. If missing, no thread creation is associated with this message * threadTitle?: string, + * // Set only when a message in a convo is private replied on a 1-1 room. Represents the parent message id + * replyToMessageId?: int, + * // Set only when a message in a convo is private replied on a 1-1 room. Represents the parent message's group conversation token. + * replyToConversationToken?: string, + * // Set only when a message in a convo is private replied on a 1-1 room. Represents the parent message's group conversation name + * replyToConversationName?: string, + * // Set only when a message in a convo is private replied on a 1-1 room. Represents the parent message's actor display name + * replyToActorDisplayName?: string, * } * * @psalm-type TalkChatMessage = TalkBaseMessage&array{ diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 1ca5b49be27..83c2692c544 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -535,7 +535,16 @@ protected function notifyMessageSent(AMessageSentEvent $event): void { if ($parent !== null) { $parentMessage = $this->messageParser->createMessage($event->getRoom(), null, $parent, $l10n); $this->messageParser->parseMessage($parentMessage); - $data['chat']['comment']['parent'] = $parentMessage->toArray('json', $thread); + if ($parent->getVerb() === ChatManager::VERB_PRIVATE_REPLY && $parent->getParentId() === '0') { + $metaData = $parent->getMetaData() ?? []; + $parentSnapshot = $metaData['originalMessage']; + $parentSnapshot['id'] = (int)$parent->getId(); + unset($metaData['originalMessage']); + $parentSnapshot['metaData'] = array_merge($metaData, $parentSnapshot['metaData'] ?? []); + $data['chat']['comment']['parent'] = $parentSnapshot; + } else { + $data['chat']['comment']['parent'] = $parentMessage->toArray('json', $thread); + } } $this->externalSignaling->sendRoomMessage($room, $data); From 7c650f30e6a81e1ff883dc2b3a33224549405132 Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Sat, 28 Mar 2026 12:06:13 +0100 Subject: [PATCH 2/6] feat(reply_private): All frontend changes Signed-off-by: existentialcoder --- src/components/MessageQuote.vue | 61 +++++++++++++++++-- .../MessageButtonsBar/MessageButtonsBar.vue | 14 ++++- .../MessagesGroup/Message/MessageItem.spec.js | 6 ++ src/components/NewMessage/NewMessage.vue | 24 +++++++- src/services/messagesService.ts | 3 + src/store/messagesStore.js | 6 +- src/stores/chatExtras.ts | 25 ++++++++ src/utils/prepareTemporaryMessage.ts | 10 +++ 8 files changed, 139 insertions(+), 10 deletions(-) diff --git a/src/components/MessageQuote.vue b/src/components/MessageQuote.vue index 5c50d472433..f6f47da68d5 100644 --- a/src/components/MessageQuote.vue +++ b/src/components/MessageQuote.vue @@ -10,11 +10,13 @@ import { t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' import { computed, ref, toRef } from 'vue' import { useRoute } from 'vue-router' +import { useStore } from 'vuex' import NcButton from '@nextcloud/vue/components/NcButton' import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import IconClose from 'vue-material-design-icons/Close.vue' import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue' import AvatarWrapper from './AvatarWrapper/AvatarWrapper.vue' +import { useGetToken } from '../composables/useGetToken.ts' import { useMessageInfo } from '../composables/useMessageInfo.ts' import { AVATAR } from '../constants.ts' import { EventBus } from '../services/EventBus.ts' @@ -35,8 +37,20 @@ const { message, canCancel = false, editMessage = false } = defineProps<{ }>() const route = useRoute() +const store = useStore() const actorStore = useActorStore() const chatExtrasStore = useChatExtrasStore() +const currentToken = useGetToken() + +const isPrivateReply = computed(() => { + if (isExistingMessage(message) && (message.metaData?.replyToConversationToken ?? '').length > 0) { + return true + } + if (canCancel) { + return !!chatExtrasStore.privateReply[currentToken.value] + } + return false +}) const { isFileShare, @@ -46,16 +60,31 @@ const { actorDisplayNameWithFallback, } = useMessageInfo(isExistingMessage(message) ? toRef(() => message) : undefined) +const targetToken = computed(() => { + if (!isExistingMessage(message)) { + return currentToken.value + } + return message.metaData?.replyToConversationToken + ?? message.token +}) const actorInfo = computed(() => [actorDisplayNameWithFallback.value, remoteServer.value].filter((value) => value).join(' ')) const hash = computed(() => '#message_' + message.id) const component = computed(() => canCancel ? { tag: 'div', link: undefined } - : { tag: 'router-link', link: { query: route.query, hash: hash.value } }) + : { tag: 'router-link', link: { query: route.query, hash: hash.value, name: 'conversation', params: { token: targetToken.value } } }) const isOwnMessageQuoted = computed(() => isExistingMessage(message) ? actorStore.checkIfSelfIsActor(message) : false) +const actorConvoName = computed(() => { + if (!isPrivateReply.value || !isExistingMessage(message)) { + return '' + } + return message.metaData?.replyToConversationName + ?? store.getters.conversation(targetToken.value)?.name ?? '' +}) + const filePreviewLoading = ref(true) const filePreviewFailed = ref(false) const filePreview = computed(() => { @@ -130,7 +159,7 @@ function handleAbort() { if (editMessage) { chatExtrasStore.removeMessageIdToEdit(message.token) } else { - chatExtrasStore.removeParentIdToReply(message.token) + chatExtrasStore.removeParentIdToReply(currentToken.value) } EventBus.emit('focus-chat-input') } @@ -190,17 +219,21 @@ function handleQuoteClick() { :source="message.actorType" :size="AVATAR.SIZE.EXTRA_SMALL" disableMenu /> - + {{ actorInfo }} + + {{ actorConvoName }} + {{ editLabel }} + class="quote__main-text" + :class="{ 'break-new-line': isPrivateReply }"> {{ shortenedQuoteMessage }} @@ -297,6 +330,11 @@ function handleQuoteClick() { flex-grow: 1; overflow: hidden; + &:has(.break-new-line) { + flex-direction: column; + align-items: flex-start; + } + &-author { display: flex; align-items: center; @@ -307,6 +345,10 @@ function handleQuoteClick() { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + &.no-ellipsis { + overflow: visible; + text-overflow: unset; + } } &-name { @@ -321,6 +363,17 @@ function handleQuoteClick() { overflow: hidden; text-align: start; max-width: 100%; + &.break-new-line { + white-space: normal; + overflow: visible; + text-overflow: unset; + } + } + + &-conversation-name { + &::before { + content: ' • '; + } } } diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue index 3670708cf29..f9c0c23d422 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue @@ -802,7 +802,19 @@ export default { async handlePrivateReply() { // open the 1:1 conversation const conversation = await this.$store.dispatch('createOneToOneConversation', this.message.actorId) - this.$router.push({ name: 'conversation', params: { token: conversation.token } }).catch((err) => console.debug(`Error while pushing the new conversation's route: ${err}`)) + if (hasTalkFeature(conversation.token, 'private-reply') && hasTalkFeature(this.message.token, 'private-reply')) { + this.chatExtrasStore.setParentIdToReply({ + token: conversation.token, + id: this.message.id, + }) + this.chatExtrasStore.setPrivateReplyParentToken({ + token: conversation.token, + parentToken: this.message.token, + }) + } + this.$router + .push({ name: 'conversation', params: { token: conversation.token } }) + .catch((err) => console.debug(`Error while pushing the new conversation's route: ${err}`)) }, async handleCopyMessageText() { diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js index 5bcb1aabeba..35e2f973eb2 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js @@ -7,6 +7,7 @@ import { flushPromises, mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { ref } from 'vue' import { createStore } from 'vuex' import NcActions from '@nextcloud/vue/components/NcActions' import NcButton from '@nextcloud/vue/components/NcButton' @@ -40,6 +41,11 @@ vi.mock('vuex', async () => { } }) +vi.mock('@vueuse/router', () => ({ + useRouteParams: vi.fn(() => ref('XXTOKENXX')), + useRouteQuery: vi.fn(), +})) + describe('MessageItem.vue', () => { const TOKEN = 'XXTOKENXX' let testStoreConfig diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index bf39a9943cb..0f54af8bedc 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -160,7 +160,7 @@