UBERF-7016: Hide channels without any activity long time (#6176)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-07-30 09:53:24 +04:00 committed by GitHub
parent 1340ca78a9
commit 416eb9942e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 743 additions and 346 deletions

View File

@ -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] }
}
})
}

View File

@ -22,10 +22,12 @@ import {
type ChatMessageViewlet, type ChatMessageViewlet,
type ChunterSpace, type ChunterSpace,
type ObjectChatPanel, type ObjectChatPanel,
type ThreadMessage type ThreadMessage,
type ChatInfo,
type ChannelInfo
} from '@hcengineering/chunter' } from '@hcengineering/chunter'
import presentation from '@hcengineering/model-presentation' import presentation from '@hcengineering/model-presentation'
import contact from '@hcengineering/contact' import contact, { type Person } from '@hcengineering/contact'
import { import {
type Class, type Class,
type Doc, type Doc,
@ -46,17 +48,20 @@ import {
TypeRef, TypeRef,
TypeString, TypeString,
TypeTimestamp, TypeTimestamp,
UX UX,
Hidden
} from '@hcengineering/model' } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment' import attachment from '@hcengineering/model-attachment'
import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core' import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core'
import notification, { notificationActionTemplates } from '@hcengineering/model-notification' import notification, { TDocNotifyContext } from '@hcengineering/model-notification'
import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view' import view from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { TActivityMessage } from '@hcengineering/model-activity' import { TActivityMessage } from '@hcengineering/model-activity'
import { type DocNotifyContext } from '@hcengineering/notification'
import chunter from './plugin' import chunter from './plugin'
import { defineActions } from './actions'
export { chunterId } from '@hcengineering/chunter' export { chunterId } from '@hcengineering/chunter'
export { chunterOperation } from './migration' export { chunterOperation } from './migration'
@ -133,18 +138,18 @@ export class TObjectChatPanel extends TClass implements ObjectChatPanel {
ignoreKeys!: string[] ignoreKeys!: string[]
} }
const actionTemplates = template({ @Mixin(chunter.mixin.ChannelInfo, notification.class.DocNotifyContext)
removeChannel: { export class TChannelInfo extends TDocNotifyContext implements ChannelInfo {
action: chunter.actionImpl.RemoveChannel, @Hidden()
label: view.string.Archive, hidden!: boolean
icon: view.icon.Delete, }
input: 'focus',
keyBinding: ['Backspace'], @Model(chunter.class.ChatInfo, core.class.Doc, DOMAIN_CHUNTER)
category: chunter.category.Chunter, export class TChatInfo extends TDoc implements ChatInfo {
target: notification.class.DocNotifyContext, user!: Ref<Person>
context: { mode: ['context', 'browser'], group: 'remove' } hidden!: Ref<DocNotifyContext>[]
} timestamp!: Timestamp
}) }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel( builder.createModel(
@ -154,7 +159,9 @@ export function createModel (builder: Builder): void {
TChatMessage, TChatMessage,
TThreadMessage, TThreadMessage,
TChatMessageViewlet, TChatMessageViewlet,
TObjectChatPanel TObjectChatPanel,
TChatInfo,
TChannelInfo
) )
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage] const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
@ -236,26 +243,6 @@ export function createModel (builder: Builder): void {
chunter.category.Chunter 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( builder.createDoc(
view.class.Viewlet, view.class.Viewlet,
core.space.Model, core.space.Model,
@ -271,43 +258,6 @@ export function createModel (builder: Builder): void {
chunter.viewlet.Channels 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( builder.createDoc(
workbench.class.Application, workbench.class.Application,
core.space.Model, core.space.Model,
@ -330,28 +280,6 @@ export function createModel (builder: Builder): void {
encode: chunter.function.GetThreadLink 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, { builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ClassFilters, {
filters: [] filters: []
}) })
@ -428,19 +356,6 @@ export function createModel (builder: Builder): void {
chunter.ids.ThreadNotification 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, { builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: chunter.string.Comments, label: chunter.string.Comments,
position: 60, position: 60,
@ -478,105 +393,6 @@ export function createModel (builder: Builder): void {
chunter.ids.ThreadMessageViewlet 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, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: chunter.class.Channel, ofClass: chunter.class.Channel,
components: { input: chunter.component.ChatMessageInput } components: { input: chunter.component.ChatMessageInput }
@ -627,25 +443,6 @@ export function createModel (builder: Builder): void {
function: chunter.function.ReplyToThread 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, { builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ClassFilters, {
filters: ['name', 'topic', 'private', 'archived', 'members'], filters: ['name', 'topic', 'private', 'archived', 'members'],
strict: true strict: true
@ -662,6 +459,8 @@ export function createModel (builder: Builder): void {
ignoredTypes: [], ignoredTypes: [],
enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification] enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification]
}) })
defineActions(builder)
} }
export default chunter export default chunter

View File

@ -63,8 +63,7 @@ export async function createDocNotifyContexts (
await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, { await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, {
user: user._id, user: user._id,
attachedTo, attachedTo,
attachedToClass, attachedToClass
hidden: false
}) })
} }
} }

View File

@ -203,10 +203,6 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext {
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
attachedToClass!: Ref<Class<Doc>> attachedToClass!: Ref<Class<Doc>>
@Prop(TypeBoolean(), core.string.Archived)
@Index(IndexKind.Indexed)
hidden!: boolean
@Prop(TypeDate(), core.string.Date) @Prop(TypeDate(), core.string.Date)
lastViewedTimestamp?: Timestamp lastViewedTimestamp?: Timestamp

View File

@ -20,6 +20,7 @@ import chunter from '@hcengineering/chunter'
import serverNotification from '@hcengineering/server-notification' import serverNotification from '@hcengineering/server-notification'
import serverCore, { type ObjectDDParticipant } from '@hcengineering/server-core' import serverCore, { type ObjectDDParticipant } from '@hcengineering/server-core'
import serverChunter from '@hcengineering/server-chunter' import serverChunter from '@hcengineering/server-chunter'
import notification from '@hcengineering/notification'
export { serverChunterId } from '@hcengineering/server-chunter' 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, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.OnChatMessageRemoved, trigger: serverChunter.trigger.OnChatMessageRemoved,
txMatch: { txMatch: {

View File

@ -84,6 +84,8 @@ export class ChannelDataProvider implements IChannelDataProvider {
private readonly isInitialLoadedStore = writable(false) private readonly isInitialLoadedStore = writable(false)
private readonly isTailLoading = writable(false) private readonly isTailLoading = writable(false)
readonly isTailLoaded = writable(false)
public datesStore = writable<Timestamp[]>([]) public datesStore = writable<Timestamp[]>([])
public newTimestampStore = writable<Timestamp | undefined>(undefined) public newTimestampStore = writable<Timestamp | undefined>(undefined)
@ -264,6 +266,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.tailStore.set(res.reverse()) this.tailStore.set(res.reverse())
} }
this.isTailLoaded.set(true)
this.isTailLoading.set(false) this.isTailLoading.set(false)
}, },
{ {
@ -557,6 +560,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.isInitialLoadedStore.set(false) this.isInitialLoadedStore.set(false)
this.tailQuery.unsubscribe() this.tailQuery.unsubscribe()
this.tailStart = undefined this.tailStart = undefined
this.isTailLoaded.set(false)
this.backwardNextPromise = undefined this.backwardNextPromise = undefined
this.forwardNextPromise = undefined this.forwardNextPromise = undefined
this.forwardNextStore.set(undefined) this.forwardNextStore.set(undefined)

View File

@ -32,6 +32,7 @@
import { Loading, ModernButton, Scroller, ScrollParams } from '@hcengineering/ui' import { Loading, ModernButton, Scroller, ScrollParams } from '@hcengineering/ui'
import { afterUpdate, beforeUpdate, onDestroy, onMount, tick } from 'svelte' import { afterUpdate, beforeUpdate, onDestroy, onMount, tick } from 'svelte'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { DocNotifyContext } from '@hcengineering/notification'
import { ChannelDataProvider, MessageMetadata } from '../channelDataProvider' import { ChannelDataProvider, MessageMetadata } from '../channelDataProvider'
import { import {
@ -52,7 +53,7 @@
export let objectClass: Ref<Class<Doc>> export let objectClass: Ref<Class<Doc>>
export let objectId: Ref<Doc> export let objectId: Ref<Doc>
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let scrollElement: HTMLDivElement | undefined = undefined export let scrollElement: HTMLDivElement | undefined | null = undefined
export let startFromBottom = false export let startFromBottom = false
export let selectedFilters: Ref<ActivityMessagesFilter>[] = [] export let selectedFilters: Ref<ActivityMessagesFilter>[] = []
export let embedded = false export let embedded = false
@ -70,9 +71,9 @@
const loadMoreThreshold = 40 const loadMoreThreshold = 40
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy()
const inboxClient = InboxNotificationsClientImpl.getClient() const inboxClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = inboxClient.contextByDoc const contextByDocStore = inboxClient.contextByDoc
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
let filters: ActivityMessagesFilter[] = [] let filters: ActivityMessagesFilter[] = []
const filterResources = new Map< const filterResources = new Map<
@ -83,6 +84,7 @@
const messagesStore = provider.messagesStore const messagesStore = provider.messagesStore
const isLoadingStore = provider.isLoadingStore const isLoadingStore = provider.isLoadingStore
const isLoadingMoreStore = provider.isLoadingMoreStore const isLoadingMoreStore = provider.isLoadingMoreStore
const isTailLoadedStore = provider.isTailLoaded
const newTimestampStore = provider.newTimestampStore const newTimestampStore = provider.newTimestampStore
const datesStore = provider.datesStore const datesStore = provider.datesStore
const metadataStore = provider.metadataStore const metadataStore = provider.metadataStore
@ -91,7 +93,7 @@
let displayMessages: DisplayActivityMessage[] = [] let displayMessages: DisplayActivityMessage[] = []
let extensions: ActivityExtension[] = [] let extensions: ActivityExtension[] = []
let scroller: Scroller | undefined = undefined let scroller: Scroller | undefined | null = undefined
let separatorElement: HTMLDivElement | undefined = undefined let separatorElement: HTMLDivElement | undefined = undefined
let scrollContentBox: HTMLDivElement | undefined = undefined let scrollContentBox: HTMLDivElement | undefined = undefined
@ -137,7 +139,7 @@
}) })
function scrollToBottom (afterScrollFn?: () => void): void { function scrollToBottom (afterScrollFn?: () => void): void {
if (scroller !== undefined && scrollElement !== undefined) { if (scroller != null && scrollElement != null) {
scroller.scrollBy(scrollElement.scrollHeight) scroller.scrollBy(scrollElement.scrollHeight)
updateSelectedDate() updateSelectedDate()
afterScrollFn?.() afterScrollFn?.()
@ -279,7 +281,7 @@
scrollToRestore = scrollElement?.scrollHeight ?? 0 scrollToRestore = scrollElement?.scrollHeight ?? 0
provider.addNextChunk('backward', messages[0]?.createdOn, limit) provider.addNextChunk('backward', messages[0]?.createdOn, limit)
backwardRequested = true backwardRequested = true
} else if (shouldLoadMoreDown()) { } else if (shouldLoadMoreDown() && !$isTailLoadedStore) {
scrollToRestore = 0 scrollToRestore = 0
shouldScrollToNew = false shouldScrollToNew = false
isScrollAtBottom = false isScrollAtBottom = false
@ -637,7 +639,7 @@
function updateDownButtonVisibility ( function updateDownButtonVisibility (
metadata: MessageMetadata[], metadata: MessageMetadata[],
displayMessages: DisplayActivityMessage[], displayMessages: DisplayActivityMessage[],
element?: HTMLDivElement element?: HTMLDivElement | null
): void { ): void {
if (metadata.length === 0 || displayMessages.length === 0) { if (metadata.length === 0 || displayMessages.length === 0) {
showScrollDownButton = false 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 const canLoadNextForwardStore = provider.canLoadNextForwardStore
</script> </script>
@ -808,5 +826,17 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
bottom: -0.75rem; bottom: -0.75rem;
animation: 1s fadeIn;
animation-fill-mode: forwards;
visibility: hidden;
}
@keyframes fadeIn {
99% {
visibility: hidden;
}
100% {
visibility: visible;
}
} }
</style> </style>

View File

@ -63,8 +63,7 @@
await client.createDoc(notification.class.DocNotifyContext, channelId, { await client.createDoc(notification.class.DocNotifyContext, channelId, {
user: accountId, user: accountId,
attachedTo: channelId, attachedTo: channelId,
attachedToClass: chunter.class.Channel, attachedToClass: chunter.class.Channel
hidden: false
}) })
openChannel(channelId, chunter.class.Channel) openChannel(channelId, chunter.class.Channel)

View File

@ -81,7 +81,6 @@
}) })
if (context !== undefined) { if (context !== undefined) {
await client.diffUpdate(context, { hidden: false })
openChannel(dmId, chunter.class.DirectMessage) openChannel(dmId, chunter.class.DirectMessage)
return return
@ -90,8 +89,7 @@
await client.createDoc(notification.class.DocNotifyContext, dmId, { await client.createDoc(notification.class.DocNotifyContext, dmId, {
user: myAccId, user: myAccId,
attachedTo: dmId, attachedTo: dmId,
attachedToClass: chunter.class.DirectMessage, attachedToClass: chunter.class.DirectMessage
hidden: false
}) })
openChannel(dmId, chunter.class.DirectMessage) openChannel(dmId, chunter.class.DirectMessage)

View File

@ -55,7 +55,7 @@
notification.class.DocNotifyContext, notification.class.DocNotifyContext,
{ {
...model.query, ...model.query,
hidden: false, [`${chunter.mixin.ChannelInfo}.hidden`]: { $ne: true },
user: getCurrentAccount()._id user: getCurrentAccount()._id
}, },
(res: DocNotifyContext[]) => { (res: DocNotifyContext[]) => {

View File

@ -128,6 +128,7 @@
{count} {count}
title={item.title} title={item.title}
description={item.description} description={item.description}
secondaryNotifyMarker={(context?.lastViewedTimestamp ?? 0) < (context?.lastUpdateTimestamp ?? 0)}
{actions} {actions}
{type} {type}
on:click={() => { on:click={() => {

View File

@ -35,6 +35,7 @@
export let isSelected: boolean = false export let isSelected: boolean = false
export let isSecondary: boolean = false export let isSecondary: boolean = false
export let count: number | null = null export let count: number | null = null
export let secondaryNotifyMarker: boolean = false
export let title: string | undefined = undefined export let title: string | undefined = undefined
export let intlTitle: IntlString | undefined = undefined export let intlTitle: IntlString | undefined = undefined
export let description: string | undefined = undefined export let description: string | undefined = undefined
@ -99,6 +100,10 @@
<div class="antiHSpacer" /> <div class="antiHSpacer" />
<NotifyMarker {count} /> <NotifyMarker {count} />
<div class="antiHSpacer" /> <div class="antiHSpacer" />
{:else if secondaryNotifyMarker}
<div class="antiHSpacer" />
<NotifyMarker count={0} kind="secondary" size="x-small" />
<div class="antiHSpacer" />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</NavItem> </NavItem>

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 notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import notification, { type DocNotifyContext } from '@hcengineering/notification'
import { import {
generateId, generateId,
type Ref, type Ref,
@ -353,8 +353,8 @@ function getActivityActions (contexts: DocNotifyContext[]): Action[] {
} }
}, },
{ {
icon: view.icon.CheckCircle, icon: view.icon.EyeCrossed,
label: notification.string.ArchiveAll, label: view.string.Hide,
action: async () => { action: async () => {
archiveActivityChannels(contexts) archiveActivityChannels(contexts)
} }
@ -400,18 +400,18 @@ export function loadSavedAttachments (): void {
} }
export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> { export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
const client = InboxNotificationsClientImpl.getClient()
const notificationsByContext = get(client.inboxNotificationsByContext)
const ops = getClient().apply(generateId(), 'removeActivityChannels') const ops = getClient().apply(generateId(), 'removeActivityChannels')
try { try {
for (const context of contexts) { for (const context of contexts) {
const notifications = notificationsByContext.get(context._id) ?? [] await ops.createMixin(context._id, context._class, context.space, chunter.mixin.ChannelInfo, { hidden: true })
await client.archiveNotifications( }
ops, const hidden = contexts.map(({ _id }) => _id)
notifications.map(({ _id }: InboxNotification) => _id) const account = getCurrentAccount() as PersonAccount
) const chatInfo = await ops.findOne(chunter.class.ChatInfo, { user: account.person })
await ops.remove(context)
if (chatInfo !== undefined) {
await ops.update(chatInfo, { hidden: chatInfo.hidden.concat(hidden) })
} }
} finally { } finally {
await ops.commit() await ops.commit()

View File

@ -35,10 +35,9 @@ import {
type Timestamp, type Timestamp,
type WithLookup type WithLookup
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { import {
InboxNotificationsClientImpl, InboxNotificationsClientImpl,
archiveContextNotifications,
isActivityNotification, isActivityNotification,
isMentionNotification isMentionNotification
} from '@hcengineering/notification-resources' } from '@hcengineering/notification-resources'
@ -343,7 +342,12 @@ export async function joinChannel (channel: Channel, value: Ref<Account> | Array
} }
} }
export async function leaveChannel (channel: Channel, value: Ref<Account> | Array<Ref<Account>>): Promise<void> { export async function leaveChannel (
channel: Channel | undefined,
value: Ref<Account> | Array<Ref<Account>>
): Promise<void> {
if (channel === undefined) return
const client = getClient() const client = getClient()
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -351,10 +355,8 @@ export async function leaveChannel (channel: Channel, value: Ref<Account> | Arra
await client.update(channel, { $pull: { members: { $in: value } } }) await client.update(channel, { $pull: { members: { $in: value } } })
} }
} else { } else {
const context = await client.findOne(notification.class.DocNotifyContext, { attachedTo: channel._id })
await client.update(channel, { $pull: { members: value } }) 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 client = getClient()
const hierarchy = client.getHierarchy()
const inboxClient = InboxNotificationsClientImpl.getClient()
await archiveContextNotifications(context) if (hierarchy.isDerived(context.attachedToClass, chunter.class.Channel)) {
await client.remove(context) const channel = await client.findOne(chunter.class.Channel, { _id: context.attachedTo as Ref<Channel> })
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 { export function isThreadMessage (message: ActivityMessage): message is ThreadMessage {

View File

@ -15,11 +15,12 @@
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity' import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
import type { Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core' 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 type { Asset, Plugin } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { Action } from '@hcengineering/view' import { Action } from '@hcengineering/view'
import { Person } from '@hcengineering/contact'
/** /**
* @public * @public
@ -72,6 +73,16 @@ export interface ChatMessageViewlet extends ActivityMessageViewlet {
label?: IntlString label?: IntlString
} }
export interface ChatInfo extends Doc {
user: Ref<Person>
hidden: Ref<DocNotifyContext>[]
timestamp: Timestamp
}
export interface ChannelInfo extends DocNotifyContext {
hidden: boolean
}
/** /**
* @public * @public
*/ */
@ -110,10 +121,12 @@ export default plugin(chunterId, {
Channel: '' as Ref<Class<Channel>>, Channel: '' as Ref<Class<Channel>>,
DirectMessage: '' as Ref<Class<DirectMessage>>, DirectMessage: '' as Ref<Class<DirectMessage>>,
ChatMessage: '' as Ref<Class<ChatMessage>>, ChatMessage: '' as Ref<Class<ChatMessage>>,
ChatMessageViewlet: '' as Ref<Class<ChatMessageViewlet>> ChatMessageViewlet: '' as Ref<Class<ChatMessageViewlet>>,
ChatInfo: '' as Ref<Class<ChatInfo>>
}, },
mixin: { mixin: {
ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>> ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>>,
ChannelInfo: '' as Ref<Mixin<ChannelInfo>>
}, },
string: { string: {
Reactions: '' as IntlString, Reactions: '' as IntlString,

View File

@ -27,7 +27,7 @@
$: notifyContext = $contextByDocStore.get(value._id) $: notifyContext = $contextByDocStore.get(value._id)
$: inboxNotifications = notifyContext ? $inboxNotificationsByContextStore.get(notifyContext._id) ?? [] : [] $: inboxNotifications = notifyContext ? $inboxNotificationsByContextStore.get(notifyContext._id) ?? [] : []
$: hasNotification = !notifyContext?.hidden && inboxNotifications.some(({ isViewed }) => !isViewed) $: hasNotification = inboxNotifications.some(({ isViewed }) => !isViewed)
</script> </script>
{#if hasNotification} {#if hasNotification}

View File

@ -14,13 +14,14 @@
--> -->
<script lang="ts"> <script lang="ts">
export let count: number = 0 export let count: number = 0
export let size: 'small' | 'medium' = 'small' export let kind: 'primary' | 'secondary' = 'primary'
export let size: 'x-small' | 'small' | 'medium' = 'small'
const maxNumber = 9 const maxNumber = 9
</script> </script>
{#if count > 0} {#if count > 0}
<div class="notifyMarker {size}"> <div class="notifyMarker {size} {kind}">
{#if count > maxNumber} {#if count > maxNumber}
{maxNumber}+ {maxNumber}+
{:else} {:else}
@ -29,6 +30,10 @@
</div> </div>
{/if} {/if}
{#if count === 0 && kind === 'secondary'}
<div class="notifyMarker {size} {kind}" />
{/if}
<style lang="scss"> <style lang="scss">
.notifyMarker { .notifyMarker {
display: flex; display: flex;
@ -36,10 +41,22 @@
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
border-radius: 50%; border-radius: 50%;
background-color: var(--global-higlight-Color);
color: var(--global-on-accent-TextColor);
font-weight: 700; font-weight: 700;
&.primary {
background-color: var(--global-higlight-Color);
color: var(--global-on-accent-TextColor);
}
&.secondary {
background-color: var(--global-subtle-BackgroundColor);
}
&.x-small {
width: 0.75rem;
height: 0.75rem;
}
&.small { &.small {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;

View File

@ -73,7 +73,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
return inboxNotifications.reduce((result, notification) => { return inboxNotifications.reduce((result, notification) => {
const notifyContext = notifyContexts.find(({ _id }) => _id === notification.docNotifyContext) const notifyContext = notifyContexts.find(({ _id }) => _id === notification.docNotifyContext)
if (notifyContext === undefined || notifyContext.hidden) { if (notifyContext === undefined) {
return result 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<Ref<InboxNotification>>): Promise<void> { async readNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {

View File

@ -87,16 +87,10 @@ export function loadNotificationSettings (): void {
loadNotificationSettings() loadNotificationSettings()
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> { export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
if (docNotifyContext.hidden) {
return false
}
return docNotifyContext.isPinned !== true return docNotifyContext.isPinned !== true
} }
export async function hasDocNotifyContextUnpinAction (docNotifyContext: DocNotifyContext): Promise<boolean> { export async function hasDocNotifyContextUnpinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
if (docNotifyContext.hidden) {
return false
}
return docNotifyContext.isPinned === true return docNotifyContext.isPinned === true
} }

View File

@ -276,7 +276,6 @@ export interface DocNotifyContext extends Doc {
attachedTo: Ref<Doc> attachedTo: Ref<Doc>
attachedToClass: Ref<Class<Doc>> attachedToClass: Ref<Class<Doc>>
hidden: boolean
isPinned?: boolean isPinned?: boolean
lastViewedTimestamp?: Timestamp lastViewedTimestamp?: Timestamp
lastUpdateTimestamp?: Timestamp lastUpdateTimestamp?: Timestamp

View File

@ -91,7 +91,7 @@
): boolean { ): boolean {
const context = notifyContextByDoc.get(space._id) const context = notifyContextByDoc.get(space._id)
if (context === undefined || context.hidden) { if (context === undefined) {
return false return false
} }

View File

@ -53,7 +53,7 @@
): boolean { ): boolean {
const notifyContext = docUpdates.get(space._id) const notifyContext = docUpdates.get(space._id)
if (notifyContext === undefined) return false 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) $: visibleSpace = spaces.find((space) => currentSpace === space._id)
</script> </script>

View File

@ -14,8 +14,15 @@
// //
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity' import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter' import chunter, {
import { Person, PersonAccount } from '@hcengineering/contact' Channel,
ChannelInfo,
ChatMessage,
chunterId,
ChunterSpace,
ThreadMessage
} from '@hcengineering/chunter'
import contact, { Person, PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
AttachedDoc, AttachedDoc,
@ -27,6 +34,7 @@ import core, {
FindResult, FindResult,
Hierarchy, Hierarchy,
Ref, Ref,
Timestamp,
Tx, Tx,
TxCollectionCUD, TxCollectionCUD,
TxCreateDoc, TxCreateDoc,
@ -34,9 +42,10 @@ import core, {
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc TxUpdateDoc,
UserStatus
} from '@hcengineering/core' } 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 { getMetadata, IntlString, translate } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core' import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { import {
@ -50,6 +59,9 @@ import { workbenchId } from '@hcengineering/workbench'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
import { encodeObjectURI } from '@hcengineering/view' import { encodeObjectURI } from '@hcengineering/view'
const updateChatInfoDelay = 12 * 60 * 60 * 1000 // 12 hours
const hideChannelDelay = 7 * 24 * 60 * 60 * 1000 // 7 days
/** /**
* @public * @public
*/ */
@ -411,14 +423,12 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
attachedTo: tx.objectId, attachedTo: tx.objectId,
attachedToClass: tx.objectClass, attachedToClass: tx.objectClass,
user: addedMember, user: addedMember,
hidden: false,
lastViewedTimestamp: tx.modifiedOn lastViewedTimestamp: tx.modifiedOn
}) })
await control.apply([createTx]) await control.apply([createTx])
} else { } else {
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
hidden: false,
lastViewedTimestamp: tx.modifiedOn lastViewedTimestamp: tx.modifiedOn
}) })
@ -455,13 +465,201 @@ async function OnCollaboratorsChanged (tx: TxMixin<Doc, Collaborators>, control:
return res return res
} }
async function hideOldDirects (
directs: DocNotifyContext[],
control: TriggerControl,
date: Timestamp
): Promise<TxMixin<DocNotifyContext, ChannelInfo>[]> {
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<TxMixin<DocNotifyContext, ChannelInfo>[]> {
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<TxMixin<DocNotifyContext, ChannelInfo>[]> {
const res: TxMixin<DocNotifyContext, ChannelInfo>[] = []
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<void> {
const account = await control.modelDb.findOne(contact.class.PersonAccount, { _id: status.user as Ref<PersonAccount> })
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<DocNotifyContext>[] = 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<UserStatus>, control: TriggerControl): Promise<Tx[]> {
const tx = TxProcessor.extractTx(originTx) as TxCUD<UserStatus>
if (tx.objectClass !== core.class.UserStatus) return []
if (tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<UserStatus>
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<UserStatus>
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<DocNotifyContext>, control: TriggerControl): Promise<Tx[]> {
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 // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({ export default async () => ({
trigger: { trigger: {
ChunterTrigger, ChunterTrigger,
OnChatMessageRemoved, OnChatMessageRemoved,
OnChannelMembersChanged, OnChannelMembersChanged,
ChatNotificationsHandler ChatNotificationsHandler,
OnUserStatus,
OnContextUpdate
}, },
function: { function: {
CommentRemove, CommentRemove,

View File

@ -31,7 +31,9 @@ export default plugin(serverChunterId, {
ChunterTrigger: '' as Resource<TriggerFunc>, ChunterTrigger: '' as Resource<TriggerFunc>,
OnChatMessageRemoved: '' as Resource<TriggerFunc>, OnChatMessageRemoved: '' as Resource<TriggerFunc>,
OnChannelMembersChanged: '' as Resource<TriggerFunc>, OnChannelMembersChanged: '' as Resource<TriggerFunc>,
ChatNotificationsHandler: '' as Resource<TriggerFunc> ChatNotificationsHandler: '' as Resource<TriggerFunc>,
OnUserStatus: '' as Resource<TriggerFunc>,
OnContextUpdate: '' as Resource<TriggerFunc>
}, },
function: { function: {
CommentRemove: '' as Resource<ObjectDDParticipantFunc>, CommentRemove: '' as Resource<ObjectDDParticipantFunc>,

View File

@ -95,8 +95,7 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
// ) // )
res.push( res.push(
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
lastUpdateTimestamp: tx.modifiedOn, lastUpdateTimestamp: tx.modifiedOn
hidden: false
}) })
) )
} }
@ -106,7 +105,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
user: tx.modifiedBy, user: tx.modifiedBy,
attachedTo: channel._id, attachedTo: channel._id,
attachedToClass: channel._class, attachedToClass: channel._class,
hidden: false,
lastUpdateTimestamp: tx.modifiedOn lastUpdateTimestamp: tx.modifiedOn
// TODO: push inbox notification // TODO: push inbox notification
// txes: [ // txes: [

View File

@ -39,6 +39,7 @@ import core, {
MixinUpdate, MixinUpdate,
Ref, Ref,
RefTo, RefTo,
SortingOrder,
Space, Space,
Timestamp, Timestamp,
toIdMap, toIdMap,
@ -349,9 +350,6 @@ export async function pushInboxNotifications (
} }
const context = getDocNotifyContext(contexts, account._id, attachedTo, res) const context = getDocNotifyContext(contexts, account._id, attachedTo, res)
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const isHidden = !!context?.hidden
let docNotifyContextId: Ref<DocNotifyContext> let docNotifyContextId: Ref<DocNotifyContext>
if (context === undefined) { if (context === undefined) {
@ -359,7 +357,6 @@ export async function pushInboxNotifications (
user: account._id, user: account._id,
attachedTo, attachedTo,
attachedToClass, attachedToClass,
hidden: false,
lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
}) })
await control.apply([createContextTx]) await control.apply([createContextTx])
@ -372,35 +369,19 @@ export async function pushInboxNotifications (
} }
docNotifyContextId = createContextTx.objectId docNotifyContextId = createContextTx.objectId
} else { } 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 docNotifyContextId = context._id
} }
if (!isHidden) { const notificationData = {
const notificationData = { user: account._id,
user: account._id, isViewed: false,
isViewed: false, docNotifyContext: docNotifyContextId,
docNotifyContext: docNotifyContextId, ...data
...data
}
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
res.push(notificationTx)
return notificationTx
} }
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
res.push(notificationTx)
return notificationTx
} }
async function activityInboxNotificationToText ( async function activityInboxNotificationToText (
@ -762,6 +743,46 @@ export async function getNotificationTxes (
return res return res
} }
async function updateContextsTimestamp (
contexts: DocNotifyContext[],
timestamp: Timestamp,
control: TriggerControl,
modifiedBy: Ref<Account>
): Promise<void> {
if (contexts.length === 0) return
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, {
_id: { $in: contexts.map((it) => it.user as Ref<PersonAccount>) }
})
const res: Tx[] = []
for (const context of contexts) {
const account = accounts.find(({ _id }) => _id === context.user)
const isViewed =
context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
lastUpdateTimestamp: timestamp,
...(isViewed && modifiedBy === context.user
? {
lastViewedTimestamp: 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 ( export async function createCollabDocInfo (
collaborators: Ref<PersonAccount>[], collaborators: Ref<PersonAccount>[],
control: TriggerControl, control: TriggerControl,
@ -778,6 +799,10 @@ export async function createCollabDocInfo (
return res return res
} }
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: object._id })
await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy)
const docMessages = activityMessages.filter((message) => message.attachedTo === object._id) const docMessages = activityMessages.filter((message) => message.attachedTo === object._id)
if (docMessages.length === 0) { if (docMessages.length === 0) {
return res return res
@ -797,10 +822,6 @@ export async function createCollabDocInfo (
return res return res
} }
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, {
attachedTo: object._id
})
const usersInfo = await getUsersInfo([...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control) const usersInfo = await getUsersInfo([...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control)
const sender = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? { const sender = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? {
_id: originTx.modifiedBy _id: originTx.modifiedBy
@ -1467,12 +1488,65 @@ export async function getCollaborators (
} }
} }
async function OnDocRemove (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { async function OnActivityMessageRemove (message: ActivityMessage, control: TriggerControl): Promise<Tx[]> {
const etx = TxProcessor.extractTx(tx) if (control.removedMap.has(message.attachedTo)) {
return []
}
if (etx._class !== core.class.TxRemoveDoc) return [] const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: message.attachedTo })
if (contexts.length === 0) return []
return await removeCollaboratorDoc(etx as TxRemoveDoc<Doc>, control) const isLastUpdate = contexts.some((context) => {
const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context
return lastUpdateTimestamp === message.createdOn && lastViewedTimestamp < lastUpdateTimestamp
})
if (!isLastUpdate) return []
const lastMessage = (
await control.findAll(
activity.class.ActivityMessage,
{ attachedTo: message.attachedTo, space: message.space },
{ sort: { createdOn: SortingOrder.Descending }, limit: 1 }
)
)[0]
if (lastMessage === undefined) return []
const res: Tx[] = []
for (const context of contexts) {
if (context.lastUpdateTimestamp === message.createdOn) {
const tx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
lastUpdateTimestamp: lastMessage.createdOn ?? lastMessage.modifiedOn
})
res.push(tx)
}
}
return res
}
async function OnDocRemove (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const tx = TxProcessor.extractTx(originTx) as TxRemoveDoc<Doc>
if (tx._class !== core.class.TxRemoveDoc) return []
const res: Tx[] = []
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined
if (message !== undefined) {
const txes = await OnActivityMessageRemove(message, control)
res.push(...txes)
}
}
const txes = await removeCollaboratorDoc(tx, control)
res.push(...txes)
return res
} }
export * from './types' export * from './types'

View File

@ -90,8 +90,7 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
// ) // )
res.push( res.push(
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
lastUpdateTimestamp: tx.modifiedOn, lastUpdateTimestamp: tx.modifiedOn
hidden: false
}) })
) )
} }
@ -101,7 +100,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
user: tx.modifiedBy, user: tx.modifiedBy,
attachedTo: channel._id, attachedTo: channel._id,
attachedToClass: channel._class, attachedToClass: channel._class,
hidden: false,
lastUpdateTimestamp: tx.modifiedOn lastUpdateTimestamp: tx.modifiedOn
// TODO: push inbox notifications // TODO: push inbox notifications
// txes: [ // txes: [

View File

@ -14,7 +14,6 @@ export async function createNotification (
const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, forDoc.space, { const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, forDoc.space, {
attachedTo: forDoc._id, attachedTo: forDoc._id,
attachedToClass: forDoc._class, attachedToClass: forDoc._class,
hidden: false,
user: data.user, user: data.user,
isPinned: false isPinned: false
}) })
@ -29,9 +28,6 @@ export async function createNotification (
props: data.props props: data.props
}) })
if (existing !== undefined) { if (existing !== undefined) {
await client.update(docNotifyContext as DocNotifyContext, {
lastUpdateTimestamp: Date.now()
})
await client.update(existing, { await client.update(existing, {
isViewed: false isViewed: false
}) })