From 249fd6b5967fd23a5f14a2067da0159a924e519f Mon Sep 17 00:00:00 2001 From: Kristina <kristin.fefelova@gmail.com> Date: Wed, 15 May 2024 19:38:11 +0400 Subject: [PATCH] Group inbox message notifications by author (#5599) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> --- .../components/DocNotifyContextCard.svelte | 100 ++++++++++++++-- .../src/components/MessagePopup.svelte | 110 ++++++++++++++++++ .../NotificationCollaboratorsChanged.svelte | 2 +- .../inbox/InboxGroupedListView.svelte | 18 +-- plugins/notification-resources/src/utils.ts | 14 +++ 5 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 plugins/notification-resources/src/components/MessagePopup.svelte diff --git a/plugins/notification-resources/src/components/DocNotifyContextCard.svelte b/plugins/notification-resources/src/components/DocNotifyContextCard.svelte index 80b6bdb9f9..66d2b3fe75 100644 --- a/plugins/notification-resources/src/components/DocNotifyContextCard.svelte +++ b/plugins/notification-resources/src/components/DocNotifyContextCard.svelte @@ -13,20 +13,30 @@ // limitations under the License. --> <script lang="ts"> - import { ButtonIcon, CheckBox, Component, IconMoreV, Label, showPopup, Spinner } from '@hcengineering/ui' + import { ButtonIcon, CheckBox, Component, IconMoreV, Label, showPopup, Spinner, tooltip } from '@hcengineering/ui' import notification, { ActivityNotificationViewlet, DisplayInboxNotification, - DocNotifyContext + DocNotifyContext, + InboxNotification } from '@hcengineering/notification' import { getClient } from '@hcengineering/presentation' import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources' import { createEventDispatcher } from 'svelte' - import { WithLookup } from '@hcengineering/core' + import { Class, Doc, IdMap, Ref, WithLookup } from '@hcengineering/core' + import chunter from '@hcengineering/chunter' + import { personAccountByIdStore } from '@hcengineering/contact-resources' + import { Person, PersonAccount } from '@hcengineering/contact' + import MessagesPopup from './MessagePopup.svelte' import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte' import NotifyContextIcon from './NotifyContextIcon.svelte' - import { archiveContextNotifications, unarchiveContextNotifications } from '../utils' + import { + archiveContextNotifications, + isActivityNotification, + isMentionNotification, + unarchiveContextNotifications + } from '../utils' export let value: DocNotifyContext export let notifications: WithLookup<DisplayInboxNotification>[] @@ -60,6 +70,62 @@ notification.mixin.NotificationContextPresenter ) + let groupedNotifications: Array<InboxNotification[]> = [] + + $: groupedNotifications = groupNotificationsByUser(notifications, $personAccountByIdStore) + + function isTextMessage (_class: Ref<Class<Doc>>): boolean { + return hierarchy.isDerived(_class, chunter.class.ChatMessage) + } + + const canGroup = (it: InboxNotification): boolean => { + if (isActivityNotification(it) && isTextMessage(it.attachedToClass)) { + return true + } + + return isMentionNotification(it) && isTextMessage(it.mentionedInClass) + } + + function groupNotificationsByUser ( + notifications: WithLookup<InboxNotification>[], + personAccountById: IdMap<PersonAccount> + ): Array<InboxNotification[]> { + const result: Array<InboxNotification[]> = [] + let group: InboxNotification[] = [] + let person: Ref<Person> | undefined = undefined + + for (const it of notifications) { + const account = it.createdBy ?? it.modifiedBy + const curPerson = personAccountById.get(account as Ref<PersonAccount>)?.person + const allowGroup = canGroup(it) + + if (!allowGroup || curPerson === undefined) { + if (group.length > 0) { + result.push(group) + group = [] + person = undefined + } + result.push([it]) + continue + } + + if (curPerson === person || person === undefined) { + group.push(it) + } else { + result.push(group) + group = [it] + } + + person = curPerson + } + + if (group.length > 0) { + result.push(group) + } + + return result + } + function showMenu (ev: MouseEvent): void { ev.stopPropagation() ev.preventDefault() @@ -99,6 +165,16 @@ await archivingPromise archivingPromise = undefined } + + function canShowTooltip (group: InboxNotification[]): boolean { + const first = group[0] + + return canGroup(first) + } + + function getKey (group: InboxNotification[]): string { + return group.map((it) => it._id).join('-') + } </script> <!-- svelte-ignore a11y-click-events-have-key-events --> @@ -152,16 +228,24 @@ <div class="content"> <div class="notifications"> - {#each notifications.slice(0, maxNotifications) as notification} - <div class="notification"> + {#each groupedNotifications.slice(0, maxNotifications) as group (getKey(group))} + <div + class="notification" + use:tooltip={canShowTooltip(group) + ? { + component: MessagesPopup, + props: { context: value, notifications: group } + } + : undefined} + > <div class="embeddedMarker" /> <InboxNotificationPresenter - value={notification} + value={group[0]} {viewlets} on:click={(e) => { e.preventDefault() e.stopPropagation() - dispatch('click', { context: value, notification }) + dispatch('click', { context: value, notification: group[0] }) }} /> </div> diff --git a/plugins/notification-resources/src/components/MessagePopup.svelte b/plugins/notification-resources/src/components/MessagePopup.svelte new file mode 100644 index 0000000000..fc9fc3a1d0 --- /dev/null +++ b/plugins/notification-resources/src/components/MessagePopup.svelte @@ -0,0 +1,110 @@ +<!-- +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +--> +<script lang="ts"> + import { Ref, WithLookup } from '@hcengineering/core' + import { getClient } from '@hcengineering/presentation' + import activity, { ActivityMessage } from '@hcengineering/activity' + import { Lazy, Spinner } from '@hcengineering/ui' + import { ActivityMessagePresenter, canGroupMessages } from '@hcengineering/activity-resources' + import { ActivityInboxNotification, InboxNotification } from '@hcengineering/notification' + + import { isActivityNotification, isMentionNotification } from '../utils' + + export let notifications: InboxNotification[] + + const client = getClient() + const hierarchy = client.getHierarchy() + + let loading = true + let messages: ActivityMessage[] = [] + + $: void updateMessages(notifications) + + async function updateMessages (notifications: InboxNotification[]): Promise<void> { + const result: ActivityMessage[] = [] + + for (const notification of notifications) { + if (isActivityNotification(notification)) { + const it = notification as WithLookup<ActivityInboxNotification> + if (it.$lookup?.attachedTo) { + result.push(it.$lookup?.attachedTo) + } + } + + if (isMentionNotification(notification)) { + const it = notification + if (hierarchy.isDerived(it.mentionedInClass, activity.class.ActivityMessage)) { + const message = await client.findOne<ActivityMessage>(it.mentionedInClass, { + _id: it.mentionedIn as Ref<ActivityMessage> + }) + if (message !== undefined) { + result.push(message) + } + } + } + } + + messages = result.reverse() + loading = false + } +</script> + +<div class="commentPopup-container"> + <div class="messages"> + {#if loading} + <div class="flex-center"> + <Spinner /> + </div> + {:else} + {#each messages as message, index} + {@const canGroup = canGroupMessages(message, messages[index - 1])} + <div class="item"> + <Lazy> + <ActivityMessagePresenter + value={message} + hideLink + skipLabel + type={canGroup ? 'short' : 'default'} + hoverStyles="filledHover" + /> + </Lazy> + </div> + {/each} + {/if} + </div> +</div> + +<style lang="scss"> + .commentPopup-container { + overflow: hidden; + display: flex; + flex-direction: column; + padding: 0; + min-width: 20rem; + min-height: 0; + max-height: 20rem; + + .messages { + overflow: auto; + flex: 1; + min-width: 0; + min-height: 0; + + .item { + max-width: 30rem; + } + } + } +</style> diff --git a/plugins/notification-resources/src/components/NotificationCollaboratorsChanged.svelte b/plugins/notification-resources/src/components/NotificationCollaboratorsChanged.svelte index 7895f45024..a532a9748e 100644 --- a/plugins/notification-resources/src/components/NotificationCollaboratorsChanged.svelte +++ b/plugins/notification-resources/src/components/NotificationCollaboratorsChanged.svelte @@ -39,7 +39,7 @@ } </script> -<BaseMessagePreview {actions} {message}> +<BaseMessagePreview {actions} {message} on:click> <span class="overflow-label flex-presenter flex-gap-1-5"> <Icon icon={contact.icon.Person} size="small" /> <Label diff --git a/plugins/notification-resources/src/components/inbox/InboxGroupedListView.svelte b/plugins/notification-resources/src/components/inbox/InboxGroupedListView.svelte index 6b3fe2e735..b3d8c1fe65 100644 --- a/plugins/notification-resources/src/components/inbox/InboxGroupedListView.svelte +++ b/plugins/notification-resources/src/components/inbox/InboxGroupedListView.svelte @@ -25,7 +25,7 @@ import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient' import DocNotifyContextCard from '../DocNotifyContextCard.svelte' - import { archiveContextNotifications, unarchiveContextNotifications } from '../../utils' + import { archiveContextNotifications, notificationsComparator, unarchiveContextNotifications } from '../../utils' import { InboxData } from '../../types' export let data: InboxData @@ -51,19 +51,9 @@ $: updateDisplayData(data) function updateDisplayData (data: InboxData): void { - displayData = Array.from(data.entries()).sort(([, notifications1], [, notifications2]) => { - const createdOn1 = notifications1[0].createdOn ?? 0 - const createdOn2 = notifications2[0].createdOn ?? 0 - - if (createdOn1 > createdOn2) { - return -1 - } - if (createdOn1 < createdOn2) { - return 1 - } - - return 0 - }) + displayData = Array.from(data.entries()).sort(([, notifications1], [, notifications2]) => + notificationsComparator(notifications1[0], notifications2[0]) + ) } function onKeydown (key: KeyboardEvent): void { diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index 361d789121..24aa063179 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -672,3 +672,17 @@ function arrayBufferToBase64 (buffer: ArrayBuffer | null): string { return '' } } + +export function notificationsComparator (notifications1: InboxNotification, notifications2: InboxNotification): number { + const createdOn1 = notifications1.createdOn ?? 0 + const createdOn2 = notifications2.createdOn ?? 0 + + if (createdOn1 > createdOn2) { + return -1 + } + if (createdOn1 < createdOn2) { + return 1 + } + + return 0 +}