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 5a468e761d..4156981bf9 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, @@ -46,17 +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 } 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' @@ -133,18 +138,18 @@ export class TObjectChatPanel extends TClass implements ObjectChatPanel { ignoreKeys!: string[] } -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' } - } -}) +@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 +} export function createModel (builder: Builder): void { builder.createModel( @@ -154,7 +159,9 @@ export function createModel (builder: Builder): void { TChatMessage, TThreadMessage, TChatMessageViewlet, - TObjectChatPanel + TObjectChatPanel, + TChatInfo, + TChannelInfo ) const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage] @@ -236,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, @@ -271,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, @@ -330,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: [] }) @@ -428,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, @@ -478,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 } @@ -627,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 @@ -662,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/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/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/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/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..9d2a945af4 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte @@ -55,7 +55,7 @@ notification.class.DocNotifyContext, { ...model.query, - hidden: false, + [`${chunter.mixin.ChannelInfo}.hidden`]: { $ne: true }, user: getCurrentAccount()._id }, (res: DocNotifyContext[]) => { 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 resetChunterLocIfEqual(context.attachedTo, context.attachedToClass, props?.object) + 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) + } + + void inboxClient.readDoc(client, context.attachedTo) } export function isThreadMessage (message: ActivityMessage): message is ThreadMessage { 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/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/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} +