mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 03:54:57 +02: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:
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user