Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2021-02-02 16:30:18 +0300
committerGitHub <noreply@github.com>2021-02-02 16:30:18 +0300
commit9bd20f3607f76541a24f7d9f11ae9e9b38521919 (patch)
treebe6d33db6196cf196c2ea6e3555df77bbf604c1c
parentb6028592ed49a0d33dddc62ad65ec8e13865ea49 (diff)
parent11a861c2cbf26916162ff989e70c3dcb61bb169f (diff)
Merge pull request #4861 from nextcloud/enh/deleteMsg
Delete chat messages
-rw-r--r--appinfo/routes.php10
-rw-r--r--docs/capabilities.md3
-rw-r--r--docs/chat.md33
-rw-r--r--lib/Capabilities.php1
-rw-r--r--lib/Chat/ChatManager.php55
-rw-r--r--lib/Chat/Parser/Listener.php20
-rw-r--r--lib/Chat/Parser/SystemMessage.php65
-rw-r--r--lib/Controller/ChatController.php69
-rw-r--r--lib/Model/Message.php9
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/Message.vue97
-rw-r--r--src/components/MessagesList/MessagesList.vue4
-rw-r--r--src/services/messagesService.js12
-rw-r--r--src/store/messagesStore.js38
-rw-r--r--tests/integration/features/bootstrap/FeatureContext.php50
-rw-r--r--tests/integration/features/chat/delete.feature148
-rw-r--r--tests/php/CapabilitiesTest.php1
-rw-r--r--tests/php/Chat/Parser/SystemMessageTest.php21
-rw-r--r--tests/php/Controller/ChatControllerTest.php5
18 files changed, 613 insertions, 28 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php
index fd51ff82c..db32cbd72 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -154,6 +154,16 @@ return [
],
],
[
+ 'name' => 'Chat#deleteMessage',
+ 'url' => '/api/{apiVersion}/chat/{token}/{messageId}',
+ 'verb' => 'DELETE',
+ 'requirements' => [
+ 'apiVersion' => 'v1',
+ 'token' => '^[a-z0-9]{4,30}$',
+ 'messageId' => '^[0-9]+$',
+ ],
+ ],
+ [
'name' => 'Chat#setReadMarker',
'url' => '/api/{apiVersion}/chat/{token}/read',
'verb' => 'POST',
diff --git a/docs/capabilities.md b/docs/capabilities.md
index faf3b18d9..2383394cb 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -64,3 +64,6 @@ title: Capabilities
* `room-description` - A description can be get and set for conversations.
* `config => chat => read-privacy` - See `chat-read-status`
* `config => previews => max-gif-size` - Maximum size in bytes below which a GIF can be embedded directly in the page at render time. Bigger files will be rendered statically using the preview endpoint instead. Can be set with `occ config:app:set spreed max-gif-size --value=X` where X is the new value in bytes. Defaults to 3 MB.
+
+## 12.0
+* `delete-messages` - Allows to delete chat messages up to 6 hours for your own messages or when being a moderator. On deleting the message text will be replaced and a follow up system message will make sure clients and users update it in their cache and storage.
diff --git a/docs/chat.md b/docs/chat.md
index f9b336ff6..3d03bf5e1 100644
--- a/docs/chat.md
+++ b/docs/chat.md
@@ -44,7 +44,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`actorDisplayName` | string | Display name of the message author
`timestamp` | int | Timestamp in seconds and UTC time zone
`systemMessage` | string | empty for normal chat message or the type of the system message (untranslated)
- `messageType` | string | Currently known types are `comment`, `system` and `command`
+ `messageType` | string | Currently known types are `comment`, `comment_deleted`, `system` and `command`
`isReplyable` | bool | True if the user can post a reply to this message (only available with `chat-replies` capability)
`referenceId` | string | A reference string that was given while posting the message to be able to identify a sent message again (only available with `chat-reference-id` capability)
`message` | string | Message string with placeholders (see [Rich Object String](https://github.com/nextcloud/server/issues/1706))
@@ -96,6 +96,35 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
- Data:
The full message array of the new message, as defined in [Receive chat messages of a conversation](#receive-chat-messages-of-a-conversation)
+
+## Deleting a chat message
+
+* Method: `DELETE`
+* Endpoint: `/chat/{token}/{messageId}`
+
+* Response:
+ - Status code:
+ + `200 OK` - When deleting was successful
+ + `202 Accepted` - When deleting was successful but Matterbridge is enabled so the message was leaked to other services
+ + `400 Bad Request` The message is already older than 6 hours
+ + `403 Forbidden` When the message is not from the current user and the user not a moderator
+ + `403 Forbidden` When the conversation is read-only
+ + `404 Not Found` When the conversation or chat message could not be found for the participant
+ + `405 Method Not Allowed` When the message is not a normal chat message
+ + `412 Precondition Failed` When the lobby is active and the user is not a moderator
+
+ - Header:
+
+ field | type | Description
+ ------|------|------------
+ `X-Chat-Last-Common-Read` | int | ID of the last message read by every user that has read privacy set to public. When the user themself has it set to private the value the header is not set (only available with `chat-read-status` capability)
+
+ - Data:
+ The full message array of the new system message "You deleted a message", as defined in [Receive chat messages of a conversation](#receive-chat-messages-of-a-conversation)
+ The parent message is the object of the deleted message with the replaced text "Message deleted by you".
+ This message should not be displayed to the user but instead be used to remove the original message from any cache/storage of the device.
+
+
## Mark chat as read
* Method: `POST`
@@ -119,7 +148,6 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`X-Chat-Last-Common-Read` | int | ID of the last message read by every user that has read privacy set to public. When the user themself has it set to private the value the header is not set (only available with `chat-read-status` capability)
-
## Get mention autocomplete suggestions
* Method: `GET`
@@ -177,3 +205,4 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
* `moderator_demoted` - {actor} demoted {user} from moderator
* `guest_moderator_promoted` - {actor} promoted {user} to moderator
* `guest_moderator_demoted` - {actor} demoted {user} from moderator
+* `message_deleted` - Message deleted by {actor} (Should not be shown to the user)
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 1f3a54720..668574f0d 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -68,6 +68,7 @@ class Capabilities implements IPublicCapability {
'last-room-activity',
'no-ping',
'system-messages',
+ 'delete-messages',
'mention-flag',
'in-call-flags',
'notification-levels',
diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php
index b21d90b4a..4202a3a46 100644
--- a/lib/Chat/ChatManager.php
+++ b/lib/Chat/ChatManager.php
@@ -106,15 +106,28 @@ class ChatManager {
* @param \DateTime $creationDateTime
* @param bool $sendNotifications
* @param string|null $referenceId
+ * @param int|null $parentId
* @return IComment
*/
- public function addSystemMessage(Room $chat, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, bool $sendNotifications, ?string $referenceId = null): IComment {
+ public function addSystemMessage(
+ Room $chat,
+ string $actorType,
+ string $actorId,
+ string $message,
+ \DateTime $creationDateTime,
+ bool $sendNotifications,
+ ?string $referenceId = null,
+ ?int $parentId = null
+ ): IComment {
$comment = $this->commentsManager->create($actorType, $actorId, 'chat', (string) $chat->getId());
$comment->setMessage($message, self::MAX_CHAT_LENGTH);
$comment->setCreationDateTime($creationDateTime);
if ($referenceId !== null) {
$comment->setReferenceId($referenceId);
}
+ if ($parentId !== null) {
+ $comment->setParentId((string) $parentId);
+ }
$comment->setVerb('system');
$event = new ChatEvent($chat, $comment);
@@ -231,6 +244,30 @@ class ChatManager {
return $comment;
}
+ public function deleteMessage(Room $chat, int $messageId, string $actorType, string $actorId, \DateTime $deletionTime): IComment {
+ $comment = $this->getComment($chat, (string) $messageId);
+ $comment->setMessage(
+ json_encode([
+ 'deleted_by_type' => $actorType,
+ 'deleted_by_id' => $actorId,
+ 'deleted_on' => $deletionTime->getTimestamp(),
+ ])
+ );
+ $comment->setVerb('comment_deleted');
+ $this->commentsManager->save($comment);
+
+ return $this->addSystemMessage(
+ $chat,
+ $actorType,
+ $actorId,
+ json_encode(['message' => 'message_deleted', 'parameters' => ['message' => $messageId]]),
+ $this->timeFactory->getDateTime(),
+ false,
+ null,
+ $messageId
+ );
+ }
+
/**
* @param Room $chat
* @param string $parentId
@@ -247,6 +284,22 @@ class ChatManager {
return $comment;
}
+ /**
+ * @param Room $chat
+ * @param string $messageId
+ * @return IComment
+ * @throws NotFoundException
+ */
+ public function getComment(Room $chat, string $messageId): IComment {
+ $comment = $this->commentsManager->get($messageId);
+
+ if ($comment->getObjectType() !== 'chat' || $comment->getObjectId() !== (string) $chat->getId()) {
+ throw new NotFoundException('Message not found in the right context');
+ }
+
+ return $comment;
+ }
+
public function getLastReadMessageFromLegacy(Room $chat, IUser $user): int {
$marker = $this->commentsManager->getReadMark('chat', $chat->getId(), $user);
if ($marker === null) {
diff --git a/lib/Chat/Parser/Listener.php b/lib/Chat/Parser/Listener.php
index 2cb2fac3a..75e068b7c 100644
--- a/lib/Chat/Parser/Listener.php
+++ b/lib/Chat/Parser/Listener.php
@@ -96,5 +96,25 @@ class Listener {
$event->stopPropagation();
}
});
+
+ $dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function (ChatMessageEvent $event) {
+ $chatMessage = $event->getMessage();
+
+ if ($chatMessage->getMessageType() !== 'comment_deleted') {
+ return;
+ }
+
+ /** @var SystemMessage $parser */
+ $parser = \OC::$server->query(SystemMessage::class);
+
+ try {
+ $parser->parseDeletedMessage($chatMessage);
+ $event->stopPropagation();
+ } catch (\OutOfBoundsException $e) {
+ // Unknown message, ignore
+ } catch (\RuntimeException $e) {
+ $event->stopPropagation();
+ }
+ }, 9999);// First things first
}
}
diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php
index 74acd0688..ab1534480 100644
--- a/lib/Chat/Parser/SystemMessage.php
+++ b/lib/Chat/Parser/SystemMessage.php
@@ -89,7 +89,7 @@ class SystemMessage {
$message = $data['message'];
$parameters = $data['parameters'];
- $parsedParameters = ['actor' => $this->getActor($comment)];
+ $parsedParameters = ['actor' => $this->getActorFromComment($comment)];
$participant = $chatMessage->getParticipant();
if (!$participant->isGuest()) {
@@ -354,6 +354,57 @@ class SystemMessage {
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You stopped Matterbridge.');
}
+ } elseif ($message === 'message_deleted') {
+ $parsedMessage = $this->l->t('{actor} deleted a message');
+ if ($currentUserIsActor) {
+ $parsedMessage = $this->l->t('You deleted a message');
+ }
+ } else {
+ throw new \OutOfBoundsException('Unknown subject');
+ }
+
+ $chatMessage->setMessage($parsedMessage, $parsedParameters, $message);
+ }
+
+ /**
+ * @param Message $chatMessage
+ * @throws \OutOfBoundsException
+ */
+ public function parseDeletedMessage(Message $chatMessage): void {
+ $this->l = $chatMessage->getL10n();
+ $data = json_decode($chatMessage->getMessage(), true);
+ if (!\is_array($data)) {
+ throw new \OutOfBoundsException('Invalid message');
+ }
+
+ $parsedParameters = ['actor' => $this->getActor($data['deleted_by_type'], $data['deleted_by_id'])];
+
+ $participant = $chatMessage->getParticipant();
+ $currentActorId = $participant->getAttendee()->getActorId();
+
+ $authorIsActor = $data['deleted_by_type'] === $chatMessage->getComment()->getActorType()
+ && $data['deleted_by_id'] === $chatMessage->getComment()->getActorId();
+
+ if (!$participant->isGuest()) {
+ $currentUserIsActor = $parsedParameters['actor']['type'] === 'user' &&
+ $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS &&
+ $currentActorId === $parsedParameters['actor']['id'];
+ } else {
+ $currentUserIsActor = $parsedParameters['actor']['type'] === 'guest' &&
+ $participant->getAttendee()->getActorType() === 'guest' &&
+ $currentActorId === $parsedParameters['actor']['id'];
+ }
+
+ if ($chatMessage->getMessageType() === 'comment_deleted') {
+ $message = 'message_deleted';
+ $parsedMessage = $this->l->t('Message deleted by author');
+
+ if (!$authorIsActor) {
+ $parsedMessage = $this->l->t('Message deleted by {actor}');
+ }
+ if ($currentUserIsActor) {
+ $parsedMessage = $this->l->t('Message deleted by you');
+ }
} else {
throw new \OutOfBoundsException('Unknown subject');
}
@@ -430,12 +481,16 @@ class SystemMessage {
];
}
- protected function getActor(IComment $comment): array {
- if ($comment->getActorType() === Attendee::ACTOR_GUESTS) {
- return $this->getGuest($comment->getActorId());
+ protected function getActorFromComment(IComment $comment): array {
+ return $this->getActor($comment->getActorType(), $comment->getActorId());
+ }
+
+ protected function getActor(string $actorType, string $actorId): array {
+ if ($actorType === Attendee::ACTOR_GUESTS) {
+ return $this->getGuest($actorId);
}
- return $this->getUser($comment->getActorId());
+ return $this->getUser($actorId);
}
protected function getUser(string $uid): array {
diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php
index 9aecf0fbe..4c5706086 100644
--- a/lib/Controller/ChatController.php
+++ b/lib/Controller/ChatController.php
@@ -29,6 +29,7 @@ use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\GuestManager;
+use OCA\Talk\MatterbridgeManager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Message;
use OCA\Talk\Model\Session;
@@ -92,6 +93,9 @@ class ChatController extends AEnvironmentAwareController {
/** @var IUserStatusManager */
private $statusManager;
+ /** @var MatterbridgeManager */
+ protected $matterbridgeManager;
+
/** @var SearchPlugin */
private $searchPlugin;
@@ -120,6 +124,7 @@ class ChatController extends AEnvironmentAwareController {
MessageParser $messageParser,
IManager $autoCompleteManager,
IUserStatusManager $statusManager,
+ MatterbridgeManager $matterbridgeManager,
SearchPlugin $searchPlugin,
ISearchResult $searchResult,
ITimeFactory $timeFactory,
@@ -138,6 +143,7 @@ class ChatController extends AEnvironmentAwareController {
$this->messageParser = $messageParser;
$this->autoCompleteManager = $autoCompleteManager;
$this->statusManager = $statusManager;
+ $this->matterbridgeManager = $matterbridgeManager;
$this->searchPlugin = $searchPlugin;
$this->searchResult = $searchResult;
$this->timeFactory = $timeFactory;
@@ -465,6 +471,69 @@ class ChatController extends AEnvironmentAwareController {
/**
* @NoAdminRequired
* @RequireParticipant
+ * @RequireReadWriteConversation
+ * @RequireModeratorOrNoLobby
+ *
+ * @param int $messageId
+ * @return DataResponse
+ */
+ public function deleteMessage(int $messageId): DataResponse {
+ try {
+ $message = $this->chatManager->getComment($this->room, (string) $messageId);
+ } catch (NotFoundException $e) {
+ return new DataResponse([], Http::STATUS_NOT_FOUND);
+ }
+
+ $attendee = $this->participant->getAttendee();
+ if (!$this->participant->hasModeratorPermissions(false)
+ && ($message->getActorType() !== $attendee->getActorType()
+ || $message->getActorId() !== $attendee->getActorId())) {
+ // Actor is not a moderator or not the owner of the message
+ return new DataResponse([], Http::STATUS_FORBIDDEN);
+ }
+
+ if ($message->getVerb() !== 'comment') {
+ // System message or file share (since the message is not parsed, it has type "system")
+ return new DataResponse([], Http::STATUS_METHOD_NOT_ALLOWED);
+ }
+
+ $maxDeleteAge = $this->timeFactory->getDateTime();
+ $maxDeleteAge->sub(new \DateInterval('PT6H'));
+ if ($message->getCreationDateTime() < $maxDeleteAge) {
+ // Message is too old
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ $systemMessageComment = $this->chatManager->deleteMessage(
+ $this->room,
+ $messageId,
+ $attendee->getActorType(),
+ $attendee->getActorId(),
+ $this->timeFactory->getDateTime()
+ );
+
+ $systemMessage = $this->messageParser->createMessage($this->room, $this->participant, $systemMessageComment, $this->l);
+ $this->messageParser->parseMessage($systemMessage);
+
+ $comment = $this->chatManager->getComment($this->room, (string) $messageId);
+ $message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
+ $this->messageParser->parseMessage($message);
+
+ $data = $systemMessage->toArray();
+ $data['parent'] = $message->toArray();
+
+ $bridge = $this->matterbridgeManager->getBridgeOfRoom($this->room);
+
+ $response = new DataResponse($data, $bridge['enabled'] ? Http::STATUS_ACCEPTED: Http::STATUS_OK);
+ if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
+ $response->addHeader('X-Chat-Last-Common-Read', $this->chatManager->getLastCommonReadMessage($this->room));
+ }
+ return $response;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @RequireParticipant
*
* @param int $lastReadMessage
* @return DataResponse
diff --git a/lib/Model/Message.php b/lib/Model/Message.php
index 97d578c18..49ed61e98 100644
--- a/lib/Model/Message.php
+++ b/lib/Model/Message.php
@@ -158,11 +158,12 @@ class Message {
public function isReplyable(): bool {
return $this->getMessageType() !== 'system' &&
$this->getMessageType() !== 'command' &&
+ $this->getMessageType() !== 'comment_deleted' &&
\in_array($this->getActorType(), [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS]);
}
public function toArray(): array {
- return [
+ $data = [
'id' => (int) $this->getComment()->getId(),
'token' => $this->getRoom()->getToken(),
'actorType' => $this->getActorType(),
@@ -176,5 +177,11 @@ class Message {
'isReplyable' => $this->isReplyable(),
'referenceId' => (string) $this->getComment()->getReferenceId(),
];
+
+ if ($this->getMessageType() === 'comment_deleted') {
+ $data['deleted'] = true;
+ }
+
+ return $data;
}
}
diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue
index 9b1032ff0..c4a04bc1a 100644
--- a/src/components/MessagesList/MessagesGroup/Message/Message.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue
@@ -52,6 +52,9 @@ the main body of the message as well as a quote.
<RichText :text="message" :arguments="richParameters" :autolink="true" />
<CallButton />
</div>
+ <div v-else-if="isDeletedMessage" class="message__main__text deleted-message">
+ <RichText :text="message" :arguments="richParameters" :autolink="true" />
+ </div>
<div v-else class="message__main__text" :class="{'system-message': isSystemMessage}">
<Quote v-if="parent" :parent-id="parent" v-bind="quote" />
<RichText :text="message" :arguments="richParameters" :autolink="true" />
@@ -87,7 +90,7 @@ the main body of the message as well as a quote.
title=""
:size="16" />
</div>
- <div v-else-if="isTemporary && !isTemporaryUpload"
+ <div v-else-if="isTemporary && !isTemporaryUpload || isDeleting"
v-tooltip.auto="loadingIconTooltip"
class="icon-loading-small message-status"
:aria-label="loadingIconTooltip" />
@@ -129,6 +132,13 @@ the main body of the message as well as a quote.
{{ action.label }}
</ActionButton>
</template>
+ <ActionButton
+ v-if="isDeleteable"
+ icon="icon-delete"
+ :close-after-click="true"
+ @click.stop="handleDelete">
+ {{ t('spreed', 'Delete') }}
+ </ActionButton>
</Actions>
</div>
</div>
@@ -148,13 +158,19 @@ import RichText from '@juliushaertl/vue-richtext'
import AlertCircle from 'vue-material-design-icons/AlertCircle'
import Check from 'vue-material-design-icons/Check'
import CheckAll from 'vue-material-design-icons/CheckAll'
+import Reload from 'vue-material-design-icons/Reload'
import Quote from '../../../Quote'
import participant from '../../../../mixins/participant'
import { EventBus } from '../../../../services/EventBus'
import emojiRegex from 'emoji-regex'
import { PARTICIPANT, CONVERSATION } from '../../../../constants'
import moment from '@nextcloud/moment'
-import Reload from 'vue-material-design-icons/Reload'
+import {
+ showError,
+ showSuccess,
+ showWarning,
+ TOAST_DEFAULT_TIMEOUT
+} from '@nextcloud/dialogs'
export default {
name: 'Message',
@@ -275,6 +291,13 @@ export default {
required: true,
},
/**
+ * The type of the message.
+ */
+ messageType: {
+ type: String,
+ required: true,
+ },
+ /**
* The parent message's id.
*/
parent: {
@@ -293,6 +316,7 @@ export default {
// Is tall enough for both actions and date upon hovering
isTallEnough: false,
showReloadButton: false,
+ isDeleting: false,
}
},
@@ -302,7 +326,7 @@ export default {
},
hasActions() {
- return this.isReplyable && !this.isConversationReadOnly
+ return (this.isReplyable || this.isDeleteable) && !this.isConversationReadOnly
},
isConversationReadOnly() {
@@ -313,6 +337,10 @@ export default {
return this.systemMessage !== ''
},
+ isDeletedMessage() {
+ return this.messageType === 'comment_deleted'
+ },
+
messageTime() {
return moment(this.timestamp * 1000).format('LT')
},
@@ -337,6 +365,7 @@ export default {
showSentIcon() {
return !this.isSystemMessage
&& !this.isTemporary
+ && !this.isDeleting
&& this.actorType === this.participant.actorType
&& this.actorId === this.participant.actorId
},
@@ -408,7 +437,7 @@ export default {
// Determines whether the date has to be displayed or not
hasDate() {
- if (this.isTemporary || this.sendingFailure) {
+ if (this.isTemporary || this.isDeleting || this.sendingFailure) {
// Never on temporary or failed messages
return false
}
@@ -446,6 +475,24 @@ export default {
return t('spreed', 'You can not send messages to this conversation at the moment')
},
+ isMyMsg() {
+ return this.actorId === this.$store.getters.getActorId()
+ && this.actorType === this.$store.getters.getActorType()
+ },
+
+ isDeleteable() {
+ const isFileShare = this.message === '{file}'
+ && this.messageParameters?.file
+
+ return (moment(this.timestamp * 1000).add(6, 'h')) > moment()
+ && this.messageType === 'comment'
+ && !this.isDeleting
+ && !isFileShare
+ && (this.participant.participantType === PARTICIPANT.TYPE.OWNER
+ || this.participant.participantType === PARTICIPANT.TYPE.MODERATOR
+ || this.isMyMsg)
+ },
+
messageActions() {
return this.$store.getters.messageActions
},
@@ -457,7 +504,6 @@ export default {
apiVersion: 'v3',
}
},
-
},
watch: {
@@ -515,8 +561,38 @@ export default {
})
EventBus.$emit('focusChatInput')
},
- handleDelete() {
- this.$store.dispatch('deleteMessage', this.message)
+ async handleDelete() {
+ this.isDeleting = true
+ try {
+ const statusCode = await this.$store.dispatch('deleteMessage', {
+ message: {
+ token: this.token,
+ id: this.id,
+ },
+ placeholder: t('spreed', 'Deleting message'),
+ })
+
+ if (statusCode === 202) {
+ showWarning(t('spreed', 'Message deleted successfully, but Matterbridge is configured and the message might already be distributed to other services'), {
+ timeout: TOAST_DEFAULT_TIMEOUT * 2,
+ })
+ } else if (statusCode === 200) {
+ showSuccess(t('spreed', 'Message deleted successfully'))
+ }
+ } catch (e) {
+ if (e?.response?.status === 400) {
+ showError(t('spreed', 'Message could not be deleted because it is too old'))
+ } else if (e?.response?.status === 405) {
+ showError(t('spreed', 'Only normal chat messages can be deleted'))
+ } else {
+ showError(t('spreed', 'An error occurred while deleting the message'))
+ console.error(e)
+ }
+ this.isDeleting = false
+ return
+ }
+
+ this.isDeleting = false
},
},
}
@@ -559,6 +635,13 @@ export default {
width: 100%;
}
+ &.deleted-message {
+ background-color: var(--color-background-dark);
+ color: var(--color-text-lighter);
+ padding: var(--border-radius) var(--border-radius-large);
+ border-radius: var(--border-radius-large);
+ }
+
::v-deep .rich-text--wrapper {
white-space: pre-wrap;
word-break: break-word;
diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue
index a7ea914e1..433b855dd 100644
--- a/src/components/MessagesList/MessagesList.vue
+++ b/src/components/MessagesList/MessagesList.vue
@@ -162,6 +162,10 @@ export default {
const groups = []
let lastMessage = null
for (const message of this.messagesList) {
+ if (message.systemMessage === 'message_deleted') {
+ continue
+ }
+
if (!this.messagesShouldBeGrouped(message, lastMessage)) {
// Add the date separator for different days
if (this.messagesHaveDifferentDate(message, lastMessage)) {
diff --git a/src/services/messagesService.js b/src/services/messagesService.js
index c17966be6..02d4af8fc 100644
--- a/src/services/messagesService.js
+++ b/src/services/messagesService.js
@@ -103,8 +103,20 @@ const postNewMessage = async function({ token, message, actorDisplayName, refere
return response
}
+/**
+ * Deletes a message from the server.
+ *
+ * @param {object} param0 The message object that is destructured
+ * @param {string} token The conversation token
+ * @param {string} id The id of the message to be deleted
+ */
+const deleteMessage = async function({ token, id }) {
+ return axios.delete(generateOcsUrl('apps/spreed/api/v1/chat', 2) + token + '/' + id)
+}
+
export {
fetchMessages,
lookForNewMessages,
postNewMessage,
+ deleteMessage,
}
diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js
index 65d17e237..dd7ed356a 100644
--- a/src/store/messagesStore.js
+++ b/src/store/messagesStore.js
@@ -20,6 +20,7 @@
*
*/
import Vue from 'vue'
+import { deleteMessage } from '../services/messagesService'
const state = {
/**
@@ -130,6 +131,16 @@ const mutations = {
},
/**
+ * Deletes a message from the store.
+ * @param {object} state current store state;
+ * @param {object} message the message;
+ * @param {string} placeholder Placeholder message until deleting finished
+ */
+ markMessageAsDeleting(state, { message, placeholder }) {
+ Vue.set(state.messages[message.token][message.id], 'messageType', 'comment_deleted')
+ Vue.set(state.messages[message.token][message.id], 'message', placeholder)
+ },
+ /**
* Adds a temporary message to the store.
* @param {object} state current store state;
* @param {object} message the temporary message;
@@ -221,10 +232,31 @@ const actions = {
* Delete a message
*
* @param {object} context default store context;
- * @param {string} message the message to be deleted;
+ * @param {object} message the message to be deleted;
+ * @param {string} placeholder Placeholder message until deleting finished
*/
- deleteMessage(context, message) {
- context.commit('deleteMessage', message)
+ async deleteMessage(context, { message, placeholder }) {
+ const messageObject = Object.assign({}, context.getters.message(message.token, message.id))
+ context.commit('markMessageAsDeleting', { message, placeholder })
+
+ let response
+ try {
+ response = await deleteMessage(message)
+ } catch (e) {
+ // Restore the previous message state
+ context.commit('addMessage', messageObject)
+ throw e
+ }
+
+ const systemMessage = response.data.ocs.data
+ if (systemMessage.parent) {
+ context.commit('addMessage', systemMessage.parent)
+ systemMessage.parent = systemMessage.parent.id
+ }
+
+ context.commit('addMessage', systemMessage)
+
+ return response.status
},
/**
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index 8f64ba0e6..feb7af04d 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -976,7 +976,6 @@ class FeatureContext implements Context, SnippetAcceptingContext {
* @param string $apiVersion
*/
public function userAddAttendeeToRoom($user, $newType, $newId, $identifier, $statusCode, $apiVersion = 'v1') {
- var_dump($newType);
$this->setCurrentUser($user);
$this->sendRequest(
'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants',
@@ -1104,6 +1103,24 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
/**
+ * @Then /^user "([^"]*)" deletes message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v(1|2|3))\))?$/
+ *
+ * @param string $user
+ * @param string $message
+ * @param string $identifier
+ * @param string $statusCode
+ * @param string $apiVersion
+ */
+ public function userDeletesMessageFromRoom($user, $message, $identifier, $statusCode, $apiVersion = 'v1') {
+ $this->setCurrentUser($user);
+ $this->sendRequest(
+ 'DELETE', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/' . self::$messages[$message],
+ new TableNode([['message', $message]])
+ );
+ $this->assertStatusCode($this->response, $statusCode);
+ }
+
+ /**
* @Then /^user "([^"]*)" reads message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v(1|2|3))\))?$/
*
* @param string $user
@@ -1192,18 +1209,43 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
/**
+ * @Then /^user "([^"]*)" received a system messages in room "([^"]*)" to delete "([^"]*)"(?: \((v(1|2|3))\))?$/
+ *
+ * @param string $user
+ * @param string $identifier
+ * @param string $message
+ * @param string $apiVersion
+ */
+ public function userReceivedDeleteMessage($user, $identifier, $message, $apiVersion = 'v1') {
+ $this->setCurrentUser($user);
+ $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=0');
+ $this->assertStatusCode($this->response, 200);
+
+ $actual = $this->getDataFromResponse($this->response);
+
+ foreach ($actual as $m) {
+ if ($m['systemMessage'] === 'message_deleted') {
+ if (isset($m['parent']['id']) && $m['parent']['id'] === self::$messages[$message]) {
+ return;
+ }
+ }
+ }
+ Assert::fail('Missing message_deleted system message for "' . $message . '"');
+ }
+
+ /**
* @Then /^user "([^"]*)" sees the following messages in room "([^"]*)" starting with "([^"]*)" with (\d+)(?: \((v(1|2|3))\))?$/
*
* @param string $user
* @param string $identifier
- * @param string $knwonMessage
+ * @param string $knownMessage
* @param string $statusCode
* @param string $apiVersion
* @param TableNode|null $formData
*/
- public function userAwaitsTheFollowingMessagesInRoom($user, $identifier, $knwonMessage, $statusCode, $apiVersion = 'v1', TableNode $formData = null) {
+ public function userAwaitsTheFollowingMessagesInRoom($user, $identifier, $knownMessage, $statusCode, $apiVersion = 'v1', TableNode $formData = null) {
$this->setCurrentUser($user);
- $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=1&includeLastKnown=1&lastKnownMessageId=' . self::$messages[$knwonMessage]);
+ $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=1&includeLastKnown=1&lastKnownMessageId=' . self::$messages[$knownMessage]);
$this->assertStatusCode($this->response, $statusCode);
$this->compareDataResponse($formData);
diff --git a/tests/integration/features/chat/delete.feature b/tests/integration/features/chat/delete.feature
new file mode 100644
index 000000000..388ceee91
--- /dev/null
+++ b/tests/integration/features/chat/delete.feature
@@ -0,0 +1,148 @@
+Feature: chat/reply
+ Background:
+ Given user "participant1" exists
+ Given user "participant2" exists
+
+ Scenario: moderator deletes their own message
+ Given user "participant1" creates room "group room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds user "participant2" to room "group room" with 200
+ And user "participant1" sends message "Message 1" to room "group room" with 201
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1 | [] | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1 | [] | |
+ And user "participant1" deletes message "Message 1" from room "group room" with 200
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message deleted by you | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message deleted by author | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant1" received a system messages in room "group room" to delete "Message 1"
+ Then user "participant2" received a system messages in room "group room" to delete "Message 1"
+
+ Scenario: user deletes their own message
+ Given user "participant1" creates room "group room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds user "participant2" to room "group room" with 200
+ And user "participant2" sends message "Message 1" to room "group room" with 201
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ And user "participant2" deletes message "Message 1" from room "group room" with 200
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message deleted by author | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname"}} | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message deleted by you | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname"}} | |
+ Then user "participant1" received a system messages in room "group room" to delete "Message 1"
+ Then user "participant2" received a system messages in room "group room" to delete "Message 1"
+
+ Scenario: moderator deletes other user message
+ Given user "participant1" creates room "group room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds user "participant2" to room "group room" with 200
+ And user "participant2" sends message "Message 1" to room "group room" with 201
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ And user "participant1" deletes message "Message 1" from room "group room" with 200
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message deleted by you | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant2 | participant2-displayname | Message deleted by {actor} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant1" received a system messages in room "group room" to delete "Message 1"
+ Then user "participant2" received a system messages in room "group room" to delete "Message 1"
+
+ Scenario: moderator deletes their own message which got replies
+ Given user "participant1" creates room "group room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds user "participant2" to room "group room" with 200
+ And user "participant2" sends message "Message 1" to room "group room" with 201
+ When user "participant1" sends reply "Message 1-1" on message "Message 1" to room "group room" with 201
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message 1 |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message 1 |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ And user "participant1" deletes message "Message 1" from room "group room" with 200
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message deleted by you |
+ | group room | users | participant2 | participant2-displayname | Message deleted by you | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message deleted by {actor} |
+ | group room | users | participant2 | participant2-displayname | Message deleted by {actor} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant1" received a system messages in room "group room" to delete "Message 1"
+ Then user "participant2" received a system messages in room "group room" to delete "Message 1"
+
+ Scenario: user deletes their own message which got replies
+ Given user "participant1" creates room "group room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds user "participant2" to room "group room" with 200
+ And user "participant2" sends message "Message 1" to room "group room" with 201
+ When user "participant1" sends reply "Message 1-1" on message "Message 1" to room "group room" with 201
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message 1 |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message 1 |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ And user "participant2" deletes message "Message 1" from room "group room" with 200
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message deleted by author |
+ | group room | users | participant2 | participant2-displayname | Message deleted by author | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname"}} | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message deleted by you |
+ | group room | users | participant2 | participant2-displayname | Message deleted by you | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname"}} | |
+ Then user "participant1" received a system messages in room "group room" to delete "Message 1"
+ Then user "participant2" received a system messages in room "group room" to delete "Message 1"
+
+ Scenario: moderator deletes other user message
+ Given user "participant1" creates room "group room"
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds user "participant2" to room "group room" with 200
+ And user "participant2" sends message "Message 1" to room "group room" with 201
+ When user "participant1" sends reply "Message 1-1" on message "Message 1" to room "group room" with 201
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message 1 |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message 1 |
+ | group room | users | participant2 | participant2-displayname | Message 1 | [] | |
+ And user "participant1" deletes message "Message 1" from room "group room" with 200
+ Then user "participant1" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message deleted by you |
+ | group room | users | participant2 | participant2-displayname | Message deleted by you | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant2" sees the following messages in room "group room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
+ | group room | users | participant1 | participant1-displayname | Message 1-1 | [] | Message deleted by {actor} |
+ | group room | users | participant2 | participant2-displayname | Message deleted by {actor} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | |
+ Then user "participant1" received a system messages in room "group room" to delete "Message 1"
+ Then user "participant2" received a system messages in room "group room" to delete "Message 1"
diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php
index b53bf418e..690e74c71 100644
--- a/tests/php/CapabilitiesTest.php
+++ b/tests/php/CapabilitiesTest.php
@@ -65,6 +65,7 @@ class CapabilitiesTest extends TestCase {
'last-room-activity',
'no-ping',
'system-messages',
+ 'delete-messages',
'mention-flag',
'in-call-flags',
'notification-levels',
diff --git a/tests/php/Chat/Parser/SystemMessageTest.php b/tests/php/Chat/Parser/SystemMessageTest.php
index b857a0000..30c504406 100644
--- a/tests/php/Chat/Parser/SystemMessageTest.php
+++ b/tests/php/Chat/Parser/SystemMessageTest.php
@@ -399,10 +399,21 @@ class SystemMessageTest extends TestCase {
/** @var IComment|MockObject $comment */
$comment = $this->createMock(IComment::class);
+ if ($recipientId && strpos($recipientId, 'guest::') !== false) {
+ $comment->method('getActorType')
+ ->willReturn('guests');
+ $comment->method('getActorId')
+ ->willReturn(substr($recipientId, strlen('guest::')));
+ } else {
+ $comment->method('getActorType')
+ ->willReturn('users');
+ $comment->method('getActorId')
+ ->willReturn($recipientId);
+ }
- $parser = $this->getParser(['getActor', 'getUser', 'getGuest', 'parseCall', 'getFileFromShare']);
+ $parser = $this->getParser(['getActorFromComment', 'getUser', 'getGuest', 'parseCall', 'getFileFromShare']);
$parser->expects($this->once())
- ->method('getActor')
+ ->method('getActorFromComment')
->with($comment)
->willReturn(['id' => 'actor', 'type' => 'user']);
$parser->expects($this->any())
@@ -475,9 +486,9 @@ class SystemMessageTest extends TestCase {
/** @var IComment|MockObject $comment */
$comment = $this->createMock(IComment::class);
- $parser = $this->getParser(['getActor']);
+ $parser = $this->getParser(['getActorFromComment']);
$parser->expects($this->any())
- ->method('getActor')
+ ->method('getActorFromComment')
->with($comment)
->willReturn(['id' => 'actor', 'type' => 'user']);
@@ -808,7 +819,7 @@ class SystemMessageTest extends TestCase {
->willReturn($userData);
}
- $this->assertSame($expected, self::invokePrivate($parser, 'getActor', [$chatMessage]));
+ $this->assertSame($expected, self::invokePrivate($parser, 'getActorFromComment', [$chatMessage]));
}
public function dataGetUser(): array {
diff --git a/tests/php/Controller/ChatControllerTest.php b/tests/php/Controller/ChatControllerTest.php
index 1291d4b6b..30086496c 100644
--- a/tests/php/Controller/ChatControllerTest.php
+++ b/tests/php/Controller/ChatControllerTest.php
@@ -28,6 +28,7 @@ use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Controller\ChatController;
use OCA\Talk\GuestManager;
+use OCA\Talk\MatterbridgeManager;
use OCA\Talk\Model\Message;
use OCA\Talk\Participant;
use OCA\Talk\Room;
@@ -75,6 +76,8 @@ class ChatControllerTest extends TestCase {
protected $autoCompleteManager;
/** @var IUserStatusManager|MockObject */
protected $statusManager;
+ /** @var MatterbridgeManager|MockObject */
+ protected $matterbridgeManager;
/** @var SearchPlugin|MockObject */
protected $searchPlugin;
/** @var ISearchResult|MockObject */
@@ -109,6 +112,7 @@ class ChatControllerTest extends TestCase {
$this->messageParser = $this->createMock(MessageParser::class);
$this->autoCompleteManager = $this->createMock(IManager::class);
$this->statusManager = $this->createMock(IUserStatusManager::class);
+ $this->matterbridgeManager = $this->createMock(MatterbridgeManager::class);
$this->searchPlugin = $this->createMock(SearchPlugin::class);
$this->searchResult = $this->createMock(ISearchResult::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
@@ -142,6 +146,7 @@ class ChatControllerTest extends TestCase {
$this->messageParser,
$this->autoCompleteManager,
$this->statusManager,
+ $this->matterbridgeManager,
$this->searchPlugin,
$this->searchResult,
$this->timeFactory,