diff --git a/.drone.jsonnet b/.drone.jsonnet index 888c316e3ca..4e065349b04 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -125,6 +125,7 @@ local PipelinePostgreSQL(test_set) = Pipeline( PipelineSQLite("conversation"), PipelineSQLite("conversation-2"), PipelineSQLite("federation"), + PipelineSQLite("integration"), PipelineSQLite("reaction"), PipelineSQLite("sharing"), PipelineSQLite("sharing-2"), @@ -135,6 +136,7 @@ local PipelinePostgreSQL(test_set) = Pipeline( PipelineMySQL("conversation"), PipelineMySQL("conversation-2"), PipelineMySQL("federation"), + PipelineMySQL("integration"), PipelineMySQL("reaction"), PipelineMySQL("sharing"), PipelineMySQL("sharing-2"), @@ -145,6 +147,7 @@ local PipelinePostgreSQL(test_set) = Pipeline( PipelinePostgreSQL("conversation"), PipelinePostgreSQL("conversation-2"), PipelinePostgreSQL("federation"), + PipelinePostgreSQL("integration"), PipelinePostgreSQL("reaction"), PipelinePostgreSQL("sharing"), PipelinePostgreSQL("sharing-2"), diff --git a/.drone.yml b/.drone.yml index 94ff45fcb49..e02e4ec7947 100644 --- a/.drone.yml +++ b/.drone.yml @@ -218,6 +218,42 @@ trigger: - push --- kind: pipeline +name: int-sqlite-integration +services: +- image: ghcr.io/nextcloud/continuous-integration-redis:latest + name: cache +steps: +- 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 + - ./occ app:enable $APP_NAME + - git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications + apps/notifications + - ./occ app:enable notifications + - cd apps/$APP_NAME + - composer --version + - composer self-update --2 + - composer install + - cd tests/integration/ + - bash run.sh features/integration + environment: + APP_NAME: spreed + CORE_BRANCH: master + DATABASEHOST: sqlite + GUESTS_BRANCH: master + NOTIFICATIONS_BRANCH: master + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + name: integration-integration +trigger: + branch: + - master + - stable* + event: + - push +--- +kind: pipeline name: int-sqlite-reaction services: - image: ghcr.io/nextcloud/continuous-integration-redis:latest @@ -634,6 +670,57 @@ trigger: - push --- kind: pipeline +name: int-mysql-integration +services: +- image: ghcr.io/nextcloud/continuous-integration-redis:latest + name: cache +- command: + - --innodb_large_prefix=true + - --innodb_file_format=barracuda + - --innodb_file_per_table=true + - --sql-mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION + environment: + MYSQL_DATABASE: oc_autotest + MYSQL_PASSWORD: owncloud + MYSQL_ROOT_PASSWORD: owncloud + MYSQL_USER: oc_autotest + image: ghcr.io/nextcloud/continuous-integration-mariadb-10.4:10.4 + name: mysql + tmpfs: + - /var/lib/mysql +steps: +- 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 + - ./occ app:enable $APP_NAME + - git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications + apps/notifications + - ./occ app:enable notifications + - cd apps/$APP_NAME + - composer --version + - composer self-update --2 + - composer install + - cd tests/integration/ + - bash run.sh features/integration + environment: + APP_NAME: spreed + CORE_BRANCH: master + DATABASEHOST: mysql + GUESTS_BRANCH: master + NOTIFICATIONS_BRANCH: master + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + name: integration-integration +trigger: + branch: + - master + - stable* + event: + - pull_request + - push +--- +kind: pipeline name: int-mysql-reaction services: - image: ghcr.io/nextcloud/continuous-integration-redis:latest @@ -1059,6 +1146,51 @@ trigger: - push --- kind: pipeline +name: int-pgsql-integration +services: +- image: ghcr.io/nextcloud/continuous-integration-redis:latest + name: cache +- environment: + POSTGRES_DB: oc_autotest_dummy + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PASSWORD: "" + POSTGRES_USER: oc_autotest + image: ghcr.io/nextcloud/continuous-integration-postgres-13:postgres-13 + name: pgsql + tmpfs: + - /var/lib/postgresql/data +steps: +- 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 + - ./occ app:enable $APP_NAME + - git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications + apps/notifications + - ./occ app:enable notifications + - cd apps/$APP_NAME + - composer --version + - composer self-update --2 + - composer install + - cd tests/integration/ + - bash run.sh features/integration + environment: + APP_NAME: spreed + CORE_BRANCH: master + DATABASEHOST: pgsql + GUESTS_BRANCH: master + NOTIFICATIONS_BRANCH: master + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + name: integration-integration +trigger: + branch: + - master + - stable* + event: + - push +--- +kind: pipeline name: int-pgsql-reaction services: - image: ghcr.io/nextcloud/continuous-integration-redis:latest diff --git a/.gitignore b/.gitignore index 7d31bd4ae81..271d2cd02d5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ /tests/php/.phpunit.result.cache /tests/integration/vendor /tests/integration/output +/drone # Compiled javascript /js diff --git a/lib/Dashboard/TalkWidget.php b/lib/Dashboard/TalkWidget.php index 3afadc5bc1d..3516f7cc3d0 100644 --- a/lib/Dashboard/TalkWidget.php +++ b/lib/Dashboard/TalkWidget.php @@ -25,21 +25,37 @@ namespace OCA\Talk\Dashboard; -use OCP\Dashboard\IWidget; +use OCA\Talk\Chat\MessageParser; +use OCA\Talk\Manager; +use OCA\Talk\Room; +use OCP\Comments\IComment; +use OCP\Dashboard\IAPIWidget; +use OCP\Dashboard\IButtonWidget; +use OCP\Dashboard\IIconWidget; +use OCP\Dashboard\IOptionWidget; +use OCP\Dashboard\Model\WidgetButton; +use OCP\Dashboard\Model\WidgetItem; +use OCP\Dashboard\Model\WidgetOptions; use OCP\IL10N; use OCP\IURLGenerator; use OCP\Util; -class TalkWidget implements IWidget { +class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidget { private IURLGenerator $url; private IL10N $l10n; + private Manager $manager; + private MessageParser $messageParser; public function __construct( IURLGenerator $url, - IL10N $l10n + IL10N $l10n, + Manager $manager, + MessageParser $messageParser ) { $this->url = $url; $this->l10n = $l10n; + $this->manager = $manager; + $this->messageParser = $messageParser; } /** @@ -70,6 +86,30 @@ public function getIconClass(): string { return 'dashboard-talk-icon'; } + public function getWidgetOptions(): WidgetOptions { + return new WidgetOptions(true); + } + + /** + * @return \OCP\Dashboard\Model\WidgetButton[] + */ + public function getWidgetButtons(string $userId): array { + $buttons = []; + $buttons[] = new WidgetButton( + WidgetButton::TYPE_MORE, + $this->url->linkToRouteAbsolute('spreed.Page.index'), + $this->l10n->t('More unread mentions') + ); + return $buttons; + } + + /** + * @inheritDoc + */ + public function getIconUrl(): string { + return $this->url->getAbsoluteURL($this->url->imagePath('spreed', 'app-dark.svg')); + } + /** * @inheritDoc */ @@ -84,4 +124,89 @@ public function load(): void { Util::addStyle('spreed', 'icons'); Util::addScript('spreed', 'talk-dashboard'); } + + public function getItems(string $userId, ?string $since = null, int $limit = 7): array { + $rooms = $this->manager->getRoomsForUser($userId, [], true); + + $rooms = array_filter($rooms, static function (Room $room) use ($userId) { + $participant = $room->getParticipant($userId); + $attendee = $participant->getAttendee(); + return $room->getLastMessage() && $room->getLastMessage()->getId() > $attendee->getLastReadMessage(); + }); + + uasort($rooms, [$this, 'sortRooms']); + + $rooms = array_slice($rooms, 0, $limit); + + $result = []; + foreach ($rooms as $room) { + $result[] = $this->prepareRoom($room, $userId); + } + + return $result; + } + + protected function prepareRoom(Room $room, string $userId): WidgetItem { + $participant = $room->getParticipant($userId); + $subtitle = ''; + + $lastMessage = $room->getLastMessage(); + if ($lastMessage instanceof IComment) { + $message = $this->messageParser->createMessage($room, $participant, $room->getLastMessage(), $this->l10n); + $this->messageParser->parseMessage($message); + if ($message->getVisibility()) { + $placeholders = $replacements = []; + + foreach ($message->getMessageParameters() as $placeholder => $parameter) { + $placeholders[] = '{' . $placeholder . '}'; + if ($parameter['type'] === 'user' || $parameter['type'] === 'guest') { + $replacements[] = '@' . $parameter['name']; + } else { + $replacements[] = $parameter['name']; + } + } + + $subtitle = str_replace($placeholders, $replacements, $message->getMessage()); + } + } + + return new WidgetItem( + $room->getDisplayName($userId), + $subtitle, + $this->url->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken()]), + $this->getRoomIconUrl($room, $userId) + ); + } + + protected function getRoomIconUrl(Room $room, string $userId): string { + if ($room->getType() === Room::TYPE_ONE_TO_ONE) { + $participants = json_decode($room->getName(), true); + + foreach ($participants as $p) { + if ($p !== $userId) { + return $this->url->linkToRouteAbsolute( + 'core.avatar.getAvatar', + [ + 'userId' => $p, + 'size' => 64, + ] + ); + } + } + } elseif ($room->getObjectType() === 'file') { + return $this->url->getAbsoluteURL($this->url->imagePath('core', 'filetypes/file.svg')); + } elseif ($room->getObjectType() === 'share:password') { + return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/password.svg')); + } elseif ($room->getObjectType() === 'emails') { + return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/mail.svg')); + } elseif ($room->getType() === Room::TYPE_PUBLIC) { + return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/public.svg')); + } + + return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/group.svg')); + } + + protected function sortRooms(Room $roomA, Room $roomB): int { + return $roomA->getLastActivity() < $roomB->getLastActivity() ? -1 : 1; + } } diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index ba930a1956d..1ded3a5dc05 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1694,6 +1694,74 @@ protected function preparePollExpectedData(array $expected): array { return $expected; } + /** + * @Then /^user "([^"]*)" sees the following entry when loading the list of dashboard widgets(?: \((v1)\))$/ + * + * @param string $user + * @param string $apiVersion + * @param ?TableNode $formData + */ + public function userGetsDashboardWidgets($user, $apiVersion = 'v1', TableNode $formData = null): void { + $this->setCurrentUser($user); + $this->sendRequest('GET', '/apps/dashboard/api/' . $apiVersion . '/widgets'); + $this->assertStatusCode($this->response, 200); + + $data = $this->getDataFromResponse($this->response); + $expectedWidgets = $formData->getColumnsHash(); + + foreach ($expectedWidgets as $widget) { + $id = $widget['id']; + Assert::assertArrayHasKey($widget['id'], $data); + + $widgetIconUrl = $widget['icon_url']; + $dataIconUrl = $data[$id]['icon_url']; + + unset($widget['icon_url'], $data[$id]['icon_url']); + + $widget['item_icons_round'] = (bool) $widget['item_icons_round']; + $widget['order'] = (int) $widget['order']; + $widget['widget_url'] = str_replace('{$BASE_URL}', $this->baseUrl, $widget['widget_url']); + $widget['buttons'] = str_replace('{$BASE_URL}', $this->baseUrl, $widget['buttons']); + $widget['buttons'] = json_decode($widget['buttons'], true); + + Assert::assertEquals($widget, $data[$id], 'Mismatch of data for widget ' . $id); + Assert::assertStringEndsWith($widgetIconUrl, $dataIconUrl, 'Mismatch of icon URL for widget ' . $id); + } + } + + /** + * @Then /^user "([^"]*)" sees the following entries for dashboard widgets "([^"]*)"(?: \((v1)\))$/ + * + * @param string $user + * @param string $widgetId + * @param string $apiVersion + * @param ?TableNode $formData + */ + public function userGetsDashboardWidgetItems($user, $widgetId, $apiVersion = 'v1', TableNode $formData = null): void { + $this->setCurrentUser($user); + $this->sendRequest('GET', '/apps/dashboard/api/' . $apiVersion . '/widget-items?widgets[]=' . $widgetId); + $this->assertStatusCode($this->response, 200); + + $data = $this->getDataFromResponse($this->response); + + Assert::assertArrayHasKey($widgetId, $data); + $expectedItems = $formData->getColumnsHash(); + + if (empty($expectedItems)) { + Assert::assertEmpty($data[$widgetId]); + return; + } + + Assert::assertCount(count($expectedItems), $data[$widgetId]); + + foreach ($expectedItems as $key => $item) { + $item['link'] = $this->baseUrl . 'index.php/call/' . self::$identifierToToken[$item['link']]; + $item['iconUrl'] = str_replace('{$BASE_URL}', $this->baseUrl, $item['iconUrl']); + + Assert::assertEquals($item, $data[$widgetId][$key], 'Wrong details for item #' . $key); + } + } + /** * @Then /^user "([^"]*)" deletes message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * @@ -2594,7 +2662,7 @@ public function userSetTheMessageExpirationToXWithStatusCode(string $user, int $ } /** - * @When wait for :seconds seconds + * @When wait for :seconds seconds? */ public function waitForXSeconds($seconds): void { sleep($seconds); diff --git a/tests/integration/features/integration/dashboard.feature b/tests/integration/features/integration/dashboard.feature new file mode 100644 index 00000000000..63b5759f19c --- /dev/null +++ b/tests/integration/features/integration/dashboard.feature @@ -0,0 +1,35 @@ +Feature: integration/dashboard + Background: + Given user "participant1" exists + Given user "participant2" exists + + Scenario: User gets the available dashboard widgets + When user "participant1" sees the following entry when loading the list of dashboard widgets (v1) + | id | title | icon_class | icon_url | widget_url | item_icons_round | order | buttons | + | spreed | Talk mentions | dashboard-talk-icon | img/app-dark.svg | {$BASE_URL}index.php/apps/spreed/ | true | 10 | [{"type":"more","text":"More unread mentions","link":"{$BASE_URL}index.php/apps/spreed/"}] | + + Scenario: User gets the dashboard widget content + When user "participant1" sees the following entries for dashboard widgets "spreed" (v1) + | title | subtitle | link | iconUrl | + Given user "participant2" creates room "one-to-one room" (v4) + | roomType | 1 | + | invite | participant1 | + And user "participant2" sends message "Hello" to room "one-to-one room" with 201 + And wait for 1 second + Given user "participant2" creates room "group room" (v4) + | roomType | 2 | + | roomName | group room | + And user "participant2" adds user "participant1" to room "group room" with 200 (v4) + And user "participant2" sends message "Hello @all" to room "group room" with 201 + And wait for 1 second + Given user "participant2" creates room "call room" (v4) + | roomType | 3 | + | roomName | call room | + And user "participant2" adds user "participant1" to room "call room" with 200 (v4) + And user "participant2" joins room "call room" with 200 (v4) + And user "participant2" joins call "call room" with 200 (v4) + Then user "participant1" sees the following entries for dashboard widgets "spreed" (v1) + | title | subtitle | link | iconUrl | sinceId | + | participant2-displayname | Hello | one-to-one room | {$BASE_URL}index.php/avatar/participant2/64 | | + | group room | Hello group room | group room | {$BASE_URL}core/img/actions/group.svg | | + | call room | @participant2-displayname started a call | call room | {$BASE_URL}core/img/actions/public.svg | |