diff options
-rw-r--r-- | .drone.yml | 130 | ||||
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | appinfo/routes/routesReactionController.php | 41 | ||||
-rw-r--r-- | docs/capabilities.md | 1 | ||||
-rw-r--r-- | docs/chat.md | 2 | ||||
-rw-r--r-- | docs/index.md | 1 | ||||
-rw-r--r-- | docs/reaction.md | 66 | ||||
-rw-r--r-- | lib/Capabilities.php | 9 | ||||
-rw-r--r-- | lib/Chat/Notifier.php | 44 | ||||
-rw-r--r-- | lib/Chat/Parser/Listener.php | 12 | ||||
-rw-r--r-- | lib/Chat/Parser/ReactionParser.php | 52 | ||||
-rw-r--r-- | lib/Chat/ReactionManager.php | 153 | ||||
-rw-r--r-- | lib/Controller/ReactionController.php | 130 | ||||
-rw-r--r-- | lib/Exceptions/ReactionAlreadyExistsException.php | 29 | ||||
-rw-r--r-- | lib/Exceptions/ReactionNotSupportedException.php | 29 | ||||
-rw-r--r-- | lib/Exceptions/ReactionOutOfContextException.php | 29 | ||||
-rw-r--r-- | lib/Manager.php | 2 | ||||
-rw-r--r-- | lib/Model/Message.php | 2 | ||||
-rw-r--r-- | lib/Notification/Notifier.php | 23 | ||||
-rw-r--r-- | mkdocs.yml | 1 | ||||
-rw-r--r-- | tests/integration/features/bootstrap/FeatureContext.php | 63 | ||||
-rw-r--r-- | tests/integration/features/reaction/react.feature | 68 | ||||
-rw-r--r-- | tests/php/CapabilitiesTest.php | 11 | ||||
-rw-r--r-- | tests/php/Chat/NotifierTest.php | 50 |
24 files changed, 933 insertions, 16 deletions
diff --git a/.drone.yml b/.drone.yml index d25ce0823..78fc3fea9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -178,6 +178,43 @@ trigger: --- kind: pipeline +name: int-sqlite-reaction + +steps: + - name: integration-reaction + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + environment: + APP_NAME: spreed + CORE_BRANCH: master + GUESTS_BRANCH: master + DATABASEHOST: sqlite + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - git clone --depth 1 -b "$GUESTS_BRANCH" https://github.com/nextcloud/guests apps/guests + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/reaction + +services: + - name: cache + image: ghcr.io/nextcloud/continuous-integration-redis:latest + +trigger: + branch: + - master + - stable* + event: + - pull_request + - push + +--- +kind: pipeline name: int-sqlite-sharing steps: @@ -477,6 +514,53 @@ trigger: --- kind: pipeline +name: int-mysql-reaction + +steps: + - name: integration-reaction + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + environment: + APP_NAME: spreed + CORE_BRANCH: master + GUESTS_BRANCH: master + DATABASEHOST: mysql + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - git clone --depth 1 -b "$GUESTS_BRANCH" https://github.com/nextcloud/guests apps/guests + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/reaction + +services: + - name: cache + image: ghcr.io/nextcloud/continuous-integration-redis:latest + - name: mysql + image: ghcr.io/nextcloud/continuous-integration-mariadb-10.4:10.4 + environment: + MYSQL_ROOT_PASSWORD: owncloud + MYSQL_USER: oc_autotest + MYSQL_PASSWORD: owncloud + MYSQL_DATABASE: oc_autotest + command: [ "--innodb_large_prefix=true", "--innodb_file_format=barracuda", "--innodb_file_per_table=true" ] + tmpfs: + - /var/lib/mysql + +trigger: + branch: + - master + - stable* + event: +# - pull_request + - push + +--- +kind: pipeline name: int-mysql-sharing steps: @@ -791,6 +875,52 @@ trigger: --- kind: pipeline +name: int-pgsql-reaction + +steps: + - name: integration-reaction + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + environment: + APP_NAME: spreed + CORE_BRANCH: master + GUESTS_BRANCH: master + DATABASEHOST: pgsql + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - git clone --depth 1 -b "$GUESTS_BRANCH" https://github.com/nextcloud/guests apps/guests + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/reaction + +services: + - name: cache + image: ghcr.io/nextcloud/continuous-integration-redis:latest + - name: pgsql + image: ghcr.io/nextcloud/continuous-integration-postgres-13:postgres-13 + environment: + POSTGRES_USER: oc_autotest + POSTGRES_DB: oc_autotest_dummy + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PASSWORD: + tmpfs: + - /var/lib/postgresql/data + +trigger: + branch: + - master + - stable* + event: +# - pull_request + - push + +--- +kind: pipeline name: int-pgsql-sharing steps: diff --git a/appinfo/routes.php b/appinfo/routes.php index e888b7006..fde29617e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -36,6 +36,7 @@ return array_merge_recursive( include(__DIR__ . '/routes/routesMatterbridgeSettingsController.php'), include(__DIR__ . '/routes/routesPageController.php'), include(__DIR__ . '/routes/routesPublicShareAuthController.php'), + include(__DIR__ . '/routes/routesReactionController.php'), include(__DIR__ . '/routes/routesRoomController.php'), include(__DIR__ . '/routes/routesSettingsController.php'), include(__DIR__ . '/routes/routesSignalingController.php'), diff --git a/appinfo/routes/routesReactionController.php b/appinfo/routes/routesReactionController.php new file mode 100644 index 000000000..375478e3f --- /dev/null +++ b/appinfo/routes/routesReactionController.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +return [ + 'ocs' => [ + ['name' => 'Reaction#react', 'url' => '/api/{apiVersion}/reaction/{token}/{messageId}', 'verb' => 'POST', 'requirements' => [ + 'apiVersion' => 'v1', + 'token' => '^[a-z0-9]{4,30}$', + ]], + ['name' => 'Reaction#delete', 'url' => '/api/{apiVersion}/reaction/{token}/{messageId}', 'verb' => 'DELETE', 'requirements' => [ + 'apiVersion' => 'v1', + 'token' => '^[a-z0-9]{4,30}$', + ]], + ['name' => 'Reaction#getReactions', 'url' => '/api/{apiVersion}/reaction/{token}/{messageId}', 'verb' => 'GET', 'requirements' => [ + 'apiVersion' => 'v1', + 'token' => '^[a-z0-9]{4,30}$', + ]], + ], +]; diff --git a/docs/capabilities.md b/docs/capabilities.md index 489ec3416..9ef1de814 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -89,3 +89,4 @@ title: Capabilities ## 14 * `chat-unread` - Whether the API to mark a conversation as unread is available +* `reactions` - Api reactions to chat message diff --git a/docs/chat.md b/docs/chat.md index ff03979ef..07c3660b1 100644 --- a/docs/chat.md +++ b/docs/chat.md @@ -50,6 +50,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` `message` | string | Message string with placeholders (see [Rich Object String](https://github.com/nextcloud/server/issues/1706)) `messageParameters` | array | Message parameters for `message` (see [Rich Object String](https://github.com/nextcloud/server/issues/1706)) `parent` | array | **Optional:** See `Parent data` below + `reactions` | array | **Optional:** An array map with relation between reaction emoji and total count of reactions with this emoji #### Parent data @@ -324,3 +325,4 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob * `matterbridge_config_removed` - {actor} removed the Matterbridge configuration * `matterbridge_config_enabled` - {actor} started Matterbridge * `matterbridge_config_disabled` - {actor} stopped Matterbridge + diff --git a/docs/index.md b/docs/index.md index 0e19b823f..3e1dec1dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,7 @@ * [Participant API](participant.md) * [Call API](call.md) * [Chat API](chat.md) +* [Reaction API](reaction.md) * [Webinar API](webinar.md) * [Internal Signaling API](internal-signaling.md) * [Standalone Signaling API](https://nextcloud-spreed-signaling.readthedocs.io/en/latest/) diff --git a/docs/reaction.md b/docs/reaction.md new file mode 100644 index 000000000..5fc16e9f6 --- /dev/null +++ b/docs/reaction.md @@ -0,0 +1,66 @@ +# Reaction API + +Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` + +## React to a message + +* Required capability: `reactions` +* Method: `POST` +* Endpoint: `/reaction/{token}/{messageId}` +* Data: + + field | type | Description + ---|---|--- + `reaction` | string | the reaction emoji + +* Response: + - Status code: + + `200 OK` Reaction already exists + + `201 Created` + + `400 Bad Request` In case of no reaction support, message out of reactions context or any other error + + `404 Not Found` When the conversation or message to react could not be found for the participant + + `409 Conflict` User already did this reaction to this message + +## Delete a reaction + +* Required capability: `reactions` +* Method: `DELETE` +* Endpoint: `/reaction/{token}/{messageId}` +* Data: + + field | type | Description + ---|---|--- + `reaction` | string | the reaction emoji + +* Response: + - Status code: + + `201 Created` + + `400 Bad Request` In case of no reaction support, message out of reactions context or any other error + + `404 Not Found` When the conversation or message to react or reaction could not be found for the participant + +## Retrieve reactions of a message by type + +* Required capability: `reactions` +* Method: `GET` +* Endpoint: `/reaction/{token}/{messageId}` +* Data: + + field | type | Description + ---|---|--- + `reaction` | string | **Optional:** the reaction emoji + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` In case of no reaction support, message out of reactions context or any other error + + `404 Not Found` When the conversation or message to react could not be found for the participant + + - Data: + Array with data of reactions: + + field | type | Description + ---|---|--- + `actorType` | string | `guests` or `users` + `actorId` | string | Actor id of the reacting participant + `actorDisplayName` | string | Display name of the reaction author + `timestamp` | int | Timestamp in seconds and UTC time zone diff --git a/lib/Capabilities.php b/lib/Capabilities.php index c4742a38d..f8426d58f 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -27,6 +27,7 @@ namespace OCA\Talk; use OCA\Talk\Chat\ChatManager; use OCP\Capabilities\IPublicCapability; +use OCP\Comments\ICommentsManager; use OCP\IConfig; use OCP\IUser; use OCP\IUserSession; @@ -37,14 +38,18 @@ class Capabilities implements IPublicCapability { protected $serverConfig; /** @var Config */ protected $talkConfig; + /** @var ICommentsManager */ + protected $commentsManager; /** @var IUserSession */ protected $userSession; public function __construct(IConfig $serverConfig, Config $talkConfig, + ICommentsManager $commentsManager, IUserSession $userSession) { $this->serverConfig = $serverConfig; $this->talkConfig = $talkConfig; + $this->commentsManager = $commentsManager; $this->userSession = $userSession; } @@ -115,6 +120,10 @@ class Capabilities implements IPublicCapability { ], ]; + if ($this->commentsManager->supportReactions()) { + $capabilities['features'][] = 'reactions'; + } + if ($user instanceof IUser) { $capabilities['config']['attachments']['folder'] = $this->talkConfig->getAttachmentFolder($user->getUID()); $capabilities['config']['chat']['read-privacy'] = $this->talkConfig->getUserReadPrivacy($user->getUID()); diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index 83b45ac4c..e8428ebba 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -200,10 +200,11 @@ class Notifier { * @param Room $chat * @param IComment $comment * @param IComment $replyTo + * @param string $subject * @return array[] Actor that was replied to * @psalm-return array<int, array{id: string, type: string}> */ - public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo): array { + public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo, string $subject = 'reply'): array { if ($replyTo->getActorType() !== Attendee::ACTOR_USERS) { // No reply notification when the replyTo-author was not a user return []; @@ -213,7 +214,7 @@ class Notifier { return []; } - $notification = $this->createNotification($chat, $comment, 'reply'); + $notification = $this->createNotification($chat, $comment, $subject); $notification->setUser($replyTo->getActorId()); $this->notificationManager->notify($notification); @@ -266,6 +267,34 @@ class Notifier { } } + public function notifyReacted(Room $chat, IComment $comment, IComment $reaction): void { + if ($comment->getActorType() !== Attendee::ACTOR_USERS) { + return; + } + + if ($comment->getActorType() === $reaction->getActorType() && $comment->getActorId() === $reaction->getActorId()) { + return; + } + + $participant = $chat->getParticipant($comment->getActorId(), false); + $notificationLevel = $participant->getAttendee()->getNotificationLevel(); + if ($notificationLevel === Participant::NOTIFY_DEFAULT) { + if ($chat->getType() === Room::TYPE_ONE_TO_ONE) { + $notificationLevel = Participant::NOTIFY_ALWAYS; + } else { + $notificationLevel = $this->getDefaultGroupNotification(); + } + } + + if ($notificationLevel === Participant::NOTIFY_ALWAYS) { + $notification = $this->createNotification($chat, $comment, 'reaction', [ + 'reaction' => $reaction->getMessage(), + ]); + $notification->setUser($comment->getActorId()); + $this->notificationManager->notify($notification); + } + } + /** * Removes all the pending notifications for the room with the given ID. * @@ -363,17 +392,18 @@ class Notifier { * @param Room $chat * @param IComment $comment * @param string $subject + * @param array $subjectData * @return INotification */ - private function createNotification(Room $chat, IComment $comment, string $subject): INotification { + private function createNotification(Room $chat, IComment $comment, string $subject, array $subjectData = []): INotification { + $subjectData['userType'] = $comment->getActorType(); + $subjectData['userId'] = $comment->getActorId(); + $notification = $this->notificationManager->createNotification(); $notification ->setApp('spreed') ->setObject('chat', $chat->getToken()) - ->setSubject($subject, [ - 'userType' => $comment->getActorType(), - 'userId' => $comment->getActorId(), - ]) + ->setSubject($subject, $subjectData) ->setMessage($comment->getVerb(), [ 'commentId' => $comment->getId(), ]) diff --git a/lib/Chat/Parser/Listener.php b/lib/Chat/Parser/Listener.php index 83e26f673..ebb63fe22 100644 --- a/lib/Chat/Parser/Listener.php +++ b/lib/Chat/Parser/Listener.php @@ -100,6 +100,18 @@ class Listener { $dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function (ChatMessageEvent $event) { $chatMessage = $event->getMessage(); + if ($chatMessage->getMessageType() !== 'reaction' && $chatMessage->getMessageType() !== 'reaction_deleted') { + return; + } + + /** @var ReactionParser $parser */ + $parser = \OC::$server->get(ReactionParser::class); + $parser->parseMessage($chatMessage); + }); + + $dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function (ChatMessageEvent $event) { + $chatMessage = $event->getMessage(); + if ($chatMessage->getMessageType() !== 'comment_deleted') { return; } diff --git a/lib/Chat/Parser/ReactionParser.php b/lib/Chat/Parser/ReactionParser.php new file mode 100644 index 000000000..7e38e0f78 --- /dev/null +++ b/lib/Chat/Parser/ReactionParser.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Chat\Parser; + +use OCA\Talk\Model\Message; +use OCP\IL10N; + +class ReactionParser { + /** @var IL10N|null */ + private $l; + /** + * @param Message $message + * @throws \OutOfBoundsException + */ + public function parseMessage(Message $message): void { + $comment = $message->getComment(); + if (!in_array($comment->getVerb(), ['reaction', 'reaction_deleted'])) { + throw new \OutOfBoundsException('Not a reaction'); + } + $this->l = $message->getL10n(); + $message->setMessageType('system'); + if ($comment->getVerb() === 'reaction_deleted') { + // This message is necessary to make compatible with old clients + $message->setMessage($this->l->t('Message deleted by author'), [], $comment->getVerb()); + } else { + $message->setMessage($message->getMessage(), [], $comment->getVerb()); + } + } +} diff --git a/lib/Chat/ReactionManager.php b/lib/Chat/ReactionManager.php new file mode 100644 index 000000000..4b4576ea4 --- /dev/null +++ b/lib/Chat/ReactionManager.php @@ -0,0 +1,153 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Chat; + +use OCA\Talk\Exceptions\ReactionAlreadyExistsException; +use OCA\Talk\Exceptions\ReactionNotSupportedException; +use OCA\Talk\Exceptions\ReactionOutOfContextException; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Comments\NotFoundException; +use OCP\IL10N; + +class ReactionManager { + /** @var ICommentsManager|CommentsManager */ + private $commentsManager; + /** @var IL10N */ + private $l; + /** @var MessageParser */ + private $messageParser; + /** @var Notifier */ + private $notifier; + /** @var ITimeFactory */ + protected $timeFactory; + + public function __construct(CommentsManager $commentsManager, + IL10N $l, + MessageParser $messageParser, + Notifier $notifier, + ITimeFactory $timeFactory) { + $this->commentsManager = $commentsManager; + $this->l = $l; + $this->messageParser = $messageParser; + $this->notifier = $notifier; + $this->timeFactory = $timeFactory; + } + + public function addReactionMessage(Room $chat, Participant $participant, IComment $parentMessage, string $reaction): IComment { + try { + // Check if the user already reacted with the same reaction + $comment = $this->commentsManager->getReactionComment( + (int) $parentMessage->getId(), + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + $reaction + ); + throw new ReactionAlreadyExistsException(); + } catch (NotFoundException $e) { + } + + $comment = $this->commentsManager->create( + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + 'chat', + (string) $chat->getId() + ); + $comment->setParentId((string) $parentMessage->getId()); + $comment->setMessage($reaction); + $comment->setVerb('reaction'); + $this->commentsManager->save($comment); + + $this->notifier->notifyReacted($chat, $parentMessage, $comment); + return $comment; + } + + public function deleteReactionMessage(Participant $participant, int $messageId, string $reaction): IComment { + $comment = $this->commentsManager->getReactionComment( + $messageId, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + $reaction + ); + $comment->setMessage( + json_encode([ + 'deleted_by_type' => $participant->getAttendee()->getActorType(), + 'deleted_by_id' => $participant->getAttendee()->getActorId(), + 'deleted_on' => $this->timeFactory->getDateTime()->getTimestamp(), + ]) + ); + $comment->setVerb('reaction_deleted'); + $this->commentsManager->save($comment); + return $comment; + } + + public function retrieveReactionMessages(Room $chat, Participant $participant, int $messageId, ?string $reaction): array { + if ($reaction) { + $comments = $this->commentsManager->retrieveAllReactionsWithSpecificReaction($messageId, $reaction); + } else { + $comments = $this->commentsManager->retrieveAllReactions($messageId); + } + + foreach ($comments as $comment) { + $message = $this->messageParser->createMessage($chat, $participant, $comment, $this->l); + $this->messageParser->parseMessage($message); + + $reactions[$comment->getMessage()][] = [ + 'actorType' => $comment->getActorType(), + 'actorId' => $comment->getActorId(), + 'actorDisplayName' => $message->getActorDisplayName(), + 'timestamp' => $comment->getCreationDateTime()->getTimestamp(), + ]; + } + return $reactions; + } + + /** + * @param Room $chat + * @param string $messageId + * @return IComment + * @throws NotFoundException + * @throws ReactionNotSupportedException + * @throws ReactionOutOfContextException + */ + public function getCommentToReact(Room $chat, string $messageId): IComment { + if (!$this->commentsManager->supportReactions()) { + throw new ReactionNotSupportedException(); + } + $comment = $this->commentsManager->get($messageId); + + if ($comment->getObjectType() !== 'chat' + || $comment->getObjectId() !== (string) $chat->getId() + || $comment->getVerb() !== 'comment') { + throw new ReactionOutOfContextException(); + } + + return $comment; + } +} diff --git a/lib/Controller/ReactionController.php b/lib/Controller/ReactionController.php new file mode 100644 index 000000000..a16820ade --- /dev/null +++ b/lib/Controller/ReactionController.php @@ -0,0 +1,130 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Controller; + +use OCA\Talk\Chat\ReactionManager; +use OCA\Talk\Exceptions\ReactionAlreadyExistsException; +use OCA\Talk\Exceptions\ReactionNotSupportedException; +use OCA\Talk\Exceptions\ReactionOutOfContextException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\Comments\NotFoundException; +use OCP\IRequest; + +class ReactionController extends AEnvironmentAwareController { + /** @var ReactionManager */ + private $reactionManager; + + public function __construct(string $appName, + IRequest $request, + ReactionManager $reactionManager) { + parent::__construct($appName, $request); + $this->reactionManager = $reactionManager; + } + + /** + * @NoAdminRequired + * @RequireParticipant + * @RequireReadWriteConversation + * @RequireModeratorOrNoLobby + * + * @param int $messageId for reaction + * @param string $reaction the reaction emoji + * @return DataResponse + */ + public function react(int $messageId, string $reaction): DataResponse { + try { + $chat = $this->getRoom(); + $participant = $this->getParticipant(); + $parentMessage = $this->reactionManager->getCommentToReact($chat, (string) $messageId); + $this->reactionManager->addReactionMessage($chat, $participant, $parentMessage, $reaction); + } catch (NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (ReactionAlreadyExistsException $e) { + return new DataResponse([], Http::STATUS_OK); + } catch (ReactionNotSupportedException | ReactionOutOfContextException | \Exception $e) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + return new DataResponse([], Http::STATUS_CREATED); + } + + /** + * @NoAdminRequired + * @RequireParticipant + * @RequireReadWriteConversation + * @RequireModeratorOrNoLobby + * + * @param int $messageId for reaction + * @param string $reaction the reaction emoji + * @return DataResponse + */ + public function delete(int $messageId, string $reaction): DataResponse { + $participant = $this->getParticipant(); + try { + // Verify that messageId is part of the room + $this->reactionManager->getCommentToReact($this->getRoom(), (string) $messageId); + } catch (ReactionNotSupportedException | ReactionOutOfContextException | NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $this->reactionManager->deleteReactionMessage( + $participant, + $messageId, + $reaction + ); + } catch (NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (\Exception $e) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([], Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @RequireParticipant + * @RequireReadWriteConversation + * @RequireModeratorOrNoLobby + * + * @param int $messageId for reaction + * @param string|null $reaction the reaction emoji + * @return DataResponse + */ + public function getReactions(int $messageId, ?string $reaction): DataResponse { + try { + // Verify that messageId is part of the room + $this->reactionManager->getCommentToReact($this->getRoom(), (string) $messageId); + } catch (ReactionNotSupportedException | ReactionOutOfContextException | NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + $reactions = $this->reactionManager->retrieveReactionMessages($this->getRoom(), $this->getParticipant(), $messageId, $reaction); + + return new DataResponse($reactions, Http::STATUS_OK); + } +} diff --git a/lib/Exceptions/ReactionAlreadyExistsException.php b/lib/Exceptions/ReactionAlreadyExistsException.php new file mode 100644 index 000000000..399856d9b --- /dev/null +++ b/lib/Exceptions/ReactionAlreadyExistsException.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Exceptions; + +class ReactionAlreadyExistsException extends \OutOfBoundsException { +} diff --git a/lib/Exceptions/ReactionNotSupportedException.php b/lib/Exceptions/ReactionNotSupportedException.php new file mode 100644 index 000000000..478f6a80a --- /dev/null +++ b/lib/Exceptions/ReactionNotSupportedException.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022, Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Exceptions; + +class ReactionNotSupportedException extends \Exception { +} diff --git a/lib/Exceptions/ReactionOutOfContextException.php b/lib/Exceptions/ReactionOutOfContextException.php new file mode 100644 index 000000000..499dee69b --- /dev/null +++ b/lib/Exceptions/ReactionOutOfContextException.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022, Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Exceptions; + +class ReactionOutOfContextException extends \Exception { +} diff --git a/lib/Manager.php b/lib/Manager.php index c3bb42004..993ade134 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -233,6 +233,7 @@ class Manager { 'reference_id' => $row['comment_reference_id'] ?? null, 'creation_timestamp' => $row['comment_creation_timestamp'], 'latest_child_timestamp' => $row['comment_latest_child_timestamp'], + 'reactions' => $row['comment_reactions'], ]); } @@ -1185,5 +1186,6 @@ class Manager { } $query->selectAlias('c.creation_timestamp', 'comment_creation_timestamp'); $query->selectAlias('c.latest_child_timestamp', 'comment_latest_child_timestamp'); + $query->selectAlias('c.reactions', 'comment_reactions'); } } diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 30f1fa6d9..118adad72 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -163,6 +163,7 @@ class Message { return $this->getMessageType() !== 'system' && $this->getMessageType() !== 'command' && $this->getMessageType() !== 'comment_deleted' && + $this->getMessageType() !== 'reaction' && \in_array($this->getActorType(), [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS]); } @@ -180,6 +181,7 @@ class Message { 'messageType' => $this->getMessageType(), 'isReplyable' => $this->isReplyable(), 'referenceId' => (string) $this->getComment()->getReferenceId(), + 'reactions' => $this->getComment()->getReactions(), ]; if ($this->getMessageType() === 'comment_deleted') { diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index e1100880f..529df5198 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -260,7 +260,7 @@ class Notifier implements INotifier { } return $this->parseCall($notification, $room, $l); } - if ($subject === 'reply' || $subject === 'mention' || $subject === 'chat') { + if ($subject === 'reply' || $subject === 'mention' || $subject === 'chat' || $subject === 'reaction') { return $this->parseChatMessage($notification, $room, $participant, $l); } @@ -456,6 +456,27 @@ class Notifier implements INotifier { $subject = $l->t('A guest replied to your message in conversation {call}'); } } + } elseif ($notification->getSubject() === 'reaction') { + $richSubjectParameters['reaction'] = [ + 'type' => 'highlight', + 'id' => $subjectParameters['reaction'], + 'name' => $subjectParameters['reaction'], + ]; + + if ($room->getType() === Room::TYPE_ONE_TO_ONE) { + $subject = $l->t('{user} reacted with {reaction} to your private message'); + } elseif ($richSubjectUser) { + $subject = $l->t('{user} reacted with {reaction} to your message in conversation {call}'); + } elseif (!$isGuest) { + $subject = $l->t('A deleted user reacted with {reaction} to your message in conversation {call}'); + } else { + try { + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $comment->getActorId()); + $subject = $l->t('{guest} (guest) reacted with {reaction} to your message in conversation {call}'); + } catch (ParticipantNotFoundException $e) { + $subject = $l->t('A guest reacted with {reaction} to your message in conversation {call}'); + } + } } elseif ($room->getType() === Room::TYPE_ONE_TO_ONE) { $subject = $l->t('{user} mentioned you in a private conversation'); } elseif ($richSubjectUser) { diff --git a/mkdocs.yml b/mkdocs.yml index 28860dae3..d83858d64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - 'Participants management': 'participant.md' - 'Call management': 'call.md' - 'Chat management': 'chat.md' + - 'Reaction management': 'reaction.md' - 'Webinar management': 'webinar.md' - 'Settings': 'settings.md' - 'Integration by other apps': 'integration.md' diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 58814dd15..3bdca6641 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -447,8 +447,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { return $attendee; }, $result); - usort($expected, [$this, 'sortAttendees']); - usort($result, [$this, 'sortAttendees']); + usort($expected, [self::class, 'sortAttendees']); + usort($result, [self::class, 'sortAttendees']); Assert::assertEquals($expected, $result); } else { @@ -477,7 +477,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { } } - protected function sortAttendees(array $a1, array $a2): int { + protected static function sortAttendees(array $a1, array $a2): int { if (array_key_exists('participantType', $a1) && array_key_exists('participantType', $a2) && $a1['participantType'] !== $a2['participantType']) { return $a1['participantType'] <=> $a2['participantType']; } @@ -1559,6 +1559,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { } $includeParents = in_array('parentMessage', $formData->getRow(0), true); $includeReferenceId = in_array('referenceId', $formData->getRow(0), true); + $includeReactions = in_array('reactions', $formData->getRow(0), true); $count = count($formData->getHash()); Assert::assertCount($count, $messages, 'Message count does not match'); @@ -1567,7 +1568,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { $messages[$i]['messageParameters'] = 'IGNORE'; } } - Assert::assertEquals($formData->getHash(), array_map(function ($message) use ($includeParents, $includeReferenceId) { + Assert::assertEquals($formData->getHash(), array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions) { $data = [ 'room' => self::$tokenToIdentifier[$message['token']], 'actorType' => $message['actorType'], @@ -1584,6 +1585,9 @@ class FeatureContext implements Context, SnippetAcceptingContext { if ($includeReferenceId) { $data['referenceId'] = $message['referenceId']; } + if ($includeReactions) { + $data['reactions'] = json_encode($message['reactions'], JSON_UNESCAPED_UNICODE); + } return $data; }, $messages)); } @@ -2079,6 +2083,57 @@ class FeatureContext implements Context, SnippetAcceptingContext { $this->setCurrentUser($currentUser); } + /** + * @Given /^user "([^"]*)" (delete react|react) with "([^"]*)" on message "([^"]*)" to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ + */ + public function userReactWithOnMessageToRoomWith(string $user, string $action, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1'): void { + $token = self::$identifierToToken[$identifier]; + $messageId = self::$messages[$message]; + $this->setCurrentUser($user); + $verb = $action === 'react' ? 'POST' : 'DELETE'; + $this->sendRequest($verb, '/apps/spreed/api/' . $apiVersion . '/reaction/' . $token . '/' . $messageId, [ + 'reaction' => $reaction + ]); + $this->assertStatusCode($this->response, $statusCode); + } + + /** + * @Given /^user "([^"]*)" retrieve reactions "([^"]*)" of message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ + */ + public function userRetrieveReactionsOfMessageInRoomWith(string $user, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', TableNode $formData): void { + $token = self::$identifierToToken[$identifier]; + $messageId = self::$messages[$message]; + $this->setCurrentUser($user); + $reaction = $reaction !== 'all' ? '?reaction=' . $reaction : ''; + $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/reaction/' . $token . '/' . $messageId . $reaction); + $this->assertStatusCode($this->response, $statusCode); + $this->assertReactionList($formData); + } + + private function assertReactionList(TableNode $formData): void { + $expected = []; + foreach ($formData->getHash() as $row) { + $reaction = $row['reaction']; + unset($row['reaction']); + $expected[$reaction][] = $row; + } + + $result = $this->getDataFromResponse($this->response); + $result = array_map(static function ($reaction, $list) use ($expected): array { + $list = array_map(function ($reaction) { + unset($reaction['timestamp']); + return $reaction; + }, $list); + Assert::assertCount(count($list), $expected[$reaction], 'Reaction count by type does not match'); + + usort($expected[$reaction], [self::class, 'sortAttendees']); + usort($list, [self::class, 'sortAttendees']); + Assert::assertEquals($expected[$reaction], $list, 'Reaction list by type does not match'); + return $list; + }, array_keys($result), array_values($result)); + Assert::assertCount(count($expected), $result, 'Reaction count does not match'); + } + /* * Requests */ diff --git a/tests/integration/features/reaction/react.feature b/tests/integration/features/reaction/react.feature new file mode 100644 index 000000000..e95805816 --- /dev/null +++ b/tests/integration/features/reaction/react.feature @@ -0,0 +1,68 @@ +Feature: reaction/react + Background: + Given user "participant1" exists + Given user "participant2" exists + + Scenario: React to message with success + Given user "participant1" creates room "room" (v4) + | roomType | 3 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + And user "participant1" sends message "Message 1" to room "room" with 201 + And user "participant2" react with "👍" on message "Message 1" to room "room" with 201 + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | + | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":1} | + And user "participant1" react with "👍" on message "Message 1" to room "room" with 201 + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | + | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":2} | + + Scenario: React two times to same message with the same reaction + Given user "participant1" creates room "room" (v4) + | roomType | 3 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + And user "participant1" sends message "Message 1" to room "room" with 201 + And user "participant2" react with "👍" on message "Message 1" to room "room" with 201 + And user "participant2" react with "👍" on message "Message 1" to room "room" with 200 + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | + | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":1} | + + Scenario: Delete reaction to message with success + Given user "participant1" creates room "room" (v4) + | roomType | 3 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + And user "participant1" sends message "Message 1" to room "room" with 201 + And user "participant2" react with "👍" on message "Message 1" to room "room" with 201 + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | + | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":1} | + And user "participant2" delete react with "👍" on message "Message 1" to room "room" with 200 + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | + | room | users | participant1 | participant1-displayname | Message 1 | [] | [] | + + Scenario: Retrieve reactions of a message + Given user "participant1" creates room "room" (v4) + | roomType | 3 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + And user "participant1" sends message "Message 1" to room "room" with 201 + And user "participant1" react with "👍" on message "Message 1" to room "room" with 201 + And user "participant2" react with "👍" on message "Message 1" to room "room" with 201 + Then user "participant1" retrieve reactions "👍" of message "Message 1" in room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | + | users | participant1 | participant1-displayname | 👍 | + | users | participant2 | participant2-displayname | 👍 | + And user "participant2" react with "👎" on message "Message 1" to room "room" with 201 + And user "participant1" retrieve reactions "👎" of message "Message 1" in room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | + | users | participant2 | participant2-displayname | 👎 | + And user "participant1" retrieve reactions "all" of message "Message 1" in room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | + | users | participant1 | participant1-displayname | 👍 | + | users | participant2 | participant2-displayname | 👎 | + | users | participant2 | participant2-displayname | 👍 | diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index 7edc4f080..a247a18bc 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -26,6 +26,7 @@ declare(strict_types=1); namespace OCA\Talk\Tests\Unit; use OCA\Talk\Capabilities; +use OCA\Talk\Chat\CommentsManager; use OCA\Talk\Config; use OCA\Talk\Participant; use OCP\Capabilities\IPublicCapability; @@ -41,6 +42,8 @@ class CapabilitiesTest extends TestCase { protected $serverConfig; /** @var Config|MockObject */ protected $talkConfig; + /** @var CommentsManager|MockObject */ + protected $commentsManager; /** @var IUserSession|MockObject */ protected $userSession; /** @var array */ @@ -50,7 +53,11 @@ class CapabilitiesTest extends TestCase { parent::setUp(); $this->serverConfig = $this->createMock(IConfig::class); $this->talkConfig = $this->createMock(Config::class); + $this->commentsManager = $this->createMock(CommentsManager::class); $this->userSession = $this->createMock(IUserSession::class); + $this->commentsManager->expects($this->any()) + ->method('supportReactions') + ->willReturn(true); $this->baseFeatures = [ 'audio', @@ -96,6 +103,7 @@ class CapabilitiesTest extends TestCase { 'direct-mention-flag', 'notification-calls', 'conversation-permissions', + 'reactions', ]; } @@ -103,6 +111,7 @@ class CapabilitiesTest extends TestCase { $capabilities = new Capabilities( $this->serverConfig, $this->talkConfig, + $this->commentsManager, $this->userSession ); @@ -160,6 +169,7 @@ class CapabilitiesTest extends TestCase { $capabilities = new Capabilities( $this->serverConfig, $this->talkConfig, + $this->commentsManager, $this->userSession ); @@ -246,6 +256,7 @@ class CapabilitiesTest extends TestCase { $capabilities = new Capabilities( $this->serverConfig, $this->talkConfig, + $this->commentsManager, $this->userSession ); diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php index f3fc6df01..2d09b9cb9 100644 --- a/tests/php/Chat/NotifierTest.php +++ b/tests/php/Chat/NotifierTest.php @@ -129,20 +129,24 @@ class NotifierTest extends TestCase { /** * @return Room|MockObject */ - private function getRoom() { + private function getRoom($settings = []) { /** @var Room|MockObject */ $room = $this->createMock(Room::class); $room->expects($this->any()) ->method('getParticipant') - ->willReturnCallback(function (string $actorId) use ($room): Participant { + ->willReturnCallback(function (string $actorId) use ($room, $settings): Participant { if ($actorId === 'userNotInOneToOneChat') { throw new ParticipantNotFoundException(); } - $attendee = Attendee::fromRow([ + $attendeeRow = [ 'actor_type' => 'user', 'actor_id' => $actorId, - ]); + ]; + if (isset($settings['attendee'][$actorId])) { + $attendeeRow = array_merge($attendeeRow, $settings['attendee'][$actorId]); + } + $attendee = Attendee::fromRow($attendeeRow); return new Participant($room, $attendee, null); }); @@ -374,6 +378,44 @@ class NotifierTest extends TestCase { } /** + * @dataProvider dataNotifyReacted + */ + public function testNotifyReacted(int $notify, int $notifyType, int $roomType, string $authorId): void { + $this->notificationManager->expects($this->exactly($notify)) + ->method('notify'); + + $room = $this->getRoom([ + 'attendee' => [ + 'testUser' => [ + 'notificationLevel' => $notifyType, + ] + ] + ]); + $room->method('getType') + ->willReturn($roomType); + $comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), 'message'); + $reaction = $this->newComment('108', 'users', $authorId, new \DateTime('@' . 1000000016), 'message'); + + $notifier = $this->getNotifier([]); + $notifier->notifyReacted($room, $comment, $reaction); + } + + public function dataNotifyReacted(): array { + return [ + 'author react to own message' => + [0, Participant::NOTIFY_MENTION, Room::TYPE_GROUP, 'testUser'], + 'notify never' => + [0, Participant::NOTIFY_NEVER, Room::TYPE_GROUP, 'testUser2'], + 'notify default, not one to one' => + [0, Participant::NOTIFY_DEFAULT, Room::TYPE_GROUP, 'testUser2'], + 'notify default, one to one' => + [1, Participant::NOTIFY_DEFAULT, Room::TYPE_ONE_TO_ONE, 'testUser2'], + 'notify always' => + [1, Participant::NOTIFY_ALWAYS, Room::TYPE_GROUP, 'testUser2'], + ]; + } + + /** * @dataProvider dataGetMentionedUsers */ public function testGetMentionedUsers(string $message, array $expectedReturn): void { |