diff options
author | Richard Steinmetz <richard@steinmetz.cloud> | 2023-08-17 15:31:27 +0200 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2024-04-09 11:18:21 +0200 |
commit | 910087c5a6976da425861a6dce6589a8017a70f7 (patch) | |
tree | 387b9616ddc6bcbdfaba4151f8b1e049c4245314 | |
parent | fd09f89535195b322cd9233fcd0e314a9ad0ffd3 (diff) |
perf(dashboard): Implement widget item api v2
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
-rw-r--r-- | lib/Dashboard/TalkWidget.php | 63 | ||||
-rw-r--r-- | src/dashboard.js | 56 | ||||
-rw-r--r-- | src/views/Dashboard.vue | 245 | ||||
-rw-r--r-- | tests/integration/features/integration/dashboard.feature | 2 | ||||
-rw-r--r-- | webpack.config.js | 1 |
5 files changed, 59 insertions, 308 deletions
diff --git a/lib/Dashboard/TalkWidget.php b/lib/Dashboard/TalkWidget.php index ccf366e1f..ca86e6703 100644 --- a/lib/Dashboard/TalkWidget.php +++ b/lib/Dashboard/TalkWidget.php @@ -5,6 +5,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> * * @author Julius Härtl <jus@bitgrid.net> + * @author Richard Steinmetz <richard@steinmetz.cloud> * * @license GNU AGPL version 3 or any later version * @@ -40,16 +41,17 @@ use OCP\Dashboard\IButtonWidget; use OCP\Dashboard\IConditionalWidget; use OCP\Dashboard\IIconWidget; use OCP\Dashboard\IOptionWidget; +use OCP\Dashboard\IReloadableWidget; use OCP\Dashboard\Model\WidgetButton; use OCP\Dashboard\Model\WidgetItem; +use OCP\Dashboard\Model\WidgetItems; use OCP\Dashboard\Model\WidgetOptions; use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; -use OCP\Util; -class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidget, IConditionalWidget { +class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidget, IConditionalWidget, IReloadableWidget { public function __construct( protected IUserSession $userSession, @@ -112,7 +114,7 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge $buttons[] = new WidgetButton( WidgetButton::TYPE_MORE, $this->url->linkToRouteAbsolute('spreed.Page.index'), - $this->l10n->t('More unread mentions') + $this->l10n->t('More conversations') ); return $buttons; } @@ -135,8 +137,6 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge * @inheritDoc */ 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 { @@ -170,6 +170,52 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge return $result; } + /** + * @inheritDoc + */ + public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems { + $allRooms = $this->manager->getRoomsForUser($userId, [], true); + + $rooms = []; + $mentions = []; + foreach ($allRooms as $room) { + if ($room->getObjectType() !== BreakoutRoom::PARENT_OBJECT_TYPE) { + $rooms[] = $room; + } + + $participant = $this->participantService->getParticipant($room, $userId); + $attendee = $participant->getAttendee(); + if ($room->getCallFlag() !== Participant::FLAG_DISCONNECTED + || $attendee->getLastMentionMessage() > $attendee->getLastReadMessage() + || ( + ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) + && $room->getLastMessage() + && $room->getLastMessage()->getId() > $attendee->getLastReadMessage() + )) { + $mentions[] = $room; + } + } + + $roomsToReturn = $rooms; + if (!empty($mentions)) { + $roomsToReturn = $mentions; + } + + uasort($roomsToReturn, [$this, 'sortRooms']); + $roomsToReturn = array_slice($roomsToReturn, 0, $limit); + + $result = []; + foreach ($roomsToReturn as $room) { + $result[] = $this->prepareRoom($room, $userId); + } + + return new WidgetItems( + $result, + empty($result) ? $this->l10n->t('Say hi to your friends and colleagues!') : '', + empty($mentions) ? $this->l10n->t('No unread mentions') : '', + ); + } + protected function prepareRoom(Room $room, string $userId): WidgetItem { $participant = $this->participantService->getParticipant($room, $userId); $subtitle = ''; @@ -219,4 +265,11 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge return $roomA->getLastActivity() >= $roomB->getLastActivity() ? -1 : 1; } + + /** + * @inheritDoc + */ + public function getReloadInterval(): int { + return 30; + } } diff --git a/src/dashboard.js b/src/dashboard.js deleted file mode 100644 index 4407997bf..000000000 --- a/src/dashboard.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license AGPL-3.0-or-later - * - * 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/>. - * - */ - -import Vue from 'vue' - -import { getRequestToken } from '@nextcloud/auth' -import { translate, translatePlural } from '@nextcloud/l10n' -import { generateFilePath } from '@nextcloud/router' - -import Dashboard from './views/Dashboard.vue' - -// CSP config for webpack dynamic chunk loading -// eslint-disable-next-line -__webpack_nonce__ = btoa(getRequestToken()) - -// Correct the root of the app for chunk loading -// OC.linkTo matches the apps folders -// OC.generateUrl ensure the index.php (or not) -// We do not want the index.php since we're loading files -// eslint-disable-next-line -__webpack_public_path__ = generateFilePath('spreed', '', 'js/') - -Vue.prototype.t = translate -Vue.prototype.n = translatePlural -Vue.prototype.OC = OC -Vue.prototype.OCA = OCA - -document.addEventListener('DOMContentLoaded', function() { - - OCA.Dashboard.register('spreed', (el) => { - const View = Vue.extend(Dashboard) - new View({ - propsData: {}, - }).$mount(el) - }) - -}) diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue deleted file mode 100644 index 05a7a8c26..000000000 --- a/src/views/Dashboard.vue +++ /dev/null @@ -1,245 +0,0 @@ -<!-- - - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - - - - @author Julius Härtl <jus@bitgrid.net> - - - - @license AGPL-3.0-or-later - - - - 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/>. - - - --> - -<template> - <NcDashboardWidget id="talk-panel" - :items="roomOptions" - :show-more-url="''" - :loading="loading" - :show-items-and-empty-content="!hasImportantConversations" - :half-empty-content-message="t('spreed', 'No unread mentions')"> - <template #default="{ item }"> - <NcDashboardWidgetItem :target-url="getItemTargetUrl(item)" - :main-text="getMainText(item)" - :sub-text="getSubText(item)" - :item="item"> - <template #avatar> - <ConversationIcon :item="item" :hide-call="false" /> - </template> - </NcDashboardWidgetItem> - </template> - <template #empty-content> - <NcEmptyContent :description="t('spreed', 'Say hi to your friends and colleagues!')"> - <template #icon> - <span class="icon icon-talk" /> - </template> - <template #action> - <NcButton class="button-start-conversation" - type="secondary" - @click="clickStartNew"> - {{ t('spreed', 'Start a conversation') }} - </NcButton> - </template> - </NcEmptyContent> - </template> - </NcDashboardWidget> -</template> - -<script> -import axios from '@nextcloud/axios' -import { generateOcsUrl, generateUrl } from '@nextcloud/router' - -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcDashboardWidget from '@nextcloud/vue/dist/Components/NcDashboardWidget.js' -import NcDashboardWidgetItem from '@nextcloud/vue/dist/Components/NcDashboardWidgetItem.js' -import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' - -import ConversationIcon from './../components/ConversationIcon.vue' - -import { CONVERSATION } from '../constants.js' - -const ROOM_POLLING_INTERVAL = 30 - -const propertySort = (properties) => (a, b) => properties.map(obj => { - let dir = 1 - if (obj[0] === '-') { - dir = -1 - obj = obj.substring(1) - } - return a[obj] > b[obj] ? dir : a[obj] < b[obj] ? -(dir) : 0 -}).reduce((p, n) => p || n, 0) - -export default { - name: 'Dashboard', - - components: { - NcDashboardWidget, - NcDashboardWidgetItem, - NcButton, - ConversationIcon, - NcEmptyContent, - }, - - data() { - return { - roomOptions: [], - hasImportantConversations: false, - loading: true, - windowVisibility: true, - } - }, - - computed: { - callLink() { - return (conversation) => { - return generateUrl('call/' + conversation.token) - } - }, - - /** - * This is a simplified version of the last chat message. - * Parameters are parsed without markup (just replaced with the name), - * e.g. no avatars on mentions. - * - * @return {string} A simple message to show below the conversation name - */ - simpleLastChatMessage() { - return (lastChatMessage) => { - if (!Object.keys(lastChatMessage).length) { - return '' - } - - const params = lastChatMessage.messageParameters - let subtitle = lastChatMessage.message.trim() - - // We don't really use rich objects in the subtitle, instead we fall back to the name of the item - Object.keys(params).forEach((parameterKey) => { - subtitle = subtitle.replace('{' + parameterKey + '}', params[parameterKey].name) - }) - - return subtitle - } - }, - - getItemTargetUrl() { - return (conversation) => { - return generateUrl(`call/${conversation.token}`) - } - }, - - getMainText() { - return (conversation) => { - return conversation.displayName - } - }, - - getSubText() { - return (conversation) => { - if (conversation.hasCall) { - return t('spreed', 'Call in progress') - } - - if (conversation.unreadMention) { - return t('spreed', 'You were mentioned') - } - - return this.simpleLastChatMessage(conversation.lastMessage) - } - }, - }, - - watch: { - windowVisibility(newValue) { - if (newValue) { - this.fetchRooms() - } - }, - }, - - beforeDestroy() { - document.removeEventListener('visibilitychange', this.changeWindowVisibility) - }, - - beforeMount() { - this.fetchRooms() - setInterval(this.fetchRooms, ROOM_POLLING_INTERVAL * 1000) - document.addEventListener('visibilitychange', this.changeWindowVisibility) - }, - - methods: { - fetchRooms() { - if (!this.windowVisibility) { - // Dashboard is not visible, so don't update the room list - return - } - - axios.get(generateOcsUrl('apps/spreed/api/v4/room')).then((response) => { - const allRooms = response.data.ocs.data - // filter out breakout rooms - const rooms = allRooms.filter((conversation) => conversation.objectType !== CONVERSATION.OBJECT_TYPE.BREAKOUT_ROOM) - const importantRooms = rooms.filter((conversation) => { - return conversation.hasCall - || conversation.unreadMention - || (conversation.unreadMessages > 0 && (conversation.type === CONVERSATION.TYPE.ONE_TO_ONE || conversation.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER)) - }) - - if (importantRooms.length) { - // FIXME unread 1-1 conversations are not sorted like unread mentions in group chats - importantRooms.sort(propertySort(['-hasCall', '-unreadMention', '-lastActivity'])) - this.roomOptions = importantRooms.slice(0, 7) - this.hasImportantConversations = true - } else { - this.roomOptions = rooms.sort(propertySort(['-isFavorite', '-lastActivity'])).slice(0, 5) - this.hasImportantConversations = false - } - - this.loading = false - }) - }, - - changeWindowVisibility() { - this.windowVisibility = !document.hidden - }, - - clickStartNew() { - window.location = generateUrl('/apps/spreed') - }, - }, -} -</script> - -<style lang="scss" scoped> - :deep(.item-list__entry) { - position: relative; - } - - .empty-content { - text-align: center; - margin-top: 5vh; - - .icon-talk { - width: 64px; - height: 64px; - background-size: 64px; - } - - &.half-screen { - margin-top: 0; - margin-bottom: 2vh; - } - } - - .button-start-conversation { - margin: 0 auto; - margin-top: 3px; - } -</style> diff --git a/tests/integration/features/integration/dashboard.feature b/tests/integration/features/integration/dashboard.feature index c5ebf83f3..c8f521d59 100644 --- a/tests/integration/features/integration/dashboard.feature +++ b/tests/integration/features/integration/dashboard.feature @@ -6,7 +6,7 @@ Feature: integration/dashboard 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 | item_api_versions | reload_interval | - | 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/"}] | [1] | 0 | + | spreed | Talk mentions | dashboard-talk-icon | img/app-dark.svg | {$BASE_URL}index.php/apps/spreed/ | true | 10 | [{"type":"more","text":"More conversations","link":"{$BASE_URL}index.php/apps/spreed/"}] | [1,2] | 30 | Scenario: User gets the dashboard widget content When user "participant1" sees the following entries for dashboard widgets "spreed" (v1) diff --git a/webpack.config.js b/webpack.config.js index 4f5bf9bdd..504d44672 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -29,7 +29,6 @@ module.exports = mergeWithRules({ 'public-share-auth-sidebar': path.join(__dirname, 'src', 'mainPublicShareAuthSidebar.js'), 'public-share-sidebar': path.join(__dirname, 'src', 'mainPublicShareSidebar.js'), flow: path.join(__dirname, 'src', 'flow.js'), - dashboard: path.join(__dirname, 'src', 'dashboard.js'), deck: path.join(__dirname, 'src', 'deck.js'), maps: path.join(__dirname, 'src', 'maps.js'), search: path.join(__dirname, 'src', 'search.js'), |