From b2d6d64437eaff7dc392163632ec60f311db684b Mon Sep 17 00:00:00 2001 From: Kristina Fefelova Date: Mon, 29 Jul 2024 17:26:21 +0400 Subject: [PATCH 1/3] Remove unused prop Signed-off-by: Kristina Fefelova --- models/chunter/src/migration.ts | 3 +-- models/notification/src/index.ts | 4 ---- .../chat/create/CreateChannel.svelte | 3 +-- .../chat/create/CreateDirectChat.svelte | 4 +--- .../chat/navigator/ChatNavGroup.svelte | 1 - .../components/NotificationPresenter.svelte | 2 +- .../src/inboxNotificationsClient.ts | 9 +------ plugins/notification-resources/src/utils.ts | 6 ----- plugins/notification/src/index.ts | 1 - .../src/components/navigator/SpacesNav.svelte | 2 +- .../components/navigator/StarredNav.svelte | 2 +- server-plugins/chunter-resources/src/index.ts | 2 -- server-plugins/gmail-resources/src/index.ts | 4 +--- .../notification-resources/src/index.ts | 24 +++++++------------ .../telegram-resources/src/index.ts | 4 +--- .../github/pod-github/src/notifications.ts | 1 - 16 files changed, 18 insertions(+), 54 deletions(-) diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index c4bb67fca2..cc49f9c27b 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -63,8 +63,7 @@ export async function createDocNotifyContexts ( await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, { user: user._id, attachedTo, - attachedToClass, - hidden: false + attachedToClass }) } } diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 3a45de3b30..2f7431ac37 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -203,10 +203,6 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext { @Index(IndexKind.Indexed) attachedToClass!: Ref> - @Prop(TypeBoolean(), core.string.Archived) - @Index(IndexKind.Indexed) - hidden!: boolean - @Prop(TypeDate(), core.string.Date) lastViewedTimestamp?: Timestamp diff --git a/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte b/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte index 2c107044f0..67bea5688f 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte @@ -63,8 +63,7 @@ await client.createDoc(notification.class.DocNotifyContext, channelId, { user: accountId, attachedTo: channelId, - attachedToClass: chunter.class.Channel, - hidden: false + attachedToClass: chunter.class.Channel }) openChannel(channelId, chunter.class.Channel) diff --git a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte index 2660dc6325..4b64e245b0 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte @@ -81,7 +81,6 @@ }) if (context !== undefined) { - await client.diffUpdate(context, { hidden: false }) openChannel(dmId, chunter.class.DirectMessage) return @@ -90,8 +89,7 @@ await client.createDoc(notification.class.DocNotifyContext, dmId, { user: myAccId, attachedTo: dmId, - attachedToClass: chunter.class.DirectMessage, - hidden: false + attachedToClass: chunter.class.DirectMessage }) openChannel(dmId, chunter.class.DirectMessage) diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte index a9f942d9ae..4930d49b9c 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte @@ -55,7 +55,6 @@ notification.class.DocNotifyContext, { ...model.query, - hidden: false, user: getCurrentAccount()._id }, (res: DocNotifyContext[]) => { diff --git a/plugins/notification-resources/src/components/NotificationPresenter.svelte b/plugins/notification-resources/src/components/NotificationPresenter.svelte index 73b12d421b..ceced3f77c 100644 --- a/plugins/notification-resources/src/components/NotificationPresenter.svelte +++ b/plugins/notification-resources/src/components/NotificationPresenter.svelte @@ -27,7 +27,7 @@ $: notifyContext = $contextByDocStore.get(value._id) $: inboxNotifications = notifyContext ? $inboxNotificationsByContextStore.get(notifyContext._id) ?? [] : [] - $: hasNotification = !notifyContext?.hidden && inboxNotifications.some(({ isViewed }) => !isViewed) + $: hasNotification = inboxNotifications.some(({ isViewed }) => !isViewed) {#if hasNotification} diff --git a/plugins/notification-resources/src/inboxNotificationsClient.ts b/plugins/notification-resources/src/inboxNotificationsClient.ts index e8f01b238c..1e7ea66e24 100644 --- a/plugins/notification-resources/src/inboxNotificationsClient.ts +++ b/plugins/notification-resources/src/inboxNotificationsClient.ts @@ -73,7 +73,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { return inboxNotifications.reduce((result, notification) => { const notifyContext = notifyContexts.find(({ _id }) => _id === notification.docNotifyContext) - if (notifyContext === undefined || notifyContext.hidden) { + if (notifyContext === undefined) { return result } @@ -207,13 +207,6 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { } ) } - - await client.createDoc(notification.class.DocNotifyContext, doc.space, { - attachedTo: _id, - attachedToClass: _class, - user: getCurrentAccount()._id, - hidden: true - }) } async readNotifications (client: TxOperations, ids: Array>): Promise { diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index 45f5de4393..a988155575 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -87,16 +87,10 @@ export function loadNotificationSettings (): void { loadNotificationSettings() export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise { - if (docNotifyContext.hidden) { - return false - } return docNotifyContext.isPinned !== true } export async function hasDocNotifyContextUnpinAction (docNotifyContext: DocNotifyContext): Promise { - if (docNotifyContext.hidden) { - return false - } return docNotifyContext.isPinned === true } diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index 3385d2a7cc..fe282ea777 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -276,7 +276,6 @@ export interface DocNotifyContext extends Doc { attachedTo: Ref attachedToClass: Ref> - hidden: boolean isPinned?: boolean lastViewedTimestamp?: Timestamp lastUpdateTimestamp?: Timestamp diff --git a/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte b/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte index 34dfd31663..a42600bbaf 100644 --- a/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte +++ b/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte @@ -91,7 +91,7 @@ ): boolean { const context = notifyContextByDoc.get(space._id) - if (context === undefined || context.hidden) { + if (context === undefined) { return false } diff --git a/plugins/workbench-resources/src/components/navigator/StarredNav.svelte b/plugins/workbench-resources/src/components/navigator/StarredNav.svelte index 8d7f483343..d0e9162528 100644 --- a/plugins/workbench-resources/src/components/navigator/StarredNav.svelte +++ b/plugins/workbench-resources/src/components/navigator/StarredNav.svelte @@ -53,7 +53,7 @@ ): boolean { const notifyContext = docUpdates.get(space._id) if (notifyContext === undefined) return false - return !notifyContext.hidden && !!inboxNotificationsByContext.get(notifyContext._id)?.length + return !!inboxNotificationsByContext.get(notifyContext._id)?.length } $: visibleSpace = spaces.find((space) => currentSpace === space._id) diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 881d49a86d..bb17f11026 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -411,14 +411,12 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc, control: Trigg attachedTo: tx.objectId, attachedToClass: tx.objectClass, user: addedMember, - hidden: false, lastViewedTimestamp: tx.modifiedOn }) await control.apply([createTx]) } else { const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { - hidden: false, lastViewedTimestamp: tx.modifiedOn }) diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index 992b234190..bc7f5c03a9 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -95,8 +95,7 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise // ) res.push( control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - lastUpdateTimestamp: tx.modifiedOn, - hidden: false + lastUpdateTimestamp: tx.modifiedOn }) ) } @@ -106,7 +105,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise user: tx.modifiedBy, attachedTo: channel._id, attachedToClass: channel._class, - hidden: false, lastUpdateTimestamp: tx.modifiedOn // TODO: push inbox notification // txes: [ diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 5988b75642..0eba4c650e 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -349,9 +349,6 @@ export async function pushInboxNotifications ( } const context = getDocNotifyContext(contexts, account._id, attachedTo, res) - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - const isHidden = !!context?.hidden - let docNotifyContextId: Ref if (context === undefined) { @@ -359,7 +356,6 @@ export async function pushInboxNotifications ( user: account._id, attachedTo, attachedToClass, - hidden: false, lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined }) await control.apply([createContextTx]) @@ -389,18 +385,16 @@ export async function pushInboxNotifications ( docNotifyContextId = context._id } - if (!isHidden) { - const notificationData = { - user: account._id, - isViewed: false, - docNotifyContext: docNotifyContextId, - ...data - } - const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) - res.push(notificationTx) - - return notificationTx + const notificationData = { + user: account._id, + isViewed: false, + docNotifyContext: docNotifyContextId, + ...data } + const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) + res.push(notificationTx) + + return notificationTx } async function activityInboxNotificationToText ( diff --git a/server-plugins/telegram-resources/src/index.ts b/server-plugins/telegram-resources/src/index.ts index f54d808c71..a14965d2b2 100644 --- a/server-plugins/telegram-resources/src/index.ts +++ b/server-plugins/telegram-resources/src/index.ts @@ -90,8 +90,7 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise // ) res.push( control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - lastUpdateTimestamp: tx.modifiedOn, - hidden: false + lastUpdateTimestamp: tx.modifiedOn }) ) } @@ -101,7 +100,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise user: tx.modifiedBy, attachedTo: channel._id, attachedToClass: channel._class, - hidden: false, lastUpdateTimestamp: tx.modifiedOn // TODO: push inbox notifications // txes: [ diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts index 63b924fe00..4da1f6c3ae 100644 --- a/services/github/pod-github/src/notifications.ts +++ b/services/github/pod-github/src/notifications.ts @@ -14,7 +14,6 @@ export async function createNotification ( const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, forDoc.space, { attachedTo: forDoc._id, attachedToClass: forDoc._class, - hidden: false, user: data.user, isPinned: false }) From 0ad0a3198ba882ac0422b4173f78b6ff8a6313d7 Mon Sep 17 00:00:00 2001 From: Kristina Fefelova Date: Mon, 29 Jul 2024 19:07:18 +0400 Subject: [PATCH 2/3] Add triggers to hide old activity channels/directs Signed-off-by: Kristina Fefelova --- models/chunter/src/index.ts | 26 ++- models/server-chunter/src/index.ts | 17 ++ .../chat/navigator/ChatNavGroup.svelte | 1 + plugins/chunter/src/index.ts | 19 +- server-plugins/chunter-resources/src/index.ts | 210 +++++++++++++++++- server-plugins/chunter/src/index.ts | 4 +- .../notification-resources/src/index.ts | 54 +++-- .../github/pod-github/src/notifications.ts | 3 - 8 files changed, 300 insertions(+), 34 deletions(-) diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 5a468e761d..702ea8d5db 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -22,10 +22,12 @@ import { type ChatMessageViewlet, type ChunterSpace, type ObjectChatPanel, - type ThreadMessage + type ThreadMessage, + type ChatInfo, + type ChannelInfo } from '@hcengineering/chunter' import presentation from '@hcengineering/model-presentation' -import contact from '@hcengineering/contact' +import contact, { type Person } from '@hcengineering/contact' import { type Class, type Doc, @@ -50,11 +52,12 @@ import { } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core' -import notification, { notificationActionTemplates } from '@hcengineering/model-notification' +import notification, { notificationActionTemplates, TDocNotifyContext } from '@hcengineering/model-notification' import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import type { IntlString } from '@hcengineering/platform' import { TActivityMessage } from '@hcengineering/model-activity' +import { type DocNotifyContext } from '@hcengineering/notification' import chunter from './plugin' @@ -133,6 +136,19 @@ export class TObjectChatPanel extends TClass implements ObjectChatPanel { ignoreKeys!: string[] } +@Mixin(chunter.mixin.ChannelInfo, notification.class.DocNotifyContext) +export class TChannelInfo extends TDocNotifyContext implements ChannelInfo { + @Hidden() + hidden!: boolean +} + +@Model(chunter.class.ChatInfo, core.class.Doc, DOMAIN_CHUNTER) +export class TChatInfo extends TDoc implements ChatInfo { + user!: Ref + hidden!: Ref[] + timestamp!: Timestamp +} + const actionTemplates = template({ removeChannel: { action: chunter.actionImpl.RemoveChannel, @@ -154,7 +170,9 @@ export function createModel (builder: Builder): void { TChatMessage, TThreadMessage, TChatMessageViewlet, - TObjectChatPanel + TObjectChatPanel, + TChatInfo, + TChannelInfo ) const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage] diff --git a/models/server-chunter/src/index.ts b/models/server-chunter/src/index.ts index 01b14c2aa9..cc93beffad 100644 --- a/models/server-chunter/src/index.ts +++ b/models/server-chunter/src/index.ts @@ -20,6 +20,7 @@ import chunter from '@hcengineering/chunter' import serverNotification from '@hcengineering/server-notification' import serverCore, { type ObjectDDParticipant } from '@hcengineering/server-core' import serverChunter from '@hcengineering/server-chunter' +import notification from '@hcengineering/notification' export { serverChunterId } from '@hcengineering/server-chunter' @@ -61,6 +62,22 @@ export function createModel (builder: Builder): void { } }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverChunter.trigger.OnUserStatus, + txMatch: { + objectClass: core.class.UserStatus + }, + isAsync: true + }) + + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverChunter.trigger.OnContextUpdate, + txMatch: { + _class: core.class.TxUpdateDoc, + objectClass: notification.class.DocNotifyContext + } + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverChunter.trigger.OnChatMessageRemoved, txMatch: { diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte index 4930d49b9c..9d2a945af4 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte @@ -55,6 +55,7 @@ notification.class.DocNotifyContext, { ...model.query, + [`${chunter.mixin.ChannelInfo}.hidden`]: { $ne: true }, user: getCurrentAccount()._id }, (res: DocNotifyContext[]) => { diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 5f2d0ea03c..f33f08c6cf 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -15,11 +15,12 @@ import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity' import type { Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core' -import { NotificationType } from '@hcengineering/notification' +import { DocNotifyContext, NotificationType } from '@hcengineering/notification' import type { Asset, Plugin } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform' import { AnyComponent } from '@hcengineering/ui' import { Action } from '@hcengineering/view' +import { Person } from '@hcengineering/contact' /** * @public @@ -72,6 +73,16 @@ export interface ChatMessageViewlet extends ActivityMessageViewlet { label?: IntlString } +export interface ChatInfo extends Doc { + user: Ref + hidden: Ref[] + timestamp: Timestamp +} + +export interface ChannelInfo extends DocNotifyContext { + hidden: boolean +} + /** * @public */ @@ -110,10 +121,12 @@ export default plugin(chunterId, { Channel: '' as Ref>, DirectMessage: '' as Ref>, ChatMessage: '' as Ref>, - ChatMessageViewlet: '' as Ref> + ChatMessageViewlet: '' as Ref>, + ChatInfo: '' as Ref> }, mixin: { - ObjectChatPanel: '' as Ref> + ObjectChatPanel: '' as Ref>, + ChannelInfo: '' as Ref> }, string: { Reactions: '' as IntlString, diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index bb17f11026..35827b8c58 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -14,8 +14,15 @@ // import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity' -import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter' -import { Person, PersonAccount } from '@hcengineering/contact' +import chunter, { + Channel, + ChannelInfo, + ChatMessage, + chunterId, + ChunterSpace, + ThreadMessage +} from '@hcengineering/chunter' +import contact, { Person, PersonAccount } from '@hcengineering/contact' import core, { Account, AttachedDoc, @@ -27,6 +34,7 @@ import core, { FindResult, Hierarchy, Ref, + Timestamp, Tx, TxCollectionCUD, TxCreateDoc, @@ -34,9 +42,10 @@ import core, { TxMixin, TxProcessor, TxRemoveDoc, - TxUpdateDoc + TxUpdateDoc, + UserStatus } from '@hcengineering/core' -import notification, { Collaborators, NotificationContent } from '@hcengineering/notification' +import notification, { Collaborators, DocNotifyContext, NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString, translate } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { @@ -50,6 +59,9 @@ import { workbenchId } from '@hcengineering/workbench' import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' import { encodeObjectURI } from '@hcengineering/view' +const updateChatInfoDelay = 12 * 60 * 60 * 1000 // 12 hours +const hideChannelDelay = 7 * 24 * 60 * 60 * 1000 // 7 days + /** * @public */ @@ -453,13 +465,201 @@ async function OnCollaboratorsChanged (tx: TxMixin, control: return res } +async function hideOldDirects ( + directs: DocNotifyContext[], + control: TriggerControl, + date: Timestamp +): Promise[]> { + const visibleDirects = directs.filter((context) => { + const hasMixin = control.hierarchy.hasMixin(context, chunter.mixin.ChannelInfo) + if (!hasMixin) return true + const info = control.hierarchy.as(context, chunter.mixin.ChannelInfo) + + return !info.hidden + }) + + const minVisibleDirects = 10 + + if (visibleDirects.length <= minVisibleDirects) return [] + const canHide = visibleDirects.length - minVisibleDirects + + let toHide: DocNotifyContext[] = [] + + for (const context of directs) { + const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context + + if (lastUpdateTimestamp > lastViewedTimestamp) continue + if (date - lastUpdateTimestamp < hideChannelDelay) continue + + toHide.push(context) + } + + if (toHide.length > canHide) { + toHide = toHide.splice(0, toHide.length - canHide) + } + + return await hideOldChannels(toHide, control) +} + +async function hideOldActivityChannels ( + contexts: DocNotifyContext[], + control: TriggerControl, + date: Timestamp +): Promise[]> { + if (contexts.length === 0) return [] + + const { hierarchy } = control + const toHide: DocNotifyContext[] = [] + + for (const context of contexts) { + const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context + + if (lastUpdateTimestamp > lastViewedTimestamp) continue + console.log({ diff: date - lastUpdateTimestamp, delay: hideChannelDelay }) + if (date - lastUpdateTimestamp < hideChannelDelay) continue + + const params = hierarchy.as(context, chunter.mixin.ChannelInfo) + if (params.hidden) continue + + toHide.push(context) + } + + return await hideOldChannels(toHide, control) +} + +async function hideOldChannels ( + contexts: DocNotifyContext[], + control: TriggerControl +): Promise[]> { + const res: TxMixin[] = [] + + for (const context of contexts) { + const tx = control.txFactory.createTxMixin(context._id, context._class, context.space, chunter.mixin.ChannelInfo, { + hidden: true + }) + res.push(tx) + } + + return res +} + +async function updateChatInfo (control: TriggerControl, status: UserStatus, date: Timestamp): Promise { + const account = await control.modelDb.findOne(contact.class.PersonAccount, { _id: status.user as Ref }) + if (account === undefined) return + + const chatUpdates = await control.queryFind(chunter.class.ChatInfo, {}) + const update = chatUpdates.find(({ user }) => user === account.person) + const shouldUpdate = update === undefined || date - update.timestamp > updateChatInfoDelay + + if (!shouldUpdate) return + + const contexts = await control.findAll(notification.class.DocNotifyContext, { + user: account._id, + isPinned: { $ne: true } + }) + + if (contexts.length === 0) return + + const { hierarchy } = control + const res: Tx[] = [] + + const directContexts = contexts.filter(({ attachedToClass }) => + hierarchy.isDerived(attachedToClass, chunter.class.DirectMessage) + ) + const activityContexts = contexts.filter( + ({ attachedToClass }) => + !hierarchy.isDerived(attachedToClass, chunter.class.DirectMessage) && + !hierarchy.isDerived(attachedToClass, chunter.class.Channel) && + !hierarchy.isDerived(attachedToClass, chunter.class.Channel) + ) + + const directTxes = await hideOldDirects(directContexts, control, date) + const activityTxes = await hideOldActivityChannels(activityContexts, control, date) + const mixinTxes = directTxes.concat(activityTxes) + const hidden: Ref[] = mixinTxes.map((tx) => tx.objectId) + + res.push(...mixinTxes) + + if (update === undefined) { + res.push( + control.txFactory.createTxCreateDoc(chunter.class.ChatInfo, core.space.Workspace, { + user: account.person, + hidden, + timestamp: date + }) + ) + } else { + res.push( + control.txFactory.createTxUpdateDoc(update._class, update.space, update._id, { + hidden: Array.from(new Set(update.hidden.concat(hidden))), + timestamp: date + }) + ) + } + + const txIds = res.map((tx) => tx._id) + + await control.apply(res) + + control.operationContext.derived.targets.docNotifyContext = (it) => { + if (txIds.includes(it._id)) { + return [account.email] + } + } +} + +async function OnUserStatus (originTx: TxCUD, control: TriggerControl): Promise { + const tx = TxProcessor.extractTx(originTx) as TxCUD + if (tx.objectClass !== core.class.UserStatus) return [] + if (tx._class === core.class.TxCreateDoc) { + const createTx = tx as TxCreateDoc + const { online } = createTx.attributes + if (online) { + const status = TxProcessor.createDoc2Doc(createTx) + await updateChatInfo(control, status, originTx.modifiedOn) + } + } else if (tx._class === core.class.TxUpdateDoc) { + const updateTx = tx as TxUpdateDoc + const { online } = updateTx.operations + if (online === true) { + const status = (await control.findAll(core.class.UserStatus, { _id: updateTx.objectId }))[0] + await updateChatInfo(control, status, originTx.modifiedOn) + } + } + + return [] +} + +async function OnContextUpdate (tx: TxUpdateDoc, control: TriggerControl): Promise { + const hasUpdate = 'lastUpdateTimestamp' in tx.operations && tx.operations.lastUpdateTimestamp !== undefined + if (!hasUpdate) return [] + + const chatUpdates = await control.queryFind(chunter.class.ChatInfo, {}) + for (const update of chatUpdates) { + if (update.hidden.includes(tx.objectId)) { + return [ + control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, chunter.mixin.ChannelInfo, { + hidden: false + }), + control.txFactory.createTxUpdateDoc(update._class, update.space, update._id, { + hidden: update.hidden.filter((id) => id !== tx.objectId) + }) + ] + } + } + + return [] +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { ChunterTrigger, OnChatMessageRemoved, OnChannelMembersChanged, - ChatNotificationsHandler + ChatNotificationsHandler, + OnUserStatus, + OnContextUpdate }, function: { CommentRemove, diff --git a/server-plugins/chunter/src/index.ts b/server-plugins/chunter/src/index.ts index 3f87afd060..4c800ff204 100644 --- a/server-plugins/chunter/src/index.ts +++ b/server-plugins/chunter/src/index.ts @@ -31,7 +31,9 @@ export default plugin(serverChunterId, { ChunterTrigger: '' as Resource, OnChatMessageRemoved: '' as Resource, OnChannelMembersChanged: '' as Resource, - ChatNotificationsHandler: '' as Resource + ChatNotificationsHandler: '' as Resource, + OnUserStatus: '' as Resource, + OnContextUpdate: '' as Resource }, function: { CommentRemove: '' as Resource, diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 0eba4c650e..6c29cb174b 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -368,20 +368,6 @@ export async function pushInboxNotifications ( } docNotifyContextId = createContextTx.objectId } else { - if (shouldUpdateTimestamp && context.lastUpdateTimestamp !== modifiedOn) { - const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { - lastUpdateTimestamp: modifiedOn - }) - await control.apply([updateTx]) - if (target.account?.email !== undefined) { - control.operationContext.derived.targets['docNotifyContext' + updateTx._id] = (it) => { - if (it._id === updateTx._id) { - return [target.account?.email as string] - } - } - } - } - docNotifyContextId = context._id } @@ -756,6 +742,38 @@ export async function getNotificationTxes ( return res } +async function updateContextsTimestamp ( + contexts: DocNotifyContext[], + timestamp: Timestamp, + control: TriggerControl +): Promise { + if (contexts.length === 0) return + + const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { + _id: { $in: contexts.map((it) => it.user as Ref) } + }) + const res: Tx[] = [] + + for (const context of contexts) { + const account = accounts.find(({ _id }) => _id === context.user) + const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { + lastUpdateTimestamp: timestamp + }) + + res.push(updateTx) + + if (account?.email !== undefined) { + control.operationContext.derived.targets['docNotifyContext' + updateTx._id] = (it) => { + if (it._id === updateTx._id) { + return [account.email] + } + } + } + } + + await control.apply(res) +} + export async function createCollabDocInfo ( collaborators: Ref[], control: TriggerControl, @@ -772,6 +790,10 @@ export async function createCollabDocInfo ( return res } + const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: object._id }) + + await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control) + const docMessages = activityMessages.filter((message) => message.attachedTo === object._id) if (docMessages.length === 0) { return res @@ -791,10 +813,6 @@ export async function createCollabDocInfo ( return res } - const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { - attachedTo: object._id - }) - const usersInfo = await getUsersInfo([...Array.from(targets), originTx.modifiedBy as Ref], control) const sender = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts index 4da1f6c3ae..310359a0b1 100644 --- a/services/github/pod-github/src/notifications.ts +++ b/services/github/pod-github/src/notifications.ts @@ -28,9 +28,6 @@ export async function createNotification ( props: data.props }) if (existing !== undefined) { - await client.update(docNotifyContext as DocNotifyContext, { - lastUpdateTimestamp: Date.now() - }) await client.update(existing, { isViewed: false }) From 152342000a4628c67521d96015ea88c0892a465e Mon Sep 17 00:00:00 2001 From: Kristina Fefelova Date: Mon, 29 Jul 2024 23:03:26 +0400 Subject: [PATCH 3/3] UI updates Signed-off-by: Kristina Fefelova --- models/chunter/src/actions.ts | 249 ++++++++++++++++++ models/chunter/src/index.ts | 233 +--------------- .../src/channelDataProvider.ts | 4 + .../src/components/ChannelScrollView.svelte | 42 ++- .../chat/navigator/ChatNavItem.svelte | 1 + .../components/chat/navigator/NavItem.svelte | 5 + .../src/components/chat/utils.ts | 22 +- plugins/chunter-resources/src/utils.ts | 36 ++- .../src/components/NotifyMarker.svelte | 25 +- .../notification-resources/src/index.ts | 76 +++++- 10 files changed, 430 insertions(+), 263 deletions(-) create mode 100644 models/chunter/src/actions.ts diff --git a/models/chunter/src/actions.ts b/models/chunter/src/actions.ts new file mode 100644 index 0000000000..57c590761a --- /dev/null +++ b/models/chunter/src/actions.ts @@ -0,0 +1,249 @@ +// +// 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. +// + +import { type Builder } from '@hcengineering/model' +import view, { actionTemplates as viewTemplates, createAction, template } from '@hcengineering/model-view' +import notification, { notificationActionTemplates } from '@hcengineering/model-notification' +import activity from '@hcengineering/activity' +import workbench from '@hcengineering/model-workbench' + +import chunter from './plugin' + +const actionTemplates = template({ + removeChannel: { + action: chunter.actionImpl.RemoveChannel, + label: view.string.Archive, + icon: view.icon.Delete, + input: 'focus', + keyBinding: ['Backspace'], + category: chunter.category.Chunter, + target: notification.class.DocNotifyContext, + context: { mode: ['context', 'browser'], group: 'remove' } + } +}) + +export function defineActions (builder: Builder): void { + createAction( + builder, + { + action: chunter.actionImpl.ReplyToThread, + label: chunter.string.ReplyToThread, + icon: chunter.icon.Thread, + input: 'focus', + category: chunter.category.Chunter, + target: activity.class.ActivityMessage, + visibilityTester: chunter.function.CanReplyToThread, + inline: true, + context: { + mode: 'context', + group: 'edit' + } + }, + chunter.action.ReplyToThreadAction + ) + + createAction( + builder, + { + action: view.actionImpl.CopyTextToClipboard, + actionProps: { + textProvider: chunter.function.GetLink + }, + label: chunter.string.CopyLink, + icon: chunter.icon.Copy, + input: 'none', + category: chunter.category.Chunter, + target: activity.class.ActivityMessage, + visibilityTester: chunter.function.CanCopyMessageLink, + context: { + mode: ['context', 'browser'], + application: chunter.app.Chunter, + group: 'copy' + } + }, + chunter.action.CopyChatMessageLink + ) + + createAction( + builder, + { + action: chunter.actionImpl.UnarchiveChannel, + label: chunter.string.UnarchiveChannel, + icon: view.icon.Archive, + input: 'focus', + category: chunter.category.Chunter, + target: chunter.class.Channel, + query: { + archived: true + }, + context: { + mode: 'context', + group: 'tools' + } + }, + chunter.action.UnarchiveChannel + ) + + createAction( + builder, + { + action: chunter.actionImpl.ConvertDmToPrivateChannel, + label: chunter.string.ConvertToPrivate, + icon: chunter.icon.Lock, + input: 'focus', + category: chunter.category.Chunter, + target: chunter.class.DirectMessage, + context: { + mode: 'context', + group: 'edit' + } + }, + chunter.action.ConvertToPrivate + ) + + createAction( + builder, + { + action: chunter.actionImpl.ArchiveChannel, + label: chunter.string.ArchiveChannel, + icon: view.icon.Archive, + input: 'focus', + category: chunter.category.Chunter, + target: chunter.class.Channel, + query: { + archived: false + }, + context: { + mode: 'context', + group: 'tools' + } + }, + chunter.action.ArchiveChannel + ) + + createAction(builder, { + ...viewTemplates.open, + target: chunter.class.Channel, + context: { + mode: ['browser', 'context'], + group: 'create' + }, + action: workbench.actionImpl.Navigate, + actionProps: { + mode: 'space' + } + }) + + createAction( + builder, + { + action: chunter.actionImpl.DeleteChatMessage, + label: view.string.Delete, + icon: view.icon.Delete, + input: 'focus', + keyBinding: ['Backspace'], + category: chunter.category.Chunter, + target: chunter.class.ChatMessage, + visibilityTester: chunter.function.CanDeleteMessage, + context: { mode: ['context', 'browser'], group: 'remove' } + }, + chunter.action.DeleteChatMessage + ) + + createAction( + builder, + { + ...actionTemplates.removeChannel, + icon: view.icon.EyeCrossed, + label: view.string.Hide, + query: { + attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } + } + }, + chunter.action.RemoveChannel + ) + + createAction( + builder, + { + ...actionTemplates.removeChannel, + label: chunter.string.CloseConversation, + query: { + attachedToClass: chunter.class.DirectMessage + } + }, + chunter.action.CloseConversation + ) + + createAction( + builder, + { + ...actionTemplates.removeChannel, + action: chunter.actionImpl.LeaveChannel, + label: chunter.string.LeaveChannel, + query: { + attachedToClass: chunter.class.Channel + } + }, + chunter.action.LeaveChannel + ) + + createAction(builder, { + ...notificationActionTemplates.pinContext, + label: chunter.string.StarChannel, + query: { + attachedToClass: chunter.class.Channel + }, + override: [notification.action.PinDocNotifyContext] + }) + + createAction(builder, { + ...notificationActionTemplates.unpinContext, + label: chunter.string.UnstarChannel, + query: { + attachedToClass: chunter.class.Channel + } + }) + + createAction(builder, { + ...notificationActionTemplates.pinContext, + label: chunter.string.StarConversation, + query: { + attachedToClass: chunter.class.DirectMessage + } + }) + + createAction(builder, { + ...notificationActionTemplates.unpinContext, + label: chunter.string.UnstarConversation, + query: { + attachedToClass: chunter.class.DirectMessage + } + }) + + createAction(builder, { + ...notificationActionTemplates.pinContext, + query: { + attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } + } + }) + + createAction(builder, { + ...notificationActionTemplates.unpinContext, + query: { + attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } + } + }) +} diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 702ea8d5db..4156981bf9 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -48,18 +48,20 @@ import { TypeRef, TypeString, TypeTimestamp, - UX + UX, + Hidden } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core' -import notification, { notificationActionTemplates, TDocNotifyContext } from '@hcengineering/model-notification' -import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view' +import notification, { TDocNotifyContext } from '@hcengineering/model-notification' +import view from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import type { IntlString } from '@hcengineering/platform' import { TActivityMessage } from '@hcengineering/model-activity' import { type DocNotifyContext } from '@hcengineering/notification' import chunter from './plugin' +import { defineActions } from './actions' export { chunterId } from '@hcengineering/chunter' export { chunterOperation } from './migration' @@ -149,19 +151,6 @@ export class TChatInfo extends TDoc implements ChatInfo { timestamp!: Timestamp } -const actionTemplates = template({ - removeChannel: { - action: chunter.actionImpl.RemoveChannel, - label: view.string.Archive, - icon: view.icon.Delete, - input: 'focus', - keyBinding: ['Backspace'], - category: chunter.category.Chunter, - target: notification.class.DocNotifyContext, - context: { mode: ['context', 'browser'], group: 'remove' } - } -}) - export function createModel (builder: Builder): void { builder.createModel( TChunterSpace, @@ -254,26 +243,6 @@ export function createModel (builder: Builder): void { chunter.category.Chunter ) - createAction( - builder, - { - action: chunter.actionImpl.ArchiveChannel, - label: chunter.string.ArchiveChannel, - icon: view.icon.Archive, - input: 'focus', - category: chunter.category.Chunter, - target: chunter.class.Channel, - query: { - archived: false - }, - context: { - mode: 'context', - group: 'tools' - } - }, - chunter.action.ArchiveChannel - ) - builder.createDoc( view.class.Viewlet, core.space.Model, @@ -289,43 +258,6 @@ export function createModel (builder: Builder): void { chunter.viewlet.Channels ) - createAction( - builder, - { - action: chunter.actionImpl.UnarchiveChannel, - label: chunter.string.UnarchiveChannel, - icon: view.icon.Archive, - input: 'focus', - category: chunter.category.Chunter, - target: chunter.class.Channel, - query: { - archived: true - }, - context: { - mode: 'context', - group: 'tools' - } - }, - chunter.action.UnarchiveChannel - ) - - createAction( - builder, - { - action: chunter.actionImpl.ConvertDmToPrivateChannel, - label: chunter.string.ConvertToPrivate, - icon: chunter.icon.Lock, - input: 'focus', - category: chunter.category.Chunter, - target: chunter.class.DirectMessage, - context: { - mode: 'context', - group: 'edit' - } - }, - chunter.action.ConvertToPrivate - ) - builder.createDoc( workbench.class.Application, core.space.Model, @@ -348,28 +280,6 @@ export function createModel (builder: Builder): void { encode: chunter.function.GetThreadLink }) - createAction( - builder, - { - action: view.actionImpl.CopyTextToClipboard, - actionProps: { - textProvider: chunter.function.GetLink - }, - label: chunter.string.CopyLink, - icon: chunter.icon.Copy, - input: 'none', - category: chunter.category.Chunter, - target: activity.class.ActivityMessage, - visibilityTester: chunter.function.CanCopyMessageLink, - context: { - mode: ['context', 'browser'], - application: chunter.app.Chunter, - group: 'copy' - } - }, - chunter.action.CopyChatMessageLink - ) - builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ClassFilters, { filters: [] }) @@ -446,19 +356,6 @@ export function createModel (builder: Builder): void { chunter.ids.ThreadNotification ) - createAction(builder, { - ...viewTemplates.open, - target: chunter.class.Channel, - context: { - mode: ['browser', 'context'], - group: 'create' - }, - action: workbench.actionImpl.Navigate, - actionProps: { - mode: 'space' - } - }) - builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { label: chunter.string.Comments, position: 60, @@ -496,105 +393,6 @@ export function createModel (builder: Builder): void { chunter.ids.ThreadMessageViewlet ) - createAction( - builder, - { - action: chunter.actionImpl.DeleteChatMessage, - label: view.string.Delete, - icon: view.icon.Delete, - input: 'focus', - keyBinding: ['Backspace'], - category: chunter.category.Chunter, - target: chunter.class.ChatMessage, - visibilityTester: chunter.function.CanDeleteMessage, - context: { mode: ['context', 'browser'], group: 'remove' } - }, - chunter.action.DeleteChatMessage - ) - - createAction( - builder, - { - ...actionTemplates.removeChannel, - query: { - attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } - } - }, - chunter.action.RemoveChannel - ) - - createAction( - builder, - { - ...actionTemplates.removeChannel, - label: chunter.string.CloseConversation, - query: { - attachedToClass: chunter.class.DirectMessage - } - }, - chunter.action.CloseConversation - ) - - createAction( - builder, - { - ...actionTemplates.removeChannel, - action: chunter.actionImpl.LeaveChannel, - label: chunter.string.LeaveChannel, - query: { - attachedToClass: chunter.class.Channel - } - }, - chunter.action.LeaveChannel - ) - - createAction(builder, { - ...notificationActionTemplates.pinContext, - label: chunter.string.StarChannel, - query: { - attachedToClass: chunter.class.Channel - }, - override: [notification.action.PinDocNotifyContext] - }) - - createAction(builder, { - ...notificationActionTemplates.unpinContext, - label: chunter.string.UnstarChannel, - query: { - attachedToClass: chunter.class.Channel - } - }) - - createAction(builder, { - ...notificationActionTemplates.pinContext, - label: chunter.string.StarConversation, - query: { - attachedToClass: chunter.class.DirectMessage - } - }) - - createAction(builder, { - ...notificationActionTemplates.unpinContext, - label: chunter.string.UnstarConversation, - query: { - attachedToClass: chunter.class.DirectMessage - } - }) - - createAction(builder, { - ...notificationActionTemplates.pinContext, - query: { - attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } - } - }) - - createAction(builder, { - ...notificationActionTemplates.unpinContext, - query: { - attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } - } - }) - builder.createDoc(activity.class.ActivityExtension, core.space.Model, { ofClass: chunter.class.Channel, components: { input: chunter.component.ChatMessageInput } @@ -645,25 +443,6 @@ export function createModel (builder: Builder): void { function: chunter.function.ReplyToThread }) - createAction( - builder, - { - action: chunter.actionImpl.ReplyToThread, - label: chunter.string.ReplyToThread, - icon: chunter.icon.Thread, - input: 'focus', - category: chunter.category.Chunter, - target: activity.class.ActivityMessage, - visibilityTester: chunter.function.CanReplyToThread, - inline: true, - context: { - mode: 'context', - group: 'edit' - } - }, - chunter.action.ReplyToThreadAction - ) - builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ClassFilters, { filters: ['name', 'topic', 'private', 'archived', 'members'], strict: true @@ -680,6 +459,8 @@ export function createModel (builder: Builder): void { ignoredTypes: [], enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification] }) + + defineActions(builder) } export default chunter diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts index 23a6601ba2..9f4e177b1d 100644 --- a/plugins/chunter-resources/src/channelDataProvider.ts +++ b/plugins/chunter-resources/src/channelDataProvider.ts @@ -84,6 +84,8 @@ export class ChannelDataProvider implements IChannelDataProvider { private readonly isInitialLoadedStore = writable(false) private readonly isTailLoading = writable(false) + readonly isTailLoaded = writable(false) + public datesStore = writable([]) public newTimestampStore = writable(undefined) @@ -264,6 +266,7 @@ export class ChannelDataProvider implements IChannelDataProvider { this.tailStore.set(res.reverse()) } + this.isTailLoaded.set(true) this.isTailLoading.set(false) }, { @@ -557,6 +560,7 @@ export class ChannelDataProvider implements IChannelDataProvider { this.isInitialLoadedStore.set(false) this.tailQuery.unsubscribe() this.tailStart = undefined + this.isTailLoaded.set(false) this.backwardNextPromise = undefined this.forwardNextPromise = undefined this.forwardNextStore.set(undefined) diff --git a/plugins/chunter-resources/src/components/ChannelScrollView.svelte b/plugins/chunter-resources/src/components/ChannelScrollView.svelte index e9f09049f2..eff16efd66 100644 --- a/plugins/chunter-resources/src/components/ChannelScrollView.svelte +++ b/plugins/chunter-resources/src/components/ChannelScrollView.svelte @@ -32,6 +32,7 @@ import { Loading, ModernButton, Scroller, ScrollParams } from '@hcengineering/ui' import { afterUpdate, beforeUpdate, onDestroy, onMount, tick } from 'svelte' import { get } from 'svelte/store' + import { DocNotifyContext } from '@hcengineering/notification' import { ChannelDataProvider, MessageMetadata } from '../channelDataProvider' import { @@ -52,7 +53,7 @@ export let objectClass: Ref> export let objectId: Ref export let selectedMessageId: Ref | undefined = undefined - export let scrollElement: HTMLDivElement | undefined = undefined + export let scrollElement: HTMLDivElement | undefined | null = undefined export let startFromBottom = false export let selectedFilters: Ref[] = [] export let embedded = false @@ -70,9 +71,9 @@ const loadMoreThreshold = 40 const client = getClient() - const hierarchy = client.getHierarchy() const inboxClient = InboxNotificationsClientImpl.getClient() const contextByDocStore = inboxClient.contextByDoc + const notificationsByContextStore = inboxClient.inboxNotificationsByContext let filters: ActivityMessagesFilter[] = [] const filterResources = new Map< @@ -83,6 +84,7 @@ const messagesStore = provider.messagesStore const isLoadingStore = provider.isLoadingStore const isLoadingMoreStore = provider.isLoadingMoreStore + const isTailLoadedStore = provider.isTailLoaded const newTimestampStore = provider.newTimestampStore const datesStore = provider.datesStore const metadataStore = provider.metadataStore @@ -91,7 +93,7 @@ let displayMessages: DisplayActivityMessage[] = [] let extensions: ActivityExtension[] = [] - let scroller: Scroller | undefined = undefined + let scroller: Scroller | undefined | null = undefined let separatorElement: HTMLDivElement | undefined = undefined let scrollContentBox: HTMLDivElement | undefined = undefined @@ -137,7 +139,7 @@ }) function scrollToBottom (afterScrollFn?: () => void): void { - if (scroller !== undefined && scrollElement !== undefined) { + if (scroller != null && scrollElement != null) { scroller.scrollBy(scrollElement.scrollHeight) updateSelectedDate() afterScrollFn?.() @@ -279,7 +281,7 @@ scrollToRestore = scrollElement?.scrollHeight ?? 0 provider.addNextChunk('backward', messages[0]?.createdOn, limit) backwardRequested = true - } else if (shouldLoadMoreDown()) { + } else if (shouldLoadMoreDown() && !$isTailLoadedStore) { scrollToRestore = 0 shouldScrollToNew = false isScrollAtBottom = false @@ -637,7 +639,7 @@ function updateDownButtonVisibility ( metadata: MessageMetadata[], displayMessages: DisplayActivityMessage[], - element?: HTMLDivElement + element?: HTMLDivElement | null ): void { if (metadata.length === 0 || displayMessages.length === 0) { showScrollDownButton = false @@ -677,6 +679,22 @@ } } + $: forceReadContext(isScrollAtBottom, notifyContext) + + function forceReadContext (isScrollAtBottom: boolean, context?: DocNotifyContext): void { + if (context === undefined || !isScrollAtBottom) return + const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context + + if (lastViewedTimestamp >= lastUpdateTimestamp) return + + const notifications = $notificationsByContextStore.get(context._id) ?? [] + const unViewed = notifications.filter(({ isViewed }) => !isViewed) + + if (unViewed.length === 0) { + void inboxClient.readDoc(client, objectId) + } + } + const canLoadNextForwardStore = provider.canLoadNextForwardStore @@ -808,5 +826,17 @@ display: flex; justify-content: center; bottom: -0.75rem; + animation: 1s fadeIn; + animation-fill-mode: forwards; + visibility: hidden; + } + + @keyframes fadeIn { + 99% { + visibility: hidden; + } + 100% { + visibility: visible; + } } diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavItem.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavItem.svelte index 62ade32991..a150169788 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavItem.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavItem.svelte @@ -128,6 +128,7 @@ {count} title={item.title} description={item.description} + secondaryNotifyMarker={(context?.lastViewedTimestamp ?? 0) < (context?.lastUpdateTimestamp ?? 0)} {actions} {type} on:click={() => { diff --git a/plugins/chunter-resources/src/components/chat/navigator/NavItem.svelte b/plugins/chunter-resources/src/components/chat/navigator/NavItem.svelte index a303b7326f..59f3172d73 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/NavItem.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/NavItem.svelte @@ -35,6 +35,7 @@ export let isSelected: boolean = false export let isSecondary: boolean = false export let count: number | null = null + export let secondaryNotifyMarker: boolean = false export let title: string | undefined = undefined export let intlTitle: IntlString | undefined = undefined export let description: string | undefined = undefined @@ -99,6 +100,10 @@
+ {:else if secondaryNotifyMarker} +
+ +
{/if} diff --git a/plugins/chunter-resources/src/components/chat/utils.ts b/plugins/chunter-resources/src/components/chat/utils.ts index 176b04c653..e0b47e8e73 100644 --- a/plugins/chunter-resources/src/components/chat/utils.ts +++ b/plugins/chunter-resources/src/components/chat/utils.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' +import notification, { type DocNotifyContext } from '@hcengineering/notification' import { generateId, type Ref, @@ -353,8 +353,8 @@ function getActivityActions (contexts: DocNotifyContext[]): Action[] { } }, { - icon: view.icon.CheckCircle, - label: notification.string.ArchiveAll, + icon: view.icon.EyeCrossed, + label: view.string.Hide, action: async () => { archiveActivityChannels(contexts) } @@ -400,18 +400,18 @@ export function loadSavedAttachments (): void { } export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise { - const client = InboxNotificationsClientImpl.getClient() - const notificationsByContext = get(client.inboxNotificationsByContext) const ops = getClient().apply(generateId(), 'removeActivityChannels') try { for (const context of contexts) { - const notifications = notificationsByContext.get(context._id) ?? [] - await client.archiveNotifications( - ops, - notifications.map(({ _id }: InboxNotification) => _id) - ) - await ops.remove(context) + await ops.createMixin(context._id, context._class, context.space, chunter.mixin.ChannelInfo, { hidden: true }) + } + const hidden = contexts.map(({ _id }) => _id) + const account = getCurrentAccount() as PersonAccount + const chatInfo = await ops.findOne(chunter.class.ChatInfo, { user: account.person }) + + if (chatInfo !== undefined) { + await ops.update(chatInfo, { hidden: chatInfo.hidden.concat(hidden) }) } } finally { await ops.commit() diff --git a/plugins/chunter-resources/src/utils.ts b/plugins/chunter-resources/src/utils.ts index bfa07e8290..17f6e655e7 100644 --- a/plugins/chunter-resources/src/utils.ts +++ b/plugins/chunter-resources/src/utils.ts @@ -35,10 +35,9 @@ import { type Timestamp, type WithLookup } from '@hcengineering/core' -import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' +import { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import { InboxNotificationsClientImpl, - archiveContextNotifications, isActivityNotification, isMentionNotification } from '@hcengineering/notification-resources' @@ -343,7 +342,12 @@ export async function joinChannel (channel: Channel, value: Ref | Array } } -export async function leaveChannel (channel: Channel, value: Ref | Array>): Promise { +export async function leaveChannel ( + channel: Channel | undefined, + value: Ref | Array> +): Promise { + if (channel === undefined) return + const client = getClient() if (Array.isArray(value)) { @@ -351,10 +355,8 @@ export async function leaveChannel (channel: Channel, value: Ref | Arra await client.update(channel, { $pull: { members: { $in: value } } }) } } else { - const context = await client.findOne(notification.class.DocNotifyContext, { attachedTo: channel._id }) - await client.update(channel, { $pull: { members: value } }) - await removeChannelAction(context, undefined, { object: channel }) + await resetChunterLocIfEqual(channel._id, channel._class, channel) } } @@ -499,11 +501,27 @@ export async function removeChannelAction ( } const client = getClient() + const hierarchy = client.getHierarchy() + const inboxClient = InboxNotificationsClientImpl.getClient() - await archiveContextNotifications(context) - await client.remove(context) + if (hierarchy.isDerived(context.attachedToClass, chunter.class.Channel)) { + const channel = await client.findOne(chunter.class.Channel, { _id: context.attachedTo as Ref }) + await leaveChannel(channel, getCurrentAccount()._id) + } else { + const object = await client.findOne(context.attachedToClass, { _id: context.attachedTo }) + const account = getCurrentAccount() as PersonAccount + + await client.createMixin(context._id, context._class, context.space, chunter.mixin.ChannelInfo, { hidden: true }) + + const chatInfo = await client.findOne(chunter.class.ChatInfo, { user: account.person }) + + if (chatInfo !== undefined) { + await client.update(chatInfo, { hidden: chatInfo.hidden.concat([context._id]) }) + } + await resetChunterLocIfEqual(context.attachedTo, context.attachedToClass, object) + } - await resetChunterLocIfEqual(context.attachedTo, context.attachedToClass, props?.object) + void inboxClient.readDoc(client, context.attachedTo) } export function isThreadMessage (message: ActivityMessage): message is ThreadMessage { diff --git a/plugins/notification-resources/src/components/NotifyMarker.svelte b/plugins/notification-resources/src/components/NotifyMarker.svelte index 4aa0781f65..806f514963 100644 --- a/plugins/notification-resources/src/components/NotifyMarker.svelte +++ b/plugins/notification-resources/src/components/NotifyMarker.svelte @@ -14,13 +14,14 @@ --> {#if count > 0} -
+
{#if count > maxNumber} {maxNumber}+ {:else} @@ -29,6 +30,10 @@
{/if} +{#if count === 0 && kind === 'secondary'} +
+{/if} +