mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-7016: Hide channels without any activity long time (#6176)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
1340ca78a9
commit
416eb9942e
249
models/chunter/src/actions.ts
Normal file
249
models/chunter/src/actions.ts
Normal 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] }
|
||||
}
|
||||
})
|
||||
}
|
@ -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<Person>
|
||||
hidden!: Ref<DocNotifyContext>[]
|
||||
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
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -203,10 +203,6 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext {
|
||||
@Index(IndexKind.Indexed)
|
||||
attachedToClass!: Ref<Class<Doc>>
|
||||
|
||||
@Prop(TypeBoolean(), core.string.Archived)
|
||||
@Index(IndexKind.Indexed)
|
||||
hidden!: boolean
|
||||
|
||||
@Prop(TypeDate(), core.string.Date)
|
||||
lastViewedTimestamp?: Timestamp
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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<Timestamp[]>([])
|
||||
public newTimestampStore = writable<Timestamp | undefined>(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)
|
||||
|
@ -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<Class<Doc>>
|
||||
export let objectId: Ref<Doc>
|
||||
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 selectedFilters: Ref<ActivityMessagesFilter>[] = []
|
||||
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
|
||||
</script>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -55,7 +55,7 @@
|
||||
notification.class.DocNotifyContext,
|
||||
{
|
||||
...model.query,
|
||||
hidden: false,
|
||||
[`${chunter.mixin.ChannelInfo}.hidden`]: { $ne: true },
|
||||
user: getCurrentAccount()._id
|
||||
},
|
||||
(res: DocNotifyContext[]) => {
|
||||
|
@ -128,6 +128,7 @@
|
||||
{count}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
secondaryNotifyMarker={(context?.lastViewedTimestamp ?? 0) < (context?.lastUpdateTimestamp ?? 0)}
|
||||
{actions}
|
||||
{type}
|
||||
on:click={() => {
|
||||
|
@ -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 @@
|
||||
<div class="antiHSpacer" />
|
||||
<NotifyMarker {count} />
|
||||
<div class="antiHSpacer" />
|
||||
{:else if secondaryNotifyMarker}
|
||||
<div class="antiHSpacer" />
|
||||
<NotifyMarker count={0} kind="secondary" size="x-small" />
|
||||
<div class="antiHSpacer" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</NavItem>
|
||||
|
@ -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<void> {
|
||||
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()
|
||||
|
@ -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<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()
|
||||
|
||||
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 } } })
|
||||
}
|
||||
} 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<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 {
|
||||
|
@ -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<Person>
|
||||
hidden: Ref<DocNotifyContext>[]
|
||||
timestamp: Timestamp
|
||||
}
|
||||
|
||||
export interface ChannelInfo extends DocNotifyContext {
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -110,10 +121,12 @@ export default plugin(chunterId, {
|
||||
Channel: '' as Ref<Class<Channel>>,
|
||||
DirectMessage: '' as Ref<Class<DirectMessage>>,
|
||||
ChatMessage: '' as Ref<Class<ChatMessage>>,
|
||||
ChatMessageViewlet: '' as Ref<Class<ChatMessageViewlet>>
|
||||
ChatMessageViewlet: '' as Ref<Class<ChatMessageViewlet>>,
|
||||
ChatInfo: '' as Ref<Class<ChatInfo>>
|
||||
},
|
||||
mixin: {
|
||||
ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>>
|
||||
ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>>,
|
||||
ChannelInfo: '' as Ref<Mixin<ChannelInfo>>
|
||||
},
|
||||
string: {
|
||||
Reactions: '' as IntlString,
|
||||
|
@ -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)
|
||||
</script>
|
||||
|
||||
{#if hasNotification}
|
||||
|
@ -14,13 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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
|
||||
</script>
|
||||
|
||||
{#if count > 0}
|
||||
<div class="notifyMarker {size}">
|
||||
<div class="notifyMarker {size} {kind}">
|
||||
{#if count > maxNumber}
|
||||
{maxNumber}+
|
||||
{:else}
|
||||
@ -29,6 +30,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if count === 0 && kind === 'secondary'}
|
||||
<div class="notifyMarker {size} {kind}" />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.notifyMarker {
|
||||
display: flex;
|
||||
@ -36,10 +41,22 @@
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-color: var(--global-higlight-Color);
|
||||
color: var(--global-on-accent-TextColor);
|
||||
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 {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
@ -73,7 +73,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
||||
return inboxNotifications.reduce((result, notification) => {
|
||||
const notifyContext = notifyContexts.find(({ _id }) => _id === notification.docNotifyContext)
|
||||
|
||||
if (notifyContext === undefined || notifyContext.hidden) {
|
||||
if (notifyContext === undefined) {
|
||||
return result
|
||||
}
|
||||
|
||||
@ -207,13 +207,6 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await client.createDoc(notification.class.DocNotifyContext, doc.space, {
|
||||
attachedTo: _id,
|
||||
attachedToClass: _class,
|
||||
user: getCurrentAccount()._id,
|
||||
hidden: true
|
||||
})
|
||||
}
|
||||
|
||||
async readNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {
|
||||
|
@ -87,16 +87,10 @@ export function loadNotificationSettings (): void {
|
||||
loadNotificationSettings()
|
||||
|
||||
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
||||
if (docNotifyContext.hidden) {
|
||||
return false
|
||||
}
|
||||
return docNotifyContext.isPinned !== true
|
||||
}
|
||||
|
||||
export async function hasDocNotifyContextUnpinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
||||
if (docNotifyContext.hidden) {
|
||||
return false
|
||||
}
|
||||
return docNotifyContext.isPinned === true
|
||||
}
|
||||
|
||||
|
@ -276,7 +276,6 @@ export interface DocNotifyContext extends Doc {
|
||||
attachedTo: Ref<Doc>
|
||||
attachedToClass: Ref<Class<Doc>>
|
||||
|
||||
hidden: boolean
|
||||
isPinned?: boolean
|
||||
lastViewedTimestamp?: Timestamp
|
||||
lastUpdateTimestamp?: Timestamp
|
||||
|
@ -91,7 +91,7 @@
|
||||
): boolean {
|
||||
const context = notifyContextByDoc.get(space._id)
|
||||
|
||||
if (context === undefined || context.hidden) {
|
||||
if (context === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
||||
): boolean {
|
||||
const notifyContext = docUpdates.get(space._id)
|
||||
if (notifyContext === undefined) return false
|
||||
return !notifyContext.hidden && !!inboxNotificationsByContext.get(notifyContext._id)?.length
|
||||
return !!inboxNotificationsByContext.get(notifyContext._id)?.length
|
||||
}
|
||||
$: visibleSpace = spaces.find((space) => currentSpace === space._id)
|
||||
</script>
|
||||
|
@ -14,8 +14,15 @@
|
||||
//
|
||||
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import chunter, {
|
||||
Channel,
|
||||
ChannelInfo,
|
||||
ChatMessage,
|
||||
chunterId,
|
||||
ChunterSpace,
|
||||
ThreadMessage
|
||||
} from '@hcengineering/chunter'
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
@ -27,6 +34,7 @@ import core, {
|
||||
FindResult,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
Timestamp,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
@ -34,9 +42,10 @@ import core, {
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc
|
||||
TxUpdateDoc,
|
||||
UserStatus
|
||||
} from '@hcengineering/core'
|
||||
import notification, { Collaborators, NotificationContent } from '@hcengineering/notification'
|
||||
import notification, { Collaborators, DocNotifyContext, NotificationContent } from '@hcengineering/notification'
|
||||
import { getMetadata, IntlString, translate } from '@hcengineering/platform'
|
||||
import serverCore, { TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
@ -50,6 +59,9 @@ import { workbenchId } from '@hcengineering/workbench'
|
||||
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
|
||||
import { encodeObjectURI } from '@hcengineering/view'
|
||||
|
||||
const updateChatInfoDelay = 12 * 60 * 60 * 1000 // 12 hours
|
||||
const hideChannelDelay = 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -411,14 +423,12 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
|
||||
attachedTo: tx.objectId,
|
||||
attachedToClass: tx.objectClass,
|
||||
user: addedMember,
|
||||
hidden: false,
|
||||
lastViewedTimestamp: tx.modifiedOn
|
||||
})
|
||||
|
||||
await control.apply([createTx])
|
||||
} else {
|
||||
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||
hidden: false,
|
||||
lastViewedTimestamp: tx.modifiedOn
|
||||
})
|
||||
|
||||
@ -455,13 +465,201 @@ async function OnCollaboratorsChanged (tx: TxMixin<Doc, Collaborators>, control:
|
||||
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
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
ChunterTrigger,
|
||||
OnChatMessageRemoved,
|
||||
OnChannelMembersChanged,
|
||||
ChatNotificationsHandler
|
||||
ChatNotificationsHandler,
|
||||
OnUserStatus,
|
||||
OnContextUpdate
|
||||
},
|
||||
function: {
|
||||
CommentRemove,
|
||||
|
@ -31,7 +31,9 @@ export default plugin(serverChunterId, {
|
||||
ChunterTrigger: '' as Resource<TriggerFunc>,
|
||||
OnChatMessageRemoved: '' as Resource<TriggerFunc>,
|
||||
OnChannelMembersChanged: '' as Resource<TriggerFunc>,
|
||||
ChatNotificationsHandler: '' as Resource<TriggerFunc>
|
||||
ChatNotificationsHandler: '' as Resource<TriggerFunc>,
|
||||
OnUserStatus: '' as Resource<TriggerFunc>,
|
||||
OnContextUpdate: '' as Resource<TriggerFunc>
|
||||
},
|
||||
function: {
|
||||
CommentRemove: '' as Resource<ObjectDDParticipantFunc>,
|
||||
|
@ -95,8 +95,7 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
|
||||
// )
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
|
||||
lastUpdateTimestamp: tx.modifiedOn,
|
||||
hidden: false
|
||||
lastUpdateTimestamp: tx.modifiedOn
|
||||
})
|
||||
)
|
||||
}
|
||||
@ -106,7 +105,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
|
||||
user: tx.modifiedBy,
|
||||
attachedTo: channel._id,
|
||||
attachedToClass: channel._class,
|
||||
hidden: false,
|
||||
lastUpdateTimestamp: tx.modifiedOn
|
||||
// TODO: push inbox notification
|
||||
// txes: [
|
||||
|
@ -39,6 +39,7 @@ import core, {
|
||||
MixinUpdate,
|
||||
Ref,
|
||||
RefTo,
|
||||
SortingOrder,
|
||||
Space,
|
||||
Timestamp,
|
||||
toIdMap,
|
||||
@ -349,9 +350,6 @@ export async function pushInboxNotifications (
|
||||
}
|
||||
const context = getDocNotifyContext(contexts, account._id, attachedTo, res)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
const isHidden = !!context?.hidden
|
||||
|
||||
let docNotifyContextId: Ref<DocNotifyContext>
|
||||
|
||||
if (context === undefined) {
|
||||
@ -359,7 +357,6 @@ export async function pushInboxNotifications (
|
||||
user: account._id,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
hidden: false,
|
||||
lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
|
||||
})
|
||||
await control.apply([createContextTx])
|
||||
@ -372,35 +369,19 @@ export async function pushInboxNotifications (
|
||||
}
|
||||
docNotifyContextId = createContextTx.objectId
|
||||
} else {
|
||||
if (shouldUpdateTimestamp && context.lastUpdateTimestamp !== modifiedOn) {
|
||||
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||
lastUpdateTimestamp: modifiedOn
|
||||
})
|
||||
await control.apply([updateTx])
|
||||
if (target.account?.email !== undefined) {
|
||||
control.operationContext.derived.targets['docNotifyContext' + updateTx._id] = (it) => {
|
||||
if (it._id === updateTx._id) {
|
||||
return [target.account?.email as string]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
docNotifyContextId = context._id
|
||||
}
|
||||
|
||||
if (!isHidden) {
|
||||
const notificationData = {
|
||||
user: account._id,
|
||||
isViewed: false,
|
||||
docNotifyContext: docNotifyContextId,
|
||||
...data
|
||||
}
|
||||
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
|
||||
res.push(notificationTx)
|
||||
|
||||
return notificationTx
|
||||
const notificationData = {
|
||||
user: account._id,
|
||||
isViewed: false,
|
||||
docNotifyContext: docNotifyContextId,
|
||||
...data
|
||||
}
|
||||
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
|
||||
res.push(notificationTx)
|
||||
|
||||
return notificationTx
|
||||
}
|
||||
|
||||
async function activityInboxNotificationToText (
|
||||
@ -762,6 +743,46 @@ export async function getNotificationTxes (
|
||||
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 (
|
||||
collaborators: Ref<PersonAccount>[],
|
||||
control: TriggerControl,
|
||||
@ -778,6 +799,10 @@ export async function createCollabDocInfo (
|
||||
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)
|
||||
if (docMessages.length === 0) {
|
||||
return res
|
||||
@ -797,10 +822,6 @@ export async function createCollabDocInfo (
|
||||
return res
|
||||
}
|
||||
|
||||
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, {
|
||||
attachedTo: object._id
|
||||
})
|
||||
|
||||
const usersInfo = await getUsersInfo([...Array.from(targets), originTx.modifiedBy as Ref<PersonAccount>], control)
|
||||
const sender = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? {
|
||||
_id: originTx.modifiedBy
|
||||
@ -1467,12 +1488,65 @@ export async function getCollaborators (
|
||||
}
|
||||
}
|
||||
|
||||
async function OnDocRemove (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const etx = TxProcessor.extractTx(tx)
|
||||
async function OnActivityMessageRemove (message: ActivityMessage, control: TriggerControl): Promise<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'
|
||||
|
@ -90,8 +90,7 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
|
||||
// )
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
|
||||
lastUpdateTimestamp: tx.modifiedOn,
|
||||
hidden: false
|
||||
lastUpdateTimestamp: tx.modifiedOn
|
||||
})
|
||||
)
|
||||
}
|
||||
@ -101,7 +100,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise
|
||||
user: tx.modifiedBy,
|
||||
attachedTo: channel._id,
|
||||
attachedToClass: channel._class,
|
||||
hidden: false,
|
||||
lastUpdateTimestamp: tx.modifiedOn
|
||||
// TODO: push inbox notifications
|
||||
// txes: [
|
||||
|
@ -14,7 +14,6 @@ export async function createNotification (
|
||||
const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, forDoc.space, {
|
||||
attachedTo: forDoc._id,
|
||||
attachedToClass: forDoc._class,
|
||||
hidden: false,
|
||||
user: data.user,
|
||||
isPinned: false
|
||||
})
|
||||
@ -29,9 +28,6 @@ export async function createNotification (
|
||||
props: data.props
|
||||
})
|
||||
if (existing !== undefined) {
|
||||
await client.update(docNotifyContext as DocNotifyContext, {
|
||||
lastUpdateTimestamp: Date.now()
|
||||
})
|
||||
await client.update(existing, {
|
||||
isViewed: false
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user