// // Copyright © 2023 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 Channel, type ChatMessage, chunterId, type ChunterSpace, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter' import contact, { type Employee, type PersonAccount, getName, type Person } from '@hcengineering/contact' import { employeeByIdStore, PersonIcon } from '@hcengineering/contact-resources' import { type Client, type Doc, getCurrentAccount, type IdMap, type Ref, type Space, type Class, type Timestamp, type Account, generateId } from '@hcengineering/core' import { getClient } from '@hcengineering/presentation' import { type AnySvelteComponent, getCurrentResolvedLocation, getLocation, type Location, navigate } from '@hcengineering/ui' import { workbenchId } from '@hcengineering/workbench' import { type Asset, getResource, translate } from '@hcengineering/platform' import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources' import activity, { type ActivityMessage, type ActivityMessagesFilter, type DisplayActivityMessage, type DisplayDocUpdateMessage, type DocUpdateMessage } from '@hcengineering/activity' import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import notification, { type DocNotifyContext, notificationId } from '@hcengineering/notification' import { get, type Unsubscriber } from 'svelte/store' import chunter from './plugin' import DirectIcon from './components/DirectIcon.svelte' import ChannelIcon from './components/ChannelIcon.svelte' import { chatSpecials } from './components/chat/utils' export async function getDmName (client: Client, space?: Space): Promise { if (space === undefined) { return '' } const employeeAccounts: PersonAccount[] = await getDmAccounts(client, space) return await buildDmName(client, employeeAccounts) } export async function buildDmName (client: Client, employeeAccounts: PersonAccount[]): Promise { if (employeeAccounts.length === 0) { return '' } let unsub: Unsubscriber | undefined const promise = new Promise>((resolve) => { unsub = employeeByIdStore.subscribe((p) => { if (p.size !== 0) { resolve(p) } }) }) const map = await promise unsub?.() const names: string[] = [] const processedPersons: Array> = [] for (const acc of employeeAccounts) { if (processedPersons.includes(acc.person)) { continue } const employee = map.get(acc.person as unknown as Ref) if (employee !== undefined) { names.push(getName(client.getHierarchy(), employee)) processedPersons.push(acc.person) } } return names.join(', ') } export async function dmIdentifierProvider (): Promise { return await translate(chunter.string.Direct, {}) } export async function canDeleteMessage (doc?: ChatMessage): Promise { if (doc === undefined) { return false } const me = getCurrentAccount() return doc.createdBy === me._id } export function canReplyToThread (doc?: ActivityMessage): boolean { if (doc === undefined) { return false } if (doc._class === chunter.class.ThreadMessage) { return false } if (doc._class === activity.class.DocUpdateMessage) { return (doc as DocUpdateMessage).objectClass !== activity.class.Reaction } return true } export async function canCopyMessageLink (doc?: ActivityMessage | ActivityMessage[]): Promise { const message = Array.isArray(doc) ? doc[0] : doc if (message === undefined) { return false } if (message._class === activity.class.DocUpdateMessage) { return (message as DocUpdateMessage).objectClass !== activity.class.Reaction } return true } async function getDmAccounts (client: Client, space?: Space): Promise { if (space === undefined) { return [] } const myAccId = getCurrentAccount()._id const employeeAccounts: PersonAccount[] = await client.findAll(contact.class.PersonAccount, { _id: { $in: (space.members ?? []) as Array> } }) return employeeAccounts.filter((p) => p._id !== myAccId) } export async function getDmPersons (client: Client, space: Space): Promise { const personAccounts: PersonAccount[] = await getDmAccounts(client, space) const persons: Person[] = [] const personRefs = new Set(personAccounts.map(({ person }) => person)) for (const personRef of personRefs) { const person = await client.findOne(contact.class.Person, { _id: personRef }) if (person !== undefined) { persons.push(person) } } return persons } export async function DirectTitleProvider (client: Client, id: Ref): Promise { const direct = await client.findOne(chunter.class.DirectMessage, { _id: id }) if (direct === undefined) { return '' } return await getDmName(client, direct) } export async function ChannelTitleProvider (client: Client, id: Ref): Promise { const channel = await client.findOne(chunter.class.Channel, { _id: id }) if (channel === undefined) { return '' } return channel.name } export async function openMessageFromSpecial (message?: ActivityMessage): Promise { if (message === undefined) { return } const inboxClient = InboxNotificationsClientImpl.getClient() const loc = getCurrentResolvedLocation() if (message._class === chunter.class.ThreadMessage) { const threadMessage = message as ThreadMessage loc.path[4] = threadMessage.attachedTo } else { const context = get(inboxClient.docNotifyContextByDoc).get(message.attachedTo) if (context === undefined) { return } loc.path[4] = context._id } loc.query = { ...loc.query, message: message._id } navigate(loc) } export function navigateToSpecial (specialId: string): void { const loc = getLocation() loc.path[2] = chunterId loc.path[3] = specialId navigate(loc) } export enum SearchType { Messages, Channels, Files, Contacts } export async function getMessageLink (message: ActivityMessage): Promise { const inboxClient = InboxNotificationsClientImpl.getClient() const location = getCurrentResolvedLocation() let context: DocNotifyContext | undefined let threadParent: string = '' if (message._class === chunter.class.ThreadMessage) { const threadMessage = message as ThreadMessage threadParent = `/${threadMessage.attachedTo}` context = get(inboxClient.docNotifyContextByDoc).get(threadMessage.objectId) } else { context = get(inboxClient.docNotifyContextByDoc).get(message.attachedTo) } if (context === undefined) { return '' } return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}/${context._id}${threadParent}?message=${message._id}` } export async function getTitle (doc: Doc): Promise { const client = getClient() const hierarchy = client.getHierarchy() let clazz = hierarchy.getClass(doc._class) let label = clazz.shortLabel while (label === undefined && clazz.extends !== undefined) { clazz = hierarchy.getClass(clazz.extends) label = clazz.shortLabel } label = label ?? doc._class return `${label}-${doc._id}` } export async function chunterSpaceLinkFragmentProvider (doc: ChunterSpace): Promise { const inboxClient = InboxNotificationsClientImpl.getClient() const context = get(inboxClient.docNotifyContextByDoc).get(doc._id) const loc = getCurrentResolvedLocation() if (context === undefined) { return loc } loc.path.length = 2 loc.fragment = undefined loc.query = undefined loc.path[2] = chunterId loc.path[3] = context._id return loc } export function getChannelIcon (_class: Ref>): Asset | AnySvelteComponent | undefined { const client = getClient() const hierarchy = client.getHierarchy() if (_class === chunter.class.Channel) { return ChannelIcon } if (_class === chunter.class.DirectMessage) { return DirectIcon } if (hierarchy.isDerived(_class, contact.class.Person)) { return PersonIcon } return classIcon(client, _class) } export async function getChannelName ( _id: Ref, _class: Ref>, object?: Doc ): Promise { const client = getClient() if (client.getHierarchy().isDerived(_class, chunter.class.ChunterSpace)) { return await getDocTitle(client, _id, _class, object) } return await getDocLinkTitle(client, _id, _class, object) } export function getUnreadThreadsCount (): number { const notificationClient = InboxNotificationsClientImpl.getClient() const threadIds = get(notificationClient.activityInboxNotifications) .filter(({ attachedToClass, isViewed }) => attachedToClass === chunter.class.ThreadMessage && !isViewed) .map(({ $lookup }) => $lookup?.attachedTo?.attachedTo) .filter((_id) => _id !== undefined) return new Set(threadIds).size } export function getClosestDate (selectedDate: Timestamp, dates: Timestamp[]): Timestamp | undefined { if (dates.length === 0) { return } let closestDate: Timestamp | undefined = dates[dates.length - 1] const reversedDates = [...dates].reverse() for (const date of reversedDates) { if (date < selectedDate) { break } else if (date - selectedDate < closestDate - selectedDate) { closestDate = date } } return closestDate } export async function filterChatMessages ( messages: DisplayActivityMessage[], filters: ActivityMessagesFilter[], objectClass: Ref>, selectedIds: Array> ): Promise { if (selectedIds.length === 0 || selectedIds.includes(activity.ids.AllFilter)) { return messages } const selectedFilters = filters.filter(({ _id }) => selectedIds.includes(_id)) if (selectedFilters.length === 0) { return messages } const filtersFns: Array<(message: ActivityMessage, _class?: Ref) => boolean> = [] for (const filter of selectedFilters) { const filterFn = await getResource(filter.filter) filtersFns.push(filterFn) } return messages.filter((message) => filtersFns.some((filterFn) => filterFn(message, objectClass))) } export function buildThreadLink (loc: Location, contextId: Ref, _id: Ref): Location { const specials = chatSpecials.map(({ id }) => id) const isSameContext = loc.path[3] === contextId if (!isSameContext) { loc.query = { message: _id } } if (loc.path[2] === chunterId && specials.includes(loc.path[3])) { loc.path[4] = _id return loc } if (loc.path[2] !== notificationId) { loc.path[2] = chunterId } loc.path[3] = contextId loc.path[4] = _id loc.fragment = undefined return loc } export async function getThreadLink (doc: ThreadMessage): Promise { const loc = getCurrentResolvedLocation() const client = getClient() const inboxClient = InboxNotificationsClientImpl.getClient() let contextId: Ref | undefined = get(inboxClient.docNotifyContextByDoc).get(doc.objectId)?._id if (contextId === undefined) { contextId = await client.createDoc(notification.class.DocNotifyContext, doc.space, { attachedTo: doc.attachedTo, attachedToClass: doc.attachedToClass, user: getCurrentAccount()._id, hidden: false, lastViewedTimestamp: Date.now() }) } if (contextId === undefined) { return loc } return buildThreadLink(loc, contextId, doc.attachedTo) } export async function joinChannel (channel: Channel, value: Ref | Array>): Promise { const client = getClient() if (Array.isArray(value)) { if (value.length > 0) { await client.update(channel, { $push: { members: { $each: value, $position: 0 } } }) } } else { await client.update(channel, { $push: { members: value } }) } } export async function leaveChannel (channel: Channel, value: Ref | Array>): Promise { const client = getClient() if (Array.isArray(value)) { if (value.length > 0) { await client.update(channel, { $pull: { members: { $in: value } } }) } } else { await client.update(channel, { $pull: { members: value } }) } } export async function readChannelMessages ( messages: DisplayActivityMessage[], context: DocNotifyContext | undefined ): Promise { if (messages.length === 0) { return } const inboxClient = InboxNotificationsClientImpl.getClient() const client = getClient() const allIds = messages .map((message) => { const combined = message._class === activity.class.DocUpdateMessage ? (message as DisplayDocUpdateMessage)?.combinedMessagesIds : undefined return [message._id, ...(combined ?? [])] }) .flat() const ops = getClient().apply(generateId()) void inboxClient.readMessages(ops, allIds).then(() => { void ops.commit() }) if (context === undefined) { return } const lastTimestamp = messages[messages.length - 1].createdOn ?? 0 if ((context.lastViewedTimestamp ?? 0) < lastTimestamp) { void client.update(context, { lastViewedTimestamp: lastTimestamp }) } }