mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 07:10:02 +00:00
Group inbox message notifications by author (#5599)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
adb971e5ab
commit
249fd6b596
@ -13,20 +13,30 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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, {
|
import notification, {
|
||||||
ActivityNotificationViewlet,
|
ActivityNotificationViewlet,
|
||||||
DisplayInboxNotification,
|
DisplayInboxNotification,
|
||||||
DocNotifyContext
|
DocNotifyContext,
|
||||||
|
InboxNotification
|
||||||
} from '@hcengineering/notification'
|
} from '@hcengineering/notification'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources'
|
import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
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 InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
|
||||||
import NotifyContextIcon from './NotifyContextIcon.svelte'
|
import NotifyContextIcon from './NotifyContextIcon.svelte'
|
||||||
import { archiveContextNotifications, unarchiveContextNotifications } from '../utils'
|
import {
|
||||||
|
archiveContextNotifications,
|
||||||
|
isActivityNotification,
|
||||||
|
isMentionNotification,
|
||||||
|
unarchiveContextNotifications
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
export let value: DocNotifyContext
|
export let value: DocNotifyContext
|
||||||
export let notifications: WithLookup<DisplayInboxNotification>[]
|
export let notifications: WithLookup<DisplayInboxNotification>[]
|
||||||
@ -60,6 +70,62 @@
|
|||||||
notification.mixin.NotificationContextPresenter
|
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 {
|
function showMenu (ev: MouseEvent): void {
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
@ -99,6 +165,16 @@
|
|||||||
await archivingPromise
|
await archivingPromise
|
||||||
archivingPromise = undefined
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
@ -152,16 +228,24 @@
|
|||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="notifications">
|
<div class="notifications">
|
||||||
{#each notifications.slice(0, maxNotifications) as notification}
|
{#each groupedNotifications.slice(0, maxNotifications) as group (getKey(group))}
|
||||||
<div class="notification">
|
<div
|
||||||
|
class="notification"
|
||||||
|
use:tooltip={canShowTooltip(group)
|
||||||
|
? {
|
||||||
|
component: MessagesPopup,
|
||||||
|
props: { context: value, notifications: group }
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
>
|
||||||
<div class="embeddedMarker" />
|
<div class="embeddedMarker" />
|
||||||
<InboxNotificationPresenter
|
<InboxNotificationPresenter
|
||||||
value={notification}
|
value={group[0]}
|
||||||
{viewlets}
|
{viewlets}
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
dispatch('click', { context: value, notification })
|
dispatch('click', { context: value, notification: group[0] })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
@ -39,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseMessagePreview {actions} {message}>
|
<BaseMessagePreview {actions} {message} on:click>
|
||||||
<span class="overflow-label flex-presenter flex-gap-1-5">
|
<span class="overflow-label flex-presenter flex-gap-1-5">
|
||||||
<Icon icon={contact.icon.Person} size="small" />
|
<Icon icon={contact.icon.Person} size="small" />
|
||||||
<Label
|
<Label
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||||
import DocNotifyContextCard from '../DocNotifyContextCard.svelte'
|
import DocNotifyContextCard from '../DocNotifyContextCard.svelte'
|
||||||
import { archiveContextNotifications, unarchiveContextNotifications } from '../../utils'
|
import { archiveContextNotifications, notificationsComparator, unarchiveContextNotifications } from '../../utils'
|
||||||
import { InboxData } from '../../types'
|
import { InboxData } from '../../types'
|
||||||
|
|
||||||
export let data: InboxData
|
export let data: InboxData
|
||||||
@ -51,19 +51,9 @@
|
|||||||
$: updateDisplayData(data)
|
$: updateDisplayData(data)
|
||||||
|
|
||||||
function updateDisplayData (data: InboxData): void {
|
function updateDisplayData (data: InboxData): void {
|
||||||
displayData = Array.from(data.entries()).sort(([, notifications1], [, notifications2]) => {
|
displayData = Array.from(data.entries()).sort(([, notifications1], [, notifications2]) =>
|
||||||
const createdOn1 = notifications1[0].createdOn ?? 0
|
notificationsComparator(notifications1[0], notifications2[0])
|
||||||
const createdOn2 = notifications2[0].createdOn ?? 0
|
)
|
||||||
|
|
||||||
if (createdOn1 > createdOn2) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (createdOn1 < createdOn2) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown (key: KeyboardEvent): void {
|
function onKeydown (key: KeyboardEvent): void {
|
||||||
|
@ -672,3 +672,17 @@ function arrayBufferToBase64 (buffer: ArrayBuffer | null): string {
|
|||||||
return ''
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user