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 12:53:24 +07:00
committed by GitHub
parent 1340ca78a9
commit 416eb9942e
28 changed files with 743 additions and 346 deletions
+205 -7
View File
@@ -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,
+3 -1
View File
@@ -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>,
+1 -3
View File
@@ -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: [