diff --git a/appinfo/info.xml b/appinfo/info.xml index 49d3b8a1ba9..29ceaa2c6bb 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -24,6 +24,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m Jan-Christoph Borchardt Jennifer Piperek Joas Schilling + Mario Danic Marco Ambrosini Talk diff --git a/css/icons.scss b/css/icons.scss index d7ecf01d96b..5b3077858aa 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -33,6 +33,9 @@ .icon-changelog { background-image: url('../img/changelog.svg'); } + .icon-notes { + background-image: url('../img/notes.svg'); + } // "forced-white" needs to be included in the class name as the UserBubble // does not accept several classes. diff --git a/docs/capabilities.md b/docs/capabilities.md index 03c878d7d75..f87a75fdeee 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -51,3 +51,4 @@ title: Capabilities * `force-mute` - "forceMute" signaling messages can be sent to mute other participants. * `conversation-v2` - The conversations API v2 is less load heavy and should be used by clients when available. Check the difference in the [Conversation API documentation](conversation.md). * `chat-reference-id` - an optional referenceId can be sent with a chat message to be able to identify it in parallel get requests to earlier fade out a temporary message +* `notes` - The notes conversation type is available and users can toggle on and off the conversation diff --git a/docs/constants.md b/docs/constants.md index 86ecff848d0..e2d109e321c 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -7,6 +7,7 @@ title: Constants * `2` group * `3` public * `4` changelog +* `5` notes ## Read-only states * `0` read-write @@ -35,7 +36,7 @@ title: Constants ## Actor types of chat messages * `guests` - guest users * `users` - logged-in users -* `bots` - used by commands (actor-id is the used `/command`) and the changelog conversation (actor-id is `changelog`) +* `bots` - used by commands (actor-id is the used `/command`), the changelog conversation (actor-id is `changelog`) and the notes conversation (actor-id is `notes`) ## Webinary lobby states * `0` no lobby diff --git a/docs/conversation.md b/docs/conversation.md index 254179803c5..2ca14b365f8 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -55,7 +55,7 @@ field | type | Description ------|------|------------ - `roomType` | int | + `roomType` | int | See [list of conversation types](constants.md#Conversation-types), but changelog and notes are not supported on this endpoint `invite` | string | user id (`roomType = 1`), group id (`roomType = 2` - optional), circle id (`roomType = 2`, `source = 'circles'`], only available with `circles-support` capability)) `source` | string | The source for the invite, only supported on `roomType = 2` for `groups` and `circles` (only available with `circles-support` capability) `roomName` | string | conversation name (Not available for `roomType = 1`) diff --git a/img/notes.svg b/img/notes.svg new file mode 100644 index 00000000000..5e4233a9f73 --- /dev/null +++ b/img/notes.svg @@ -0,0 +1,60 @@ + +image/svg+xml diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index fcdff6582aa..8553dca65c1 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -24,7 +24,7 @@ use OCA\Talk\Activity\Listener as ActivityListener; use OCA\Talk\Capabilities; -use OCA\Talk\Chat\Changelog\Listener as ChangelogListener; +use OCA\Talk\Chat\SpecialRoom\Listener as SpecialRoomListener; use OCA\Talk\Chat\ChatManager; use OCA\Talk\Chat\Command\Listener as CommandListener; use OCA\Talk\Chat\Parser\Listener as ParserListener; @@ -103,7 +103,7 @@ public function register(): void { CommandListener::register($dispatcher); CollaboratorsListener::register($dispatcher); ResourceListener::register($dispatcher); - ChangelogListener::register($dispatcher); + SpecialRoomListener::register($dispatcher); ShareListener::register($dispatcher); Operation::register($dispatcher); diff --git a/lib/BackgroundJob/RemoveEmptyRooms.php b/lib/BackgroundJob/RemoveEmptyRooms.php index 592a59a8e6c..05641815a02 100644 --- a/lib/BackgroundJob/RemoveEmptyRooms.php +++ b/lib/BackgroundJob/RemoveEmptyRooms.php @@ -63,7 +63,7 @@ protected function run($argument): void { } public function callback(Room $room): void { - if ($room->getType() === Room::CHANGELOG_CONVERSATION) { + if ($room->getType() === Room::CHANGELOG_CONVERSATION || $room->getType() === Room::NOTES_CONVERSATION) { return; } diff --git a/lib/Capabilities.php b/lib/Capabilities.php index caa5f86c38e..a2156842309 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -32,17 +32,13 @@ class Capabilities implements IPublicCapability { - /** @var IConfig */ - protected $serverConfig; /** @var Config */ protected $talkConfig; /** @var IUserSession */ protected $userSession; - public function __construct(IConfig $serverConfig, - Config $talkConfig, + public function __construct(Config $talkConfig, IUserSession $userSession) { - $this->serverConfig = $serverConfig; $this->talkConfig = $talkConfig; $this->userSession = $userSession; } @@ -53,23 +49,7 @@ public function getCapabilities(): array { return []; } - $maxChatLength = 1000; - if (version_compare($this->serverConfig->getSystemValueString('version', '0.0.0'), '16.0.2', '>=')) { - $maxChatLength = ChatManager::MAX_CHAT_LENGTH; - } - - $attachments = [ - 'allowed' => $user instanceof IUser, - ]; - if ($user instanceof IUser) { - $attachments['folder'] = $this->talkConfig->getAttachmentFolder($user->getUID()); - } - - $conversations = [ - 'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user), - ]; - - return [ + $capabilities = [ 'spreed' => [ 'features' => [ 'audio', @@ -99,13 +79,30 @@ public function getCapabilities(): array { 'chat-reference-id', ], 'config' => [ - 'attachments' => $attachments, + 'attachments' => [ + 'allowed' => false, + ], 'chat' => [ - 'max-length' => $maxChatLength, + 'max-length' => ChatManager::MAX_CHAT_LENGTH, + ], + 'conversations' => [ + 'can-create' => false ], - 'conversations' => $conversations, ], ], ]; + + if ($user instanceof IUser) { + $capabilities['spreed']['features'][] = 'notes'; + + $capabilities['spreed']['config']['attachments'] = [ + 'allowed' => true, + 'folder' => $this->talkConfig->getAttachmentFolder($user->getUID()), + ]; + + $capabilities['spreed']['config']['conversations']['can-create'] = !$this->talkConfig->isNotAllowedToCreateConversations($user); + } + + return $capabilities; } } diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index aa704f5ef5f..f94b3454def 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -115,8 +115,8 @@ public function addSystemMessage(Room $chat, string $actorType, string $actorId, * @param string $message * @return IComment */ - public function addChangelogMessage(Room $chat, string $message): IComment { - $comment = $this->commentsManager->create('guests', 'changelog', 'chat', (string) $chat->getId()); + public function addSpecialMessage(Room $chat, string $type, string $message): IComment { + $comment = $this->commentsManager->create('guests', $type, 'chat', (string) $chat->getId()); $comment->setMessage($message, self::MAX_CHAT_LENGTH); $comment->setCreationDateTime($this->timeFactory->getDateTime()); diff --git a/lib/Chat/Parser/Listener.php b/lib/Chat/Parser/Listener.php index fbda032e6df..11d2c26ecf7 100644 --- a/lib/Chat/Parser/Listener.php +++ b/lib/Chat/Parser/Listener.php @@ -50,13 +50,23 @@ public static function register(IEventDispatcher $dispatcher): void { } /** @var Changelog $parser */ - $parser = \OC::$server->query(Changelog::class); + $changelogParser = \OC::$server->query(Changelog::class); try { - $parser->parseMessage($message); + $changelogParser->parseMessage($message); + $event->stopPropagation(); + } catch (\OutOfBoundsException $e) { + // Unknown message, ignore + } + + /** @var Notes $parser */ + $notesParser = \OC::$server->query(Notes::class); + try { + $notesParser->parseMessage($message); $event->stopPropagation(); } catch (\OutOfBoundsException $e) { // Unknown message, ignore } + }, -75); $dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function(ChatMessageEvent $event) { diff --git a/lib/Chat/Parser/Notes.php b/lib/Chat/Parser/Notes.php new file mode 100644 index 00000000000..5bc8c4f993b --- /dev/null +++ b/lib/Chat/Parser/Notes.php @@ -0,0 +1,43 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Chat\Parser; + +use OCA\Talk\Model\Message; + +class Notes { + + /** + * @param Message $chatMessage + * @throws \OutOfBoundsException + */ + public function parseMessage(Message $chatMessage): void { + + if ($chatMessage->getActorType() !== 'guests' || + $chatMessage->getActorId() !== 'notes') { + throw new \OutOfBoundsException('Not notes'); + } + + $l = $chatMessage->getL10n(); + $chatMessage->setActor('bots', 'notes', $l->t('My notes')); + } +} diff --git a/lib/Chat/Changelog/Listener.php b/lib/Chat/SpecialRoom/Listener.php similarity index 94% rename from lib/Chat/Changelog/Listener.php rename to lib/Chat/SpecialRoom/Listener.php index 8ce066b27df..8cd3a21b02a 100644 --- a/lib/Chat/Changelog/Listener.php +++ b/lib/Chat/SpecialRoom/Listener.php @@ -20,7 +20,7 @@ * */ -namespace OCA\Talk\Chat\Changelog; +namespace OCA\Talk\Chat\SpecialRoom; use OCA\Talk\Controller\RoomController; use OCA\Talk\Events\UserEvent; @@ -46,6 +46,8 @@ public function __construct(Manager $manager) { } public function preGetRooms(string $userId): void { + $this->manager->createNotesIfNeeded($userId); + if (!$this->manager->userHasNewChangelog($userId)) { return; } diff --git a/lib/Chat/Changelog/Manager.php b/lib/Chat/SpecialRoom/Manager.php similarity index 70% rename from lib/Chat/Changelog/Manager.php rename to lib/Chat/SpecialRoom/Manager.php index 6b3371289c3..cfb30030894 100644 --- a/lib/Chat/Changelog/Manager.php +++ b/lib/Chat/SpecialRoom/Manager.php @@ -20,15 +20,18 @@ * */ -namespace OCA\Talk\Chat\Changelog; +namespace OCA\Talk\Chat\SpecialRoom; use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Room; use OCA\Talk\Manager as RoomManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IL10N; + class Manager { /** @var IConfig */ @@ -54,16 +57,59 @@ public function __construct(IConfig $config, $this->l = $l; } + public function getNotesForUser(string $userId): int { + return (int) $this->config->getUserValue($userId, 'spreed', 'notes', '2'); + } + public function getChangelogForUser(string $userId): int { - return (int) $this->config->getUserValue($userId, 'spreed', 'changelog', 0); + return (int) $this->config->getUserValue($userId, 'spreed', 'changelog', '0'); } public function userHasNewChangelog(string $userId): bool { return $this->getChangelogForUser($userId) < count($this->getChangelogs()); } + public function createNotesIfNeeded(string $userId): void { + if ($this->getNotesForUser($userId) === 2) { + $room = $this->roomManager->getSpecialRoom($userId, Room::NOTES_CONVERSATION); + $this->setNotesConversationAsFavorite($room, $userId); + $this->addNotesWelcomeMessages($room, $userId); + } + } + + public function setNotesConversationAsFavorite(Room $room, string $userId): void { + try { + $participant = $room->getParticipant($userId); + $participant->setFavorite(true); + } catch (ParticipantNotFoundException $e) { + // do nothing + } + } + + public function addNotesWelcomeMessages(Room $room, string $userId): void { + $notesWelcomeMessages = $this->getNotesWelcomeMessages(); + foreach ($notesWelcomeMessages as $key => $welcomeMessage) { + if ($welcomeMessage === '') { + continue; + } + $this->chatManager->addSpecialMessage($room, 'notes', $welcomeMessage); + } + + $this->config->setUserValue($userId, 'spreed', 'notes', 1); + } + + public function getNotesWelcomeMessages(): array { + return [ + $this->l->t( + "Welcome to your notes!" + . "\nYou can use this conversation to share notes between your different devices." + . " When you deleted it, you can recreate it via the settings." + ) + ]; + } + public function updateChangelog(string $userId): void { - $room = $this->roomManager->getChangelogRoom($userId); + $room = $this->roomManager->getSpecialRoom($userId, Room::CHANGELOG_CONVERSATION); $logs = $this->getChangelogs(); $hasReceivedLog = $this->getChangelogForUser($userId); @@ -72,7 +118,7 @@ public function updateChangelog(string $userId): void { if ($key < $hasReceivedLog || $changelog === '') { continue; } - $this->chatManager->addChangelogMessage($room, $changelog); + $this->chatManager->addSpecialMessage($room, 'changelog', $changelog); } $this->config->setUserValue($userId, 'spreed', 'changelog', count($this->getChangelogs())); diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 6eee773a280..0a3ddaacfef 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -490,6 +490,11 @@ protected function formatRoomV2(Room $room, ?Participant $currentParticipant): a && $currentParticipant->hasModeratorPermissions(false); $roomData['canLeaveConversation'] = !$roomData['canDeleteConversation'] || ($room->getType() !== Room::ONE_TO_ONE_CALL && $room->getNumberOfParticipants() > 1); + + if ($room->getType() === Room::NOTES_CONVERSATION) { + $roomData['canDeleteConversation'] = true; + $roomData['canLeaveConversation'] = false; + } } // FIXME This should not be done, but currently all the clients use it to get the avatar of the user … @@ -1032,7 +1037,7 @@ protected function removeSelfFromRoomLogic(Room $room, Participant $participant) && $room->getNumberOfModerators() === 1) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } - } else if ($room->getType() !== Room::CHANGELOG_CONVERSATION && + } else if ($room->getType() !== Room::CHANGELOG_CONVERSATION && $room->getType() !== Room::NOTES_CONVERSATION && $room->getNumberOfParticipants() === 1) { $room->deleteRoom(); return new DataResponse(); diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 8b96818dc1c..f465bd7e95e 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -25,6 +25,10 @@ namespace OCA\Talk\Controller; use OCA\Files_Sharing\SharedStorage; +use OCA\Talk\Chat\SpecialRoom\Manager as SpecialRoomManager; +use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Manager; +use OCA\Talk\Room; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; @@ -38,6 +42,10 @@ class SettingsController extends OCSController { + /** @var Manager */ + protected $talkManager; + /** @var SpecialRoomManager */ + protected $specialRoomManager; /** @var IRootFolder */ protected $rootFolder; /** @var IConfig */ @@ -49,11 +57,15 @@ class SettingsController extends OCSController { public function __construct(string $appName, IRequest $request, + Manager $talkManager, + SpecialRoomManager $specialRoomManager, IRootFolder $rootFolder, IConfig $config, ILogger $logger, ?string $userId) { parent::__construct($appName, $request); + $this->talkManager = $talkManager; + $this->specialRoomManager = $specialRoomManager; $this->rootFolder = $rootFolder; $this->config = $config; $this->logger = $logger; @@ -72,8 +84,26 @@ public function setUserSetting(string $key, ?string $value): DataResponse { return new DataResponse([], Http::STATUS_BAD_REQUEST); } + if ($key === 'notes' && $value === '1') { + // Setting it to 2 so createNotesIfNeeded can create the conversation and set it to 1 again + $value = '2'; + } + $this->config->setUserValue($this->userId, 'spreed', $key, $value); + + if ($key === 'notes') { + if ($value === '0') { + try { + $room = $this->talkManager->getSpecialRoom($this->userId, Room::NOTES_CONVERSATION, false); + $room->deleteRoom(); + } catch (RoomNotFoundException $e) { + } + } else { + $this->specialRoomManager->createNotesIfNeeded($this->userId); + } + } + return new DataResponse(); } @@ -95,6 +125,9 @@ protected function validateUserSetting(string $setting, ?string $value): bool { } return false; } + if ($setting === 'notes') { + return $value === '0' || $value === '1'; + } return false; } diff --git a/lib/Manager.php b/lib/Manager.php index 002afb624df..e28bd3a6f9e 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -23,7 +23,7 @@ namespace OCA\Talk; -use OCA\Talk\Chat\Changelog; +use OCA\Talk\Chat\SpecialRoom; use OCA\Talk\Chat\CommentsManager; use OCA\Talk\Events\CreateRoomTokenEvent; use OCA\Talk\Events\RoomEvent; @@ -555,16 +555,21 @@ public function createPublicRoom(string $name = '', string $objectType = '', str } /** - * Makes sure the user is part of a changelog room and returns it * * @param string $userId + * @param int type + * @param bool $createIfMissing * @return Room */ - public function getChangelogRoom(string $userId): Room { + public function getSpecialRoom(string $userId, int $type, bool $createIfMissing = true): Room { + if ($type !== Room::CHANGELOG_CONVERSATION && $type !== Room::NOTES_CONVERSATION) { + throw new \OutOfBoundsException('Unsupported type'); + } + $query = $this->db->getQueryBuilder(); $query->select('*') ->from('talk_rooms') - ->where($query->expr()->eq('type', $query->createNamedParameter(Room::CHANGELOG_CONVERSATION, IQueryBuilder::PARAM_INT))) + ->where($query->expr()->eq('type', $query->createNamedParameter($type, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('name', $query->createNamedParameter($userId))); $result = $query->execute(); @@ -572,9 +577,22 @@ public function getChangelogRoom(string $userId): Room { $result->closeCursor(); if ($row === false) { - $room = $this->createRoom(Room::CHANGELOG_CONVERSATION, $userId); - $room->addUsers(['userId' => $userId]); - $room->setReadOnly(Room::READ_ONLY); + if (!$createIfMissing) { + throw new RoomNotFoundException('Conversation does not exist'); + } + $room = $this->createRoom($type, $userId); + if ($type === ROOM::NOTES_CONVERSATION) { + $room->addUsers([ + 'userId' => $userId, + 'participantType' => Participant::OWNER, + ]); + $room->setReadOnly(Room::READ_ONLY); + } else { + $room->addUsers(['userId' => $userId]); + if ($type === ROOM::CHANGELOG_CONVERSATION) { + $room->setReadOnly(Room::READ_ONLY); + } + } return $room; } @@ -687,11 +705,15 @@ public function resolveRoomDisplayName(Room $room, string $userId): string { if ($room->getType() === Room::CHANGELOG_CONVERSATION) { return $this->l->t('Talk updates ✅'); } + + if ($room->getType() === Room::NOTES_CONVERSATION) { + return $this->l->t('My notes'); + } + if ($userId === '' && $room->getType() !== Room::PUBLIC_CALL) { return $this->l->t('Private conversation'); } - if ($room->getType() !== Room::ONE_TO_ONE_CALL && $room->getName() === '') { $room->setName($this->getRoomNameByParticipants($room)); } diff --git a/lib/Room.php b/lib/Room.php index 3e7d2fdd328..5a78af412b9 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -56,6 +56,7 @@ class Room { public const GROUP_CALL = 2; public const PUBLIC_CALL = 3; public const CHANGELOG_CONVERSATION = 4; + public const NOTES_CONVERSATION = 5; public const READ_WRITE = 0; public const READ_ONLY = 1; diff --git a/lib/TInitialState.php b/lib/TInitialState.php index 7452436de6d..1a43d233780 100644 --- a/lib/TInitialState.php +++ b/lib/TInitialState.php @@ -87,6 +87,11 @@ protected function publishInitialStateForUser(IUser $user, IRootFolder $rootFold } catch (NoUserException $e) { } } + + $this->initialStateService->provideInitialState( + 'talk', 'my_notes', + $this->serverConfig->getUserValue($user->getUID(), 'spreed', 'notes', '0') !== '0' + ); } protected function publishInitialStateForGuest(): void { @@ -107,5 +112,10 @@ protected function publishInitialStateForGuest(): void { 'talk', 'attachment_folder', '' ); + + $this->initialStateService->provideInitialState( + 'talk', 'my_notes', + false + ); } } diff --git a/src/components/ConversationIcon.vue b/src/components/ConversationIcon.vue index 0d551db6d62..4dfba5182bf 100644 --- a/src/components/ConversationIcon.vue +++ b/src/components/ConversationIcon.vue @@ -95,6 +95,8 @@ export default { return 'icon-contacts' } else if (this.item.type === CONVERSATION.TYPE.PUBLIC) { return 'icon-public' + } else if (this.item.type === CONVERSATION.TYPE.NOTES) { + return 'icon-notes' } return '' @@ -121,6 +123,10 @@ $icon-size: 44px; background-size: $icon-size; } + &.icon-notes { + background-size: $icon-size; + } + &.icon-public, &.icon-contacts, &.icon-password, diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.vue b/src/components/LeftSidebar/ConversationsList/Conversation.vue index 8557312f55f..bad90cf641d 100644 --- a/src/components/LeftSidebar/ConversationsList/Conversation.vue +++ b/src/components/LeftSidebar/ConversationsList/Conversation.vue @@ -56,28 +56,30 @@ {{ t('spreed', 'Copy link') }} - - - - - {{ t('spreed', 'All messages') }} - - - {{ t('spreed', '@-mentions only') }} - - - {{ t('spreed', 'Off') }} - + @@ -90,7 +92,7 @@ icon="icon-delete-critical" class="critical" @click.prevent.exact="deleteConversation"> - {{ t('spreed', 'Delete conversation') }} + {{ labelDeleteConversation }} @@ -145,6 +147,9 @@ export default { counterShouldBePrimary() { return this.item.unreadMention || (this.item.unreadMessages && this.item.type === CONVERSATION.TYPE.ONE_TO_ONE) }, + isMyNotesConversation() { + return this.item.type === CONVERSATION.TYPE.NOTES + }, linkToConversation() { return window.location.protocol + '//' + window.location.host + generateUrl('/call/' + this.item.token) }, @@ -166,6 +171,12 @@ export default { isNotifyNever() { return this.item.notificationLevel === PARTICIPANT.NOTIFY.NEVER }, + labelDeleteConversation() { + if (this.item.type === CONVERSATION.TYPE.NOTES) { + return t('spreed', 'Delete my notes') + } + return t('spreed', 'Delete conversation') + }, canDeleteConversation() { return this.item.canDeleteConversation }, @@ -184,7 +195,8 @@ export default { return '' } - if (this.shortLastChatMessageAuthor === '') { + if (this.shortLastChatMessageAuthor === '' + || this.item.type === CONVERSATION.TYPE.NOTES) { return this.simpleLastChatMessage } diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index 48e3050ee00..41fc5fe0f09 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -84,12 +84,20 @@ - - {{ t('spreed', 'Default location for attachments') }} + + + + @@ -108,7 +116,10 @@ import { createGroupConversation, createOneToOneConversation, searchPossibleConversations, } from '../../services/conversationsService' -import { setAttachmentFolder } from '../../services/settingsService' +import { + setAttachmentFolder, + toggleMyNotes, +} from '../../services/settingsService' import { CONVERSATION } from '../../constants' import { loadState } from '@nextcloud/initial-state' import NewGroupConversation from './NewGroupConversation/NewGroupConversation' @@ -140,6 +151,7 @@ export default { isCirclesEnabled: loadState('talk', 'circles_enabled'), canStartConversations: loadState('talk', 'start_conversations'), attachmentFolderLoading: true, + myNotesEnabled: loadState('talk', 'my_notes'), } }, @@ -285,6 +297,12 @@ export default { this.attachmentFolderLoading = false }) }, + + async toggleMyNotes() { + this.myNotesEnabled = !this.myNotesEnabled + await toggleMyNotes(this.myNotesEnabled) + EventBus.$emit('shouldRefreshConversations') + }, }, } diff --git a/src/components/MessagesList/MessagesGroup/AuthorAvatar.vue b/src/components/MessagesList/MessagesGroup/AuthorAvatar.vue index 27ee34e9865..9b14787e419 100644 --- a/src/components/MessagesList/MessagesGroup/AuthorAvatar.vue +++ b/src/components/MessagesList/MessagesGroup/AuthorAvatar.vue @@ -34,6 +34,8 @@
+
>_ @@ -67,6 +69,9 @@ export default { isChangelog() { return this.authorType === 'bots' && this.authorId === 'changelog' }, + isNotes() { + return this.authorType === 'bots' && this.authorId === 'notes' + }, isUser() { return this.authorType === 'users' }, diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue index 0dfb32cb49b..abfa3e44634 100644 --- a/src/components/MessagesList/MessagesList.vue +++ b/src/components/MessagesList/MessagesList.vue @@ -249,7 +249,7 @@ export default { return message2 // Is there a previous message && ( message1.actorType !== 'bots' // Don't group messages of commands and bots - || message1.actorId === 'changelog') // Apart from the changelog bot + || message1.actorId === 'changelog' || message1.actorId === 'notes') // Apart from the changelog and notes bot && (message1.systemMessage.length === 0) === (message2.systemMessage.length === 0) // Only group system messages with each others && message1.actorType === message2.actorType // To have the same author, the type && message1.actorId === message2.actorId // and the id of the author must be the same diff --git a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue index 05514dcf844..508d90d1438 100644 --- a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue +++ b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue @@ -203,7 +203,7 @@ export default { }, showModeratorLabel() { return this.isModerator - && [CONVERSATION.TYPE.ONE_TO_ONE, CONVERSATION.TYPE.CHANGELOG].indexOf(this.conversation.type) === -1 + && [CONVERSATION.TYPE.ONE_TO_ONE, CONVERSATION.TYPE.CHANGELOG, CONVERSATION.TYPE.NOTES].indexOf(this.conversation.type) === -1 }, canModerate() { return this.participantType !== PARTICIPANT.TYPE.OWNER && !this.isSelf && this.selfIsModerator diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue index 2edd3bd3c4a..c531ec908cd 100644 --- a/src/components/TopBar/CallButton.vue +++ b/src/components/TopBar/CallButton.vue @@ -81,6 +81,7 @@ export default { participantFlags: PARTICIPANT.CALL_FLAG.DISCONNECTED, participantType: PARTICIPANT.TYPE.USER, readOnly: CONVERSATION.STATE.READ_ONLY, + type: CONVERSATION.TYPE.GROUP, hasCall: false, canStartCall: false, lobbyState: WEBINAR.LOBBY.NONE, @@ -152,6 +153,7 @@ export default { showStartCallButton() { return this.conversation.readOnly === CONVERSATION.STATE.READ_WRITE + && this.conversation.type !== CONVERSATION.TYPE.NOTES && this.participant.inCall === PARTICIPANT.CALL_FLAG.DISCONNECTED }, diff --git a/src/constants.js b/src/constants.js index 6a56f20fd27..5bc87b40450 100644 --- a/src/constants.js +++ b/src/constants.js @@ -32,6 +32,7 @@ export const CONVERSATION = { GROUP: 2, PUBLIC: 3, CHANGELOG: 4, + NOTES: 5, }, } export const PARTICIPANT = { diff --git a/src/services/settingsService.js b/src/services/settingsService.js index 65e262658bf..72599627e6e 100644 --- a/src/services/settingsService.js +++ b/src/services/settingsService.js @@ -36,6 +36,20 @@ const setAttachmentFolder = async function(path) { }) } +/** + * Gets the conversation token for a given file id + * + * @param {bool} enabled The name of the folder + * @returns {Object} The axios response + */ +const toggleMyNotes = async function(enabled) { + return axios.post(generateOcsUrl('apps/spreed/api/v1/settings', 2) + 'user', { + key: 'notes', + value: enabled ? '1' : '0', + }) +} + export { setAttachmentFolder, + toggleMyNotes, } diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index e7fda0f1477..8abce88ec5f 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -36,9 +36,9 @@ */ class FeatureContext implements Context, SnippetAcceptingContext { - /** @var array[] */ + /** @var string[] */ protected static $identifierToToken; - /** @var array[] */ + /** @var string[] */ protected static $tokenToIdentifier; /** @var array[] */ protected static $sessionIdToUser; @@ -135,7 +135,7 @@ public function userIsParticipantOfRooms($user, TableNode $formData = null) { $rooms = $this->getDataFromResponse($this->response); $rooms = array_filter($rooms, function($room) { - return $room['type'] !== 4; + return $room['type'] !== 4 && $room['type'] !== 5; }); if ($formData === null) { @@ -178,6 +178,43 @@ public function userIsParticipantOfRooms($user, TableNode $formData = null) { }, $rooms, $formData->getHash())); } + /** + * @Then /^user "([^"]*)" has (the|no) notes conversation$/ + * + * @param string $user + * @param string $hasConversation + */ + public function userHasNotesConversation(string $user, string $hasConversation): void { + $this->setCurrentUser($user); + $this->sendRequest('GET', '/apps/spreed/api/v1/room'); + $this->assertStatusCode($this->response, 200); + + $rooms = $this->getDataFromResponse($this->response); + + $rooms = array_filter($rooms, function($room) { + return $room['type'] === 5; + }); + + $notesIndex = $user . '-notes'; + if ($hasConversation === 'the') { + Assert::assertCount(1, $rooms); + $room = array_pop($rooms); + + self::$identifierToToken[$notesIndex] = $room['token']; + self::$tokenToIdentifier[$room['token']] = $notesIndex; + } else { + Assert::assertEmpty($rooms); + + if (isset(self::$identifierToToken[$notesIndex])) { + $notesToken = self::$identifierToToken[$notesIndex]; + unset( + self::$tokenToIdentifier[$notesToken], + self::$identifierToToken[$notesIndex] + ); + } + } + } + /** * @Then /^user "([^"]*)" (is|is not) participant of room "([^"]*)"$/ * @@ -201,7 +238,7 @@ public function userIsParticipantOfRoom($user, $isOrNotParticipant, $identifier) $rooms = $this->getDataFromResponse($this->response); $rooms = array_filter($rooms, function($room) { - return $room['type'] !== 4; + return $room['type'] !== 4 && $room['type'] !== 5; }); if ($isParticipant) { @@ -249,6 +286,23 @@ private function guestIsParticipantOfRoom($guest, $isOrNotParticipant, $identifi $this->assertStatusCode($this->response, 404); } + /** + * @Then /^user "([^"]*)" sets setting "([^"]*)" to "([^"]*)" with (\d+)$/ + * + * @param string $user + * @param string $setting + * @param string $value + * @param int $statusCode + */ + public function userSetsSetting(string $user, string $setting, string $value, int $statusCode): void { + $this->setCurrentUser($user); + $this->sendRequest('POST', '/apps/spreed/api/v1/settings/user', [ + 'key' => $setting, + 'value' => $value, + ]); + $this->assertStatusCode($this->response, $statusCode); + } + /** * @Then /^user "([^"]*)" creates room "([^"]*)"$/ * @@ -812,7 +866,7 @@ public function userSeesTheFollowingMessagesInRoom($user, $identifier, $statusCo 'actorDisplayName' => $message['actorDisplayName'], // TODO test timestamp; it may require using Runkit, php-timecop // or something like that. - 'message' => $message['message'], + 'message' => str_replace("\n", '\n', $message['message']), 'messageParameters' => json_encode($message['messageParameters']), ]; if ($includeParents) { @@ -985,12 +1039,13 @@ public function assureUserExists($user) { $response = $this->userExists($user); if ($response->getStatusCode() !== 200) { $this->createUser($user); - // Set a display name different than the user ID to be able to - // ensure in the tests that the right value was returned. - $this->setUserDisplayName($user); - $response = $this->userExists($user); - $this->assertStatusCode($response, 200); } + + // Set a display name different than the user ID to be able to + // ensure in the tests that the right value was returned. + $this->setUserDisplayName($user); + $response = $this->userExists($user); + $this->assertStatusCode($response, 200); } private function userExists($user) { diff --git a/tests/integration/features/conversation/notes.feature b/tests/integration/features/conversation/notes.feature new file mode 100644 index 00000000000..d65b910b8d7 --- /dev/null +++ b/tests/integration/features/conversation/notes.feature @@ -0,0 +1,28 @@ +Feature: notes + Background: + Given user "participant1" exists + + Scenario: Notes is enabled by default and can be toggled + Given user "participant1" has the notes conversation + When user "participant1" sets setting "notes" to "0" with 200 + Then user "participant1" has no notes conversation + When user "participant1" sets setting "notes" to "1" with 200 + Then user "participant1" has the notes conversation + + Scenario: Notes is always new + Given user "participant1" has the notes conversation + Then user "participant1" sees the following messages in room "participant1-notes" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | + | participant1-notes | bots | notes | My notes | Welcome to your notes!\nYou can use this conversation to share notes between your different devices. When you deleted it, you can recreate it via the settings. | [] | + When user "participant1" sends message "Another note" to room "participant1-notes" with 201 + Then user "participant1" sees the following messages in room "participant1-notes" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | + | participant1-notes | users | participant1 | participant1-displayname | Another note | [] | + | participant1-notes | bots | notes | My notes | Welcome to your notes!\nYou can use this conversation to share notes between your different devices. When you deleted it, you can recreate it via the settings. | [] | + When user "participant1" sets setting "notes" to "0" with 200 + Then user "participant1" has no notes conversation + When user "participant1" sets setting "notes" to "1" with 200 + Then user "participant1" has the notes conversation + And user "participant1" sees the following messages in room "participant1-notes" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | + | participant1-notes | bots | notes | My notes | Welcome to your notes!\nYou can use this conversation to share notes between your different devices. When you deleted it, you can recreate it via the settings. | [] | diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index eea12ab44b3..bf1d3925d7c 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -27,7 +27,6 @@ use OCA\Talk\Capabilities; use OCA\Talk\Config; use OCP\Capabilities\IPublicCapability; -use OCP\IConfig; use OCP\IUser; use OCP\IUserSession; use PHPUnit\Framework\MockObject\MockObject; @@ -35,8 +34,6 @@ class CapabilitiesTest extends TestCase { - /** @var IConfig|MockObject */ - protected $serverConfig; /** @var Config|MockObject */ protected $talkConfig; /** @var IUserSession|MockObject */ @@ -44,14 +41,12 @@ class CapabilitiesTest extends TestCase { public function setUp(): void { parent::setUp(); - $this->serverConfig = $this->createMock(IConfig::class); $this->talkConfig = $this->createMock(Config::class); $this->userSession = $this->createMock(IUserSession::class); } public function testGetCapabilitiesGuest(): void { $capabilities = new Capabilities( - $this->serverConfig, $this->talkConfig, $this->userSession ); @@ -63,11 +58,6 @@ public function testGetCapabilitiesGuest(): void { $this->talkConfig->expects($this->never()) ->method('isDisabledForUser'); - $this->serverConfig->expects($this->once()) - ->method('getSystemValueString') - ->with('version', '0.0.0') - ->willReturn('16.0.1'); - $this->assertInstanceOf(IPublicCapability::class, $capabilities); $this->assertSame([ 'spreed' => [ @@ -103,7 +93,7 @@ public function testGetCapabilitiesGuest(): void { 'allowed' => false, ], 'chat' => [ - 'max-length' => 1000, + 'max-length' => 32000, ], 'conversations' => [ 'can-create' => false, @@ -127,7 +117,6 @@ public function dataGetCapabilitiesUserAllowed(): array { */ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCreate): void { $capabilities = new Capabilities( - $this->serverConfig, $this->talkConfig, $this->userSession ); @@ -155,11 +144,6 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea ->with($user) ->willReturn($isNotAllowed); - $this->serverConfig->expects($this->once()) - ->method('getSystemValueString') - ->with('version', '0.0.0') - ->willReturn('16.0.2'); - $this->assertInstanceOf(IPublicCapability::class, $capabilities); $this->assertSame([ 'spreed' => [ @@ -189,6 +173,7 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea 'circles-support', 'force-mute', 'chat-reference-id', + 'notes', ], 'config' => [ 'attachments' => [ @@ -208,7 +193,6 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea public function testGetCapabilitiesUserDisallowed(): void { $capabilities = new Capabilities( - $this->serverConfig, $this->talkConfig, $this->userSession ); @@ -223,9 +207,6 @@ public function testGetCapabilitiesUserDisallowed(): void { ->with($user) ->willReturn(true); - $this->serverConfig->expects($this->never()) - ->method('getSystemValueString'); - $this->assertInstanceOf(IPublicCapability::class, $capabilities); $this->assertSame([], $capabilities->getCapabilities()); }