Add keyboard support for inbox and simplify code (#4380)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-01-18 19:45:14 +04:00 committed by GitHub
parent cf8ff1be31
commit ed634ccabf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 283 additions and 127 deletions

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { ActionIcon, CheckBox, Component, IconMoreH, Label, showPopup } from '@hcengineering/ui' import { ActionIcon, Component, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import notification, { import notification, {
ActivityNotificationViewlet, ActivityNotificationViewlet,
DisplayInboxNotification, DisplayInboxNotification,
@ -71,13 +71,13 @@
}} }}
> >
<div class="header"> <div class="header">
<CheckBox <!-- <CheckBox-->
circle <!-- circle-->
kind="primary" <!-- kind="primary"-->
on:value={(event) => { <!-- on:value={(event) => {-->
dispatch('check', event.detail) <!-- dispatch('check', event.detail)-->
}} <!-- }}-->
/> <!-- />-->
<NotifyContextIcon {value} /> <NotifyContextIcon {value} />
{#if presenterMixin?.labelPresenter} {#if presenterMixin?.labelPresenter}
@ -135,10 +135,6 @@
font-weight: 500; font-weight: 500;
max-width: 20.5rem; max-width: 20.5rem;
} }
&:hover {
background-color: var(--highlight-hover);
}
} }
.labels { .labels {

View File

@ -32,7 +32,8 @@
Scroller, Scroller,
Separator, Separator,
TabItem, TabItem,
TabList TabList,
Location
} from '@hcengineering/ui' } from '@hcengineering/ui'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import { Ref, WithLookup } from '@hcengineering/core' import { Ref, WithLookup } from '@hcengineering/core'
@ -41,7 +42,7 @@
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient' import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import Filter from '../Filter.svelte' import Filter from '../Filter.svelte'
import { getDisplayInboxNotifications } from '../../utils' import { getDisplayInboxNotifications, resolveLocation } from '../../utils'
import { InboxNotificationsFilter } from '../../types' import { InboxNotificationsFilter } from '../../types'
export let visibleNav: boolean = true export let visibleNav: boolean = true
@ -89,17 +90,21 @@
viewlets = res viewlets = res
}) })
$: getDisplayInboxNotifications($notificationsByContextStore, filter).then((res) => { $: displayNotifications = getDisplayInboxNotifications($notificationsByContextStore, filter)
displayNotifications = res
})
locationStore.subscribe((newLocation) => { locationStore.subscribe((newLocation) => {
selectedContextId = newLocation.fragment as Ref<DocNotifyContext> | undefined syncLocation(newLocation)
})
async function syncLocation (newLocation: Location) {
const loc = await resolveLocation(newLocation)
selectedContextId = loc?.loc.fragment as Ref<DocNotifyContext> | undefined
if (selectedContextId !== selectedContext?._id) { if (selectedContextId !== selectedContext?._id) {
selectedContext = undefined selectedContext = undefined
} }
}) }
$: selectedContext = selectedContextId $: selectedContext = selectedContextId
? selectedContext ?? $notifyContextsStore.find(({ _id }) => _id === selectedContextId) ? selectedContext ?? $notifyContextsStore.find(({ _id }) => _id === selectedContextId)
@ -242,7 +247,8 @@
props={{ props={{
notifications: filteredNotifications, notifications: filteredNotifications,
checkedContexts, checkedContexts,
viewlets viewlets,
selectedContext
}} }}
on:click={selectContext} on:click={selectContext}
/> />

View File

@ -13,10 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Scroller } from '@hcengineering/ui' import { ListView } from '@hcengineering/ui'
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification' import { ActivityNotificationViewlet, DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import InboxNotificationPresenter from './InboxNotificationPresenter.svelte' import InboxNotificationPresenter from './InboxNotificationPresenter.svelte'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient' import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
@ -29,44 +28,94 @@
const inboxClient = InboxNotificationsClientImpl.getClient() const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextsStore = inboxClient.docNotifyContexts const notifyContextsStore = inboxClient.docNotifyContexts
async function handleCheck (notification: DisplayInboxNotification, isChecked: boolean) { let list: ListView
if (!isChecked) { let listSelection = 0
return let element: HTMLDivElement | undefined
}
await deleteInboxNotification(notification) function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection + 1)
}
if (key.code === 'Backspace') {
key.preventDefault()
key.stopPropagation()
const notification = notifications[listSelection]
deleteInboxNotification(notification)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const notification = notifications[listSelection]
const context = $notifyContextsStore.find(({ _id }) => _id === notification.docNotifyContext)
dispatch('click', {
context,
notification
})
}
} }
$: if (element) {
element.focus()
}
// async function handleCheck(notification: DisplayInboxNotification, isChecked: boolean) {
// if (!isChecked) {
// return
// }
//
// await deleteInboxNotification(notification)
// }
</script> </script>
<Scroller> <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
{#each notifications as notification (notification._id)} <!-- svelte-ignore a11y-no-static-element-interactions -->
<div animate:flip={{ duration: 500 }}> <div class="root" bind:this={element} tabindex="0" on:keydown={onKeydown}>
<div class="notification gap-2 ml-2"> <ListView bind:this={list} bind:selection={listSelection} count={notifications.length}>
<!-- <div class="mt-6">--> <svelte:fragment slot="item" let:item={itemIndex}>
<!-- <CheckBox--> {@const notification = notifications[itemIndex]}
<!-- circle--> {#key notification._id}
<!-- kind="primary"--> <div class="notification gap-2 ml-2">
<!-- on:value={(event) => {--> <!-- <div class="mt-6">-->
<!-- handleCheck(notification, event.detail)--> <!-- <CheckBox-->
<!-- }}--> <!-- circle-->
<!-- />--> <!-- kind="primary"-->
<!-- </div>--> <!-- on:value={(event) => {-->
<InboxNotificationPresenter <!-- handleCheck(notification, event.detail)-->
value={notification} <!-- }}-->
{viewlets} <!-- />-->
onClick={() => { <!-- </div>-->
dispatch('click', { <InboxNotificationPresenter
context: $notifyContextsStore.find(({ _id }) => _id === notification.docNotifyContext), value={notification}
notification {viewlets}
}) onClick={() => {
}} dispatch('click', {
/> context: $notifyContextsStore.find(({ _id }) => _id === notification.docNotifyContext),
</div> notification
</div> })
{/each} }}
</Scroller> />
</div>
{/key}
</svelte:fragment>
</ListView>
</div>
<style lang="scss"> <style lang="scss">
.root {
&:focus {
outline: 0;
}
}
.notification { .notification {
display: flex; display: flex;
} }

View File

@ -14,8 +14,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import { ActivityNotificationViewlet, DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification' import { ActivityNotificationViewlet, DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import { groupByArray, Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import { flip } from 'svelte/animate' import { createEventDispatcher } from 'svelte'
import { ListView } from '@hcengineering/ui'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient' import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import DocNotifyContextCard from '../DocNotifyContextCard.svelte' import DocNotifyContextCard from '../DocNotifyContextCard.svelte'
@ -24,12 +25,33 @@
export let notifications: DisplayInboxNotification[] = [] export let notifications: DisplayInboxNotification[] = []
export let viewlets: ActivityNotificationViewlet[] = [] export let viewlets: ActivityNotificationViewlet[] = []
const dispatch = createEventDispatcher()
const inboxClient = InboxNotificationsClientImpl.getClient() const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextsStore = inboxClient.docNotifyContexts const notifyContextsStore = inboxClient.docNotifyContexts
let displayNotificationsByContext = new Map<Ref<DocNotifyContext>, DisplayInboxNotification[]>() let list: ListView
let listSelection = 0
let element: HTMLDivElement | undefined
$: displayNotificationsByContext = groupByArray(notifications, ({ docNotifyContext }) => docNotifyContext) let displayData: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = []
$: updateDisplayData(notifications)
function updateDisplayData (notifications: DisplayInboxNotification[]) {
const result: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = []
notifications.forEach((item) => {
const data = result.find(([_id]) => _id === item.docNotifyContext)
if (!data) {
result.push([item.docNotifyContext, [item]])
} else {
data[1].push(item)
}
})
displayData = result
}
async function handleCheck (context: DocNotifyContext, isChecked: boolean) { async function handleCheck (context: DocNotifyContext, isChecked: boolean) {
if (!isChecked) { if (!isChecked) {
@ -38,28 +60,73 @@
await deleteContextNotifications(context) await deleteContextNotifications(context)
} }
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection + 1)
}
if (key.code === 'Backspace') {
key.preventDefault()
key.stopPropagation()
const context = $notifyContextsStore.find(({ _id }) => _id === displayData[listSelection][0])
deleteContextNotifications(context)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const context = $notifyContextsStore.find(({ _id }) => _id === displayData[listSelection][0])
dispatch('click', { context })
}
}
$: if (element) {
element.focus()
}
</script> </script>
{#each displayNotificationsByContext as [contextId, contextNotifications] (contextId)} <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div animate:flip={{ duration: 500 }}> <!-- svelte-ignore a11y-no-static-element-interactions -->
{#if contextNotifications.length} <div class="root" bind:this={element} tabindex="0" on:keydown={onKeydown}>
<ListView bind:this={list} bind:selection={listSelection} count={displayData.length}>
<svelte:fragment slot="item" let:item={itemIndex}>
{@const contextId = displayData[itemIndex][0]}
{@const contextNotifications = displayData[itemIndex][1]}
{@const context = $notifyContextsStore.find(({ _id }) => _id === contextId)} {@const context = $notifyContextsStore.find(({ _id }) => _id === contextId)}
{#key contextId}
{#if context} {#if context}
<DocNotifyContextCard <DocNotifyContextCard
value={context} value={context}
notifications={contextNotifications} notifications={contextNotifications}
{viewlets} {viewlets}
on:click on:click={(event) => {
on:check={(event) => handleCheck(context, event.detail)} dispatch('click', event.detail)
/> listSelection = itemIndex
<div class="separator" /> }}
{/if} on:check={(event) => handleCheck(context, event.detail)}
{/if} />
</div> <div class="separator" />
{/each} {/if}
{/key}
</svelte:fragment>
</ListView>
</div>
<style lang="scss"> <style lang="scss">
.root {
&:focus {
outline: 0;
}
}
.separator { .separator {
width: 100%; width: 100%;
height: 1px; height: 1px;

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { type ActivityMessage } from '@hcengineering/activity' import activity, { type ActivityMessage } from '@hcengineering/activity'
import { SortingOrder, getCurrentAccount, type Class, type Doc, type Ref, type WithLookup } from '@hcengineering/core' import { SortingOrder, getCurrentAccount, type Class, type Doc, type Ref, type WithLookup } from '@hcengineering/core'
import notification, { import notification, {
type ActivityInboxNotification, type ActivityInboxNotification,
@ -22,7 +22,7 @@ import notification, {
type InboxNotificationsClient type InboxNotificationsClient
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { derived, writable } from 'svelte/store' import { derived, get, writable } from 'svelte/store'
/** /**
* @public * @public
@ -33,7 +33,19 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
readonly docNotifyContexts = writable<DocNotifyContext[]>([]) readonly docNotifyContexts = writable<DocNotifyContext[]>([])
readonly docNotifyContextByDoc = writable<Map<Ref<Doc>, DocNotifyContext>>(new Map()) readonly docNotifyContextByDoc = writable<Map<Ref<Doc>, DocNotifyContext>>(new Map())
readonly inboxNotifications = writable<InboxNotification[]>([]) readonly activityInboxNotifications = writable<Array<WithLookup<ActivityInboxNotification>>>([])
readonly otherInboxNotifications = writable<InboxNotification[]>([])
readonly inboxNotifications = derived(
[this.activityInboxNotifications, this.otherInboxNotifications],
([activityNotifications, otherNotifications]) => {
return otherNotifications
.concat(activityNotifications)
.sort((n1, n2) => (n2.createdOn ?? n2.modifiedOn) - (n1.createdOn ?? n1.modifiedOn))
},
[] as InboxNotification[]
)
readonly inboxNotificationsByContext = derived( readonly inboxNotificationsByContext = derived(
[this.docNotifyContexts, this.inboxNotifications], [this.docNotifyContexts, this.inboxNotifications],
([notifyContexts, inboxNotifications]) => { ([notifyContexts, inboxNotifications]) => {
@ -53,20 +65,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
} }
) )
readonly activityInboxNotifications = derived(
[this.inboxNotifications],
([notifications]) =>
notifications.filter(
(n): n is ActivityInboxNotification => n._class === notification.class.ActivityInboxNotification
),
[] as ActivityInboxNotification[]
)
private readonly docNotifyContextsQuery = createQuery(true) private readonly docNotifyContextsQuery = createQuery(true)
private readonly inboxNotificationsQuery = createQuery(true) private readonly otherInboxNotificationsQuery = createQuery(true)
private readonly activityInboxNotificationsQuery = createQuery(true)
private _docNotifyContextByDoc = new Map<Ref<Doc>, DocNotifyContext>() private _docNotifyContextByDoc = new Map<Ref<Doc>, DocNotifyContext>()
private _inboxNotifications: Array<WithLookup<InboxNotification>> = []
private constructor () { private constructor () {
this.docNotifyContextsQuery.query( this.docNotifyContextsQuery.query(
@ -80,14 +83,14 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
this.docNotifyContextByDoc.set(this._docNotifyContextByDoc) this.docNotifyContextByDoc.set(this._docNotifyContextByDoc)
} }
) )
this.inboxNotificationsQuery.query( this.otherInboxNotificationsQuery.query(
notification.class.InboxNotification, notification.class.InboxNotification,
{ {
_class: { $nin: [notification.class.ActivityInboxNotification] },
user: getCurrentAccount()._id user: getCurrentAccount()._id
}, },
(result: InboxNotification[]) => { (result: InboxNotification[]) => {
this.inboxNotifications.set(result) this.otherInboxNotifications.set(result)
this._inboxNotifications = result
}, },
{ {
sort: { sort: {
@ -95,6 +98,24 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
} }
} }
) )
this.activityInboxNotificationsQuery.query(
notification.class.ActivityInboxNotification,
{
user: getCurrentAccount()._id
},
(result: ActivityInboxNotification[]) => {
this.activityInboxNotifications.set(result)
},
{
sort: {
createdOn: SortingOrder.Descending
},
lookup: {
attachedTo: activity.class.ActivityMessage
}
}
)
} }
static createClient (): InboxNotificationsClientImpl { static createClient (): InboxNotificationsClientImpl {
@ -117,7 +138,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
return return
} }
const inboxNotifications = this._inboxNotifications.filter( const inboxNotifications = get(this.inboxNotifications).filter(
(notification) => notification.docNotifyContext === docNotifyContext._id && !notification.isViewed (notification) => notification.docNotifyContext === docNotifyContext._id && !notification.isViewed
) )
@ -180,7 +201,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
async readMessages (ids: Array<Ref<ActivityMessage>>): Promise<void> { async readMessages (ids: Array<Ref<ActivityMessage>>): Promise<void> {
const client = getClient() const client = getClient()
const notificationsToRead = this._inboxNotifications const notificationsToRead = get(this.inboxNotifications)
.filter((n): n is ActivityInboxNotification => n._class === notification.class.ActivityInboxNotification) .filter((n): n is ActivityInboxNotification => n._class === notification.class.ActivityInboxNotification)
.filter(({ attachedTo, isViewed }) => ids.includes(attachedTo) && !isViewed) .filter(({ attachedTo, isViewed }) => ids.includes(attachedTo) && !isViewed)
@ -191,7 +212,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
async readNotifications (ids: Array<Ref<InboxNotification>>): Promise<void> { async readNotifications (ids: Array<Ref<InboxNotification>>): Promise<void> {
const client = getClient() const client = getClient()
const notificationsToRead = this._inboxNotifications.filter(({ _id }) => ids.includes(_id)) const notificationsToRead = get(this.inboxNotifications).filter(({ _id }) => ids.includes(_id))
await Promise.all( await Promise.all(
notificationsToRead.map(async (notification) => await client.update(notification, { isViewed: true })) notificationsToRead.map(async (notification) => await client.update(notification, { isViewed: true }))
@ -200,7 +221,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
async unreadNotifications (ids: Array<Ref<InboxNotification>>): Promise<void> { async unreadNotifications (ids: Array<Ref<InboxNotification>>): Promise<void> {
const client = getClient() const client = getClient()
const notificationsToUnread = this._inboxNotifications.filter(({ _id }) => ids.includes(_id)) const notificationsToUnread = get(this.inboxNotifications).filter(({ _id }) => ids.includes(_id))
await Promise.all( await Promise.all(
notificationsToUnread.map(async (notification) => await client.update(notification, { isViewed: false })) notificationsToUnread.map(async (notification) => await client.update(notification, { isViewed: false }))
@ -209,7 +230,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
async deleteNotifications (ids: Array<Ref<InboxNotification>>): Promise<void> { async deleteNotifications (ids: Array<Ref<InboxNotification>>): Promise<void> {
const client = getClient() const client = getClient()
const inboxNotifications = this._inboxNotifications.filter(({ _id }) => ids.includes(_id)) const inboxNotifications = get(this.inboxNotifications).filter(({ _id }) => ids.includes(_id))
await Promise.all(inboxNotifications.map(async (notification) => await client.remove(notification))) await Promise.all(inboxNotifications.map(async (notification) => await client.remove(notification)))
} }
} }

View File

@ -48,7 +48,8 @@ import {
canUnReadNotifyContext, canUnReadNotifyContext,
readNotifyContext, readNotifyContext,
unReadNotifyContext, unReadNotifyContext,
deleteContextNotifications deleteContextNotifications,
hasInboxNotifications
} from './utils' } from './utils'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -85,7 +86,8 @@ export default async (): Promise<Resources> => ({
IsDocNotifyContextTracked: isDocNotifyContextVisible, IsDocNotifyContextTracked: isDocNotifyContextVisible,
HasHiddenDocNotifyContext: hasHiddenDocNotifyContext, HasHiddenDocNotifyContext: hasHiddenDocNotifyContext,
CanReadNotifyContext: canReadNotifyContext, CanReadNotifyContext: canReadNotifyContext,
CanUnReadNotifyContext: canUnReadNotifyContext CanUnReadNotifyContext: canUnReadNotifyContext,
HasInboxNotifications: hasInboxNotifications
}, },
actionImpl: { actionImpl: {
Unsubscribe: unsubscribe, Unsubscribe: unsubscribe,

View File

@ -21,7 +21,8 @@ import {
getCurrentAccount, getCurrentAccount,
type Ref, type Ref,
SortingOrder, SortingOrder,
type TxOperations type TxOperations,
type WithLookup
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { import notification, {
type ActivityInboxNotification, type ActivityInboxNotification,
@ -203,7 +204,11 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
/** /**
* @public * @public
*/ */
export async function deleteContextNotifications (doc: DocNotifyContext): Promise<void> { export async function deleteContextNotifications (doc?: DocNotifyContext): Promise<void> {
if (doc === undefined) {
return
}
const client = getClient() const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient() const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? [] const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
@ -324,9 +329,11 @@ async function generateLocation (
const workspace = loc.path[1] ?? '' const workspace = loc.path[1] ?? ''
const messageId = loc.query?.message as Ref<ActivityMessage> | undefined const messageId = loc.query?.message as Ref<ActivityMessage> | undefined
const context = await client.findOne(notification.class.DocNotifyContext, { _id: contextId }) const contextNotification = await client.findOne(notification.class.InboxNotification, {
docNotifyContext: contextId
})
if (context === undefined) { if (contextNotification === undefined) {
return { return {
loc: { loc: {
path: [loc.path[0], loc.path[1], inboxId], path: [loc.path[0], loc.path[1], inboxId],
@ -355,12 +362,11 @@ async function generateLocation (
} }
} }
export async function getDisplayInboxNotifications ( export function getDisplayInboxNotifications (
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>, notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>,
filter: InboxNotificationsFilter, filter: InboxNotificationsFilter = 'all',
objectClass?: Ref<Class<Doc>> objectClass?: Ref<Class<Doc>>
): Promise<DisplayInboxNotification[]> { ): DisplayInboxNotification[] {
const client = getClient()
const filteredNotifications = Array.from(notificationsByContext.values()) const filteredNotifications = Array.from(notificationsByContext.values())
.flat() .flat()
.filter(({ isViewed }) => { .filter(({ isViewed }) => {
@ -377,20 +383,14 @@ export async function getDisplayInboxNotifications (
}) })
const activityNotifications = filteredNotifications.filter( const activityNotifications = filteredNotifications.filter(
(n): n is ActivityInboxNotification => n._class === notification.class.ActivityInboxNotification (n): n is WithLookup<ActivityInboxNotification> => n._class === notification.class.ActivityInboxNotification
) )
const displayNotifications: DisplayInboxNotification[] = filteredNotifications.filter( const displayNotifications: DisplayInboxNotification[] = filteredNotifications.filter(
({ _class }) => _class !== notification.class.ActivityInboxNotification ({ _class }) => _class !== notification.class.ActivityInboxNotification
) )
const messages: Array<ActivityMessage | undefined> = await Promise.all( const messages: ActivityMessage[] = activityNotifications
activityNotifications.map( .map((activityNotification) => activityNotification.$lookup?.attachedTo)
async (activityNotification) =>
await client.findOne(activity.class.ActivityMessage, { _id: activityNotification.attachedTo })
)
)
const filteredMessages = messages
.filter((message): message is ActivityMessage => { .filter((message): message is ActivityMessage => {
if (message === undefined) { if (message === undefined) {
return false return false
@ -406,7 +406,7 @@ export async function getDisplayInboxNotifications (
}) })
.sort(activityMessagesComparator) .sort(activityMessagesComparator)
const combinedMessages = combineActivityMessages(filteredMessages, SortingOrder.Descending) const combinedMessages = combineActivityMessages(messages, SortingOrder.Descending)
for (const message of combinedMessages) { for (const message of combinedMessages) {
if (message._class === activity.class.DocUpdateMessage) { if (message._class === activity.class.DocUpdateMessage) {
@ -442,3 +442,11 @@ export async function getDisplayInboxNotifications (
return displayNotifications return displayNotifications
} }
export async function hasInboxNotifications (
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>
): Promise<boolean> {
const displayNotifications = getDisplayInboxNotifications(notificationsByContext)
return displayNotifications.some(({ isViewed }) => !isViewed)
}

View File

@ -272,8 +272,8 @@ export interface DocNotifyContext extends Doc {
export interface InboxNotificationsClient { export interface InboxNotificationsClient {
docNotifyContextByDoc: Writable<Map<Ref<Doc>, DocNotifyContext>> docNotifyContextByDoc: Writable<Map<Ref<Doc>, DocNotifyContext>>
docNotifyContexts: Writable<DocNotifyContext[]> docNotifyContexts: Writable<DocNotifyContext[]>
inboxNotifications: Writable<InboxNotification[]> inboxNotifications: Readable<InboxNotification[]>
activityInboxNotifications: Readable<ActivityInboxNotification[]> activityInboxNotifications: Writable<ActivityInboxNotification[]>
inboxNotificationsByContext: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>> inboxNotificationsByContext: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>>
readDoc: (_id: Ref<Doc>) => Promise<void> readDoc: (_id: Ref<Doc>) => Promise<void>
forceReadDoc: (_id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void> forceReadDoc: (_id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void>
@ -393,7 +393,10 @@ const notification = plugin(notificationId, {
GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>, GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>,
HasHiddenDocNotifyContext: '' as Resource<(doc: Doc[]) => Promise<boolean>>, HasHiddenDocNotifyContext: '' as Resource<(doc: Doc[]) => Promise<boolean>>,
IsDocNotifyContextHidden: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>, IsDocNotifyContextHidden: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
IsDocNotifyContextTracked: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>> IsDocNotifyContextTracked: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasInboxNotifications: '' as Resource<
(notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>
>
}, },
resolver: { resolver: {
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>> Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>

View File

@ -16,7 +16,7 @@
import contact, { Employee, PersonAccount } from '@hcengineering/contact' import contact, { Employee, PersonAccount } from '@hcengineering/contact'
import core, { Class, Doc, getCurrentAccount, Ref, setCurrentAccount, Space } from '@hcengineering/core' import core, { Class, Doc, getCurrentAccount, Ref, setCurrentAccount, Space } from '@hcengineering/core'
import login from '@hcengineering/login' import login from '@hcengineering/login'
import notification, { inboxId } from '@hcengineering/notification' import notification, { DocNotifyContext, inboxId, InboxNotification } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { broadcastEvent, getMetadata, getResource, IntlString } from '@hcengineering/platform' import { broadcastEvent, getMetadata, getResource, IntlString } from '@hcengineering/platform'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation' import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
@ -162,17 +162,21 @@
) )
const workspaceId = $location.path[1] const workspaceId = $location.path[1]
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
let hasNotificationsFn: ((data: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>) | undefined =
undefined
let hasInboxNotifications = false let hasInboxNotifications = false
let syncPromise: Promise<void> | undefined = undefined let syncPromise: Promise<void> | undefined = undefined
let locUpdate = 0 let locUpdate = 0
const notificationClient = InboxNotificationsClientImpl.getClient() getResource(notification.function.HasInboxNotifications).then((f) => {
hasNotificationsFn = f
})
notificationClient.inboxNotificationsByContext.subscribe((inboxNotificationsByContext) => { $: hasNotificationsFn?.($inboxNotificationsByContextStore).then((res) => {
hasInboxNotifications = Array.from(inboxNotificationsByContext.entries()).some(([_, notifications]) => hasInboxNotifications = res
notifications.some(({ isViewed }) => !isViewed)
)
}) })
const doSyncLoc = async (loc: Location, iteration: number): Promise<void> => { const doSyncLoc = async (loc: Location, iteration: number): Promise<void> => {