diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index f8405261a8..f570e4cd0a 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -74,7 +74,10 @@ export { chunterOperation } from './migration' export const DOMAIN_CHUNTER = 'chunter' as Domain @Model(chunter.class.ChunterSpace, core.class.Space) -export class TChunterSpace extends TSpace implements ChunterSpace {} +export class TChunterSpace extends TSpace implements ChunterSpace { + @Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages) + messages?: number +} @Model(chunter.class.Channel, chunter.class.ChunterSpace) @UX(chunter.string.Channel, chunter.icon.Hashtag, undefined, undefined, undefined, chunter.string.Channels) diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index 7faa157a29..5ab8d328c6 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { chunterId, type ThreadMessage } from '@hcengineering/chunter' +import { chunterId, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter' import core, { type Account, TxOperations, @@ -33,12 +33,13 @@ import { } from '@hcengineering/model' import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity' import notification from '@hcengineering/notification' -import contactPlugin, { type PersonAccount } from '@hcengineering/contact' -import { DOMAIN_NOTIFICATION } from '@hcengineering/model-notification' +import contactPlugin, { type Person, type PersonAccount } from '@hcengineering/contact' +import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification' +import { type DocUpdateMessage } from '@hcengineering/activity' +import { DOMAIN_SPACE } from '@hcengineering/model-core' import chunter from './plugin' import { DOMAIN_CHUNTER } from './index' -import { type DocUpdateMessage } from '@hcengineering/activity' export const DOMAIN_COMMENT = 'comment' as Domain @@ -239,6 +240,53 @@ async function removeWrongActivity (client: MigrationClient): Promise { }) } +async function removeDuplicatedDirects (client: MigrationClient): Promise { + const directs = await client.find(DOMAIN_SPACE, { _class: chunter.class.DirectMessage }) + const personAccounts = await client.model.findAll(contactPlugin.class.PersonAccount, {}) + const personByAccount = new Map(personAccounts.map((it) => [it._id, it.person])) + + const accountsToPersons = (members: Ref[]): Ref[] => { + const personsSet = new Set( + members + .map((it) => personByAccount.get(it as Ref)) + .filter((it): it is Ref => it !== undefined) + ) + return Array.from(personsSet) + } + + const map: Map = new Map() + const toRemove: Ref[] = [] + + for (const direct of directs) { + const persons = accountsToPersons(direct.members) + + if (persons.length === 0) { + toRemove.push(direct._id) + continue + } + + const key = persons.sort().join(',') + + if (!map.has(key)) { + map.set(key, [direct]) + } else { + map.get(key)?.push(direct) + } + } + + for (const [, directs] of map) { + if (directs.length === 1) continue + const toSave = directs.reduce((acc, it) => ((it.messages ?? 0) > (acc.messages ?? 0) ? it : acc), directs[0]) + const rest = directs.filter((it) => it._id !== toSave._id) + toRemove.push(...rest.map((it) => it._id)) + } + + await client.deleteMany(DOMAIN_SPACE, { _id: { $in: toRemove } }) + await client.deleteMany(DOMAIN_ACTIVITY, { attachedTo: { $in: toRemove } }) + await client.deleteMany(DOMAIN_ACTIVITY, { objectId: { $in: toRemove } }) + await client.deleteMany(DOMAIN_DOC_NOTIFY, { objectId: { $in: toRemove } }) +} + export const chunterOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, chunterId, [ @@ -283,6 +331,12 @@ export const chunterOperation: MigrateOperation = { func: async (client) => { await removeWrongActivity(client) } + }, + { + state: 'remove-duplicated-directs-v1', + func: async (client) => { + await removeDuplicatedDirects(client) + } } ]) }, diff --git a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte index 08bcb2400b..e777e90381 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte @@ -17,9 +17,9 @@ import { deepEqual } from 'fast-equals' import { DirectMessage } from '@hcengineering/chunter' - import contact, { Employee, PersonAccount } from '@hcengineering/contact' + import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import core, { getCurrentAccount, Ref } from '@hcengineering/core' - import { SelectUsersPopup } from '@hcengineering/contact-resources' + import { personIdByAccountId, SelectUsersPopup } from '@hcengineering/contact-resources' import notification from '@hcengineering/notification' import presentation, { createQuery, getClient } from '@hcengineering/presentation' import { Modal, showPopup } from '@hcengineering/ui' @@ -55,15 +55,31 @@ const accIds = [myAcc._id, ...employeeAccounts.filter(({ _id }) => _id !== myAcc._id).map(({ _id }) => _id)].sort() const existingDms = await client.findAll(chunter.class.DirectMessage, {}) + const newDirectPersons = Array.from(new Set([...employeeIds, myAcc.person])).sort() let direct: DirectMessage | undefined + for (const dm of existingDms) { - if (deepEqual(dm.members.sort(), accIds)) { + const existDirectPersons = Array.from( + new Set(dm.members.map((id) => $personIdByAccountId.get(id as Ref))) + ) + .filter((person): person is Ref => person !== undefined) + .sort() + if (deepEqual(existDirectPersons, newDirectPersons)) { direct = dm break } } + const existingMembers = direct?.members + const missingAccounts = existingMembers !== undefined ? accIds.filter((id) => !existingMembers.includes(id)) : [] + + if (direct !== undefined && missingAccounts.length > 0) { + await client.updateDoc(chunter.class.DirectMessage, direct.space, direct._id, { + members: [...direct.members, ...missingAccounts] + }) + } + const dmId = direct?._id ?? (await client.createDoc(chunter.class.DirectMessage, core.space.Space, { @@ -75,12 +91,13 @@ })) const context = await client.findOne(notification.class.DocNotifyContext, { - person: myAcc.person, + user: myAcc._id, objectId: dmId, objectClass: chunter.class.DirectMessage }) if (context !== undefined) { + dispatch('close') openChannel(dmId, chunter.class.DirectMessage) return @@ -97,6 +114,7 @@ }) openChannel(dmId, chunter.class.DirectMessage) + dispatch('close') } function handleCancel (): void { diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 1c87455e66..b027c3f459 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -25,7 +25,9 @@ import { Person, ChannelProvider as SocialChannelProvider } from '@hcengineering /** * @public */ -export interface ChunterSpace extends Space {} +export interface ChunterSpace extends Space { + messages?: number +} /** * @public diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index 096de69a57..ad63124e20 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -16,38 +16,38 @@ import { AvatarType, + type Channel, + type ChannelProvider, + type Contact, contactId, + type Employee, formatName, getFirstName, getLastName, getName, - type Channel, - type ChannelProvider, - type Contact, - type Employee, type Person, type PersonAccount } from '@hcengineering/contact' import core, { - getCurrentAccount, - toIdMap, type Account, + AggregateValue, + AggregateValueData, type Class, type Client, type Doc, + type DocumentQuery, + getCurrentAccount, + type Hierarchy, type IdMap, + matchQuery, type ObjQueryType, type Ref, + type Space, type Timestamp, + toIdMap, type TxOperations, type UserStatus, - type WithLookup, - AggregateValue, - type Space, - type Hierarchy, - type DocumentQuery, - AggregateValueData, - matchQuery + type WithLookup } from '@hcengineering/core' import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform' @@ -61,8 +61,8 @@ import { type ResolvedLocation, type TabItem } from '@hcengineering/ui' -import view, { type GrouppingManager, type Filter } from '@hcengineering/view' -import { FilterQuery, accessDeniedStore } from '@hcengineering/view-resources' +import view, { type Filter, type GrouppingManager } from '@hcengineering/view' +import { accessDeniedStore, FilterQuery } from '@hcengineering/view-resources' import { derived, get, writable } from 'svelte/store' import contact from './plugin' @@ -305,6 +305,10 @@ export const channelProviders = writable([]) export const personAccountPersonByIdStore = writable>>(new Map()) +export const personIdByAccountId = derived(personAccountByIdStore, (vals) => { + return new Map, Ref>(Array.from(vals.values()).map((it) => [it._id, it.person])) +}) + export const statusByUserStore = writable, UserStatus>>(new Map()) export const personByIdStore = derived([personAccountPersonByIdStore, employeeByIdStore], (vals) => {