platform/plugins/chunter-resources/src/components/chat/utils.ts
Andrey Sobolev 4cce003364
UBERF-7959: Fix async issues (#6409)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2024-08-28 11:51:20 +05:00

428 lines
12 KiB
TypeScript

//
// 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 attachment, { type SavedAttachments } from '@hcengineering/attachment'
import { type DirectMessage } from '@hcengineering/chunter'
import contact, { type PersonAccount } from '@hcengineering/contact'
import core, {
type Account,
AccountRole,
generateId,
getCurrentAccount,
hasAccountRole,
type IdMap,
type Ref,
SortingOrder,
type UserStatus,
type WithLookup
} from '@hcengineering/core'
import notification, { type DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { type Action, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import workbench, { type SpecialNavModel } from '@hcengineering/workbench'
import { get, writable } from 'svelte/store'
import chunter from '../../plugin'
import { type ChatNavGroupModel, type ChatNavItemModel, type SortFnOptions } from './types'
const navigatorStateStorageKey = 'chunter.navigatorState'
interface NavigatorState {
collapsedSections: string[]
}
export const savedAttachmentsStore = writable<Array<WithLookup<SavedAttachments>>>([])
export const navigatorStateStore = writable<NavigatorState>(restoreNavigatorState())
function restoreNavigatorState (): NavigatorState {
const raw = localStorage.getItem(navigatorStateStorageKey)
if (raw == null) return { collapsedSections: [] }
try {
return JSON.parse(raw) as NavigatorState
} catch (e) {
return { collapsedSections: [] }
}
}
export function toggleSections (_id: string): void {
const navState = get(navigatorStateStore)
const result: NavigatorState = navState.collapsedSections.includes(_id)
? {
collapsedSections: navState.collapsedSections.filter((id) => id !== _id)
}
: { collapsedSections: [...navState.collapsedSections, _id] }
localStorage.setItem(navigatorStateStorageKey, JSON.stringify(result))
navigatorStateStore.set(result)
}
export const chatSpecials: SpecialNavModel[] = [
{
id: 'threads',
label: chunter.string.Threads,
icon: chunter.icon.Thread,
component: chunter.component.Threads,
position: 'top',
notificationsCountProvider: chunter.function.GetUnreadThreadsCount
},
{
id: 'saved',
label: chunter.string.Saved,
icon: chunter.icon.Bookmarks,
position: 'top',
component: chunter.component.SavedMessages
},
{
id: 'chunterBrowser',
label: chunter.string.ChunterBrowser,
icon: chunter.icon.ChunterBrowser,
component: chunter.component.ChunterBrowser,
position: 'top'
},
{
id: 'channels',
label: chunter.string.Channels,
icon: chunter.icon.ChannelBrowser,
component: workbench.component.SpecialView,
componentProps: {
_class: chunter.class.Channel,
icon: chunter.icon.ChannelBrowser,
label: chunter.string.Channels,
createLabel: chunter.string.CreateChannel,
createComponent: chunter.component.CreateChannel
},
position: 'top'
}
// TODO: Should be reworked or removed
// {
// id: 'archive',
// component: workbench.component.Archive,
// icon: view.icon.Archive,
// label: workbench.string.Archive,
// position: 'top',
// componentProps: {
// _class: notification.class.DocNotifyContext,
// config: [
// { key: '', label: chunter.string.ChannelName },
// { key: 'attachedToClass', label: view.string.Type },
// 'modifiedOn'
// ],
// baseMenuClass: notification.class.DocNotifyContext,
// query: {
// _class: notification.class.DocNotifyContext,
// hidden: true
// }
// },
// visibleIf: notification.function.HasHiddenDocNotifyContext
// }
]
export const chatNavGroupModels: ChatNavGroupModel[] = [
{
id: 'starred',
label: chunter.string.Starred,
sortFn: sortAlphabetically,
wrap: false,
getActionsFn: getPinnedActions,
isPinned: true
},
{
id: 'channels',
sortFn: sortAlphabetically,
wrap: true,
getActionsFn: getChannelsActions,
isPinned: false,
_class: chunter.class.Channel
},
{
id: 'direct',
sortFn: sortDirects,
wrap: true,
getActionsFn: getDirectActions,
isPinned: false,
_class: chunter.class.DirectMessage
},
{
id: 'activity',
sortFn: sortActivityChannels,
wrap: true,
getActionsFn: getActivityActions,
maxSectionItems: 5,
isPinned: false,
skipClasses: [chunter.class.DirectMessage, chunter.class.Channel, contact.class.Channel]
}
]
function sortAlphabetically (items: ChatNavItemModel[]): ChatNavItemModel[] {
return items.sort((i1, i2) => i1.title.localeCompare(i2.title))
}
function getDirectCompanion (
direct: DirectMessage,
me: PersonAccount,
personAccountById: IdMap<PersonAccount>
): Ref<Account> | undefined {
return direct.members.find((member) => personAccountById.get(member as Ref<PersonAccount>)?.person !== me.person)
}
function isOnline (account: Ref<Account> | undefined, userStatusByAccount: Map<Ref<Account>, UserStatus>): boolean {
if (account === undefined) {
return false
}
return userStatusByAccount.get(account)?.online ?? false
}
function isGroupChat (direct: DirectMessage, personAccountById: IdMap<PersonAccount>): boolean {
const persons = new Set(
direct.members
.map((member) => personAccountById.get(member as Ref<PersonAccount>)?.person)
.filter((it) => it !== undefined)
)
return persons.size > 2
}
function sortDirects (items: ChatNavItemModel[], option: SortFnOptions): ChatNavItemModel[] {
const { userStatusByAccount, personAccountById } = option
const me = getCurrentAccount() as PersonAccount
return items.sort((i1, i2) => {
const direct1 = i1.object as DirectMessage
const direct2 = i2.object as DirectMessage
const isGroupChat1 = isGroupChat(direct1, personAccountById)
const isGroupChat2 = isGroupChat(direct2, personAccountById)
if (isGroupChat1 && isGroupChat2) {
return i1.title.localeCompare(i2.title)
}
if (isGroupChat1 && !isGroupChat2) {
const isOnline2 = isOnline(getDirectCompanion(direct2, me, personAccountById), userStatusByAccount)
return isOnline2 ? 1 : -1
}
if (!isGroupChat1 && isGroupChat2) {
const isOnline1 = isOnline(getDirectCompanion(direct1, me, personAccountById), userStatusByAccount)
return isOnline1 ? -1 : 1
}
const account1 = getDirectCompanion(direct1, me, personAccountById)
const account2 = getDirectCompanion(direct2, me, personAccountById)
if (account1 === undefined) {
return 1
}
if (account2 === undefined) {
return -1
}
const isOnline1 = isOnline(account1, userStatusByAccount)
const isOnline2 = isOnline(account2, userStatusByAccount)
if (isOnline1 === isOnline2) {
return i1.title.localeCompare(i2.title)
}
if (isOnline1 && !isOnline2) {
return -1
}
return 1
})
}
function sortActivityChannels (items: ChatNavItemModel[], option: SortFnOptions): ChatNavItemModel[] {
const { contexts } = option
const contextByDoc = new Map(contexts.map((context) => [context.objectId, context]))
return items.sort((i1, i2) => {
const context1 = contextByDoc.get(i1.id)
const context2 = contextByDoc.get(i2.id)
if (context1 === undefined || context2 === undefined) {
return 1
}
const hasNewMessages1 = (context1.lastUpdateTimestamp ?? 0) > (context1.lastViewedTimestamp ?? 0)
const hasNewMessages2 = (context2.lastUpdateTimestamp ?? 0) > (context2.lastViewedTimestamp ?? 0)
if (hasNewMessages1 && hasNewMessages2) {
return (context2.lastUpdateTimestamp ?? 0) - (context1.lastUpdateTimestamp ?? 0)
}
if (hasNewMessages1 && !hasNewMessages2) {
return -1
}
if (hasNewMessages2 && !hasNewMessages1) {
return 1
}
return (context2.lastUpdateTimestamp ?? 0) - (context1.lastUpdateTimestamp ?? 0)
})
}
function getPinnedActions (contexts: DocNotifyContext[]): Action[] {
return [
{
icon: view.icon.Delete,
label: chunter.string.DeleteStarred,
action: async () => {
await unpinAllChannels(contexts)
}
}
]
}
async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
const ops = getClient().apply(generateId(), 'unpinAllChannels')
try {
for (const context of contexts) {
await ops.update(context, { isPinned: false })
}
} finally {
await ops.commit()
}
}
function getChannelsActions (): Action[] {
return hasAccountRole(getCurrentAccount(), AccountRole.User)
? [
{
icon: chunter.icon.Hashtag,
label: chunter.string.CreateChannel,
action: async (): Promise<void> => {
showPopup(chunter.component.CreateChannel, {}, 'top')
}
}
]
: []
}
function getDirectActions (): Action[] {
return hasAccountRole(getCurrentAccount(), AccountRole.User)
? [
{
label: chunter.string.NewDirectChat,
icon: chunter.icon.Thread,
action: async (): Promise<void> => {
showPopup(chunter.component.CreateDirectChat, {}, 'top')
}
}
]
: []
}
function getActivityActions (contexts: DocNotifyContext[]): Action[] {
return [
{
icon: view.icon.Eye,
label: notification.string.MarkReadAll,
action: async () => {
await readActivityChannels(contexts)
}
},
{
icon: view.icon.EyeCrossed,
label: view.string.Hide,
action: async () => {
archiveActivityChannels(contexts)
}
}
]
}
function archiveActivityChannels (contexts: DocNotifyContext[]): void {
showPopup(
MessageBox,
{
label: chunter.string.ArchiveActivityConfirmationTitle,
message: chunter.string.ArchiveActivityConfirmationMessage,
action: async () => {
await removeActivityChannels(contexts)
}
},
'top'
)
}
export function loadSavedAttachments (): void {
const client = getClient()
if (client !== undefined) {
const savedAttachmentsQuery = createQuery(true)
savedAttachmentsQuery.query(
attachment.class.SavedAttachments,
{ space: core.space.Workspace },
(res) => {
savedAttachmentsStore.set(res.filter(({ $lookup }) => $lookup?.attachedTo !== undefined))
},
{ lookup: { attachedTo: attachment.class.Attachment }, sort: { modifiedOn: SortingOrder.Descending } }
)
} else {
setTimeout(() => {
loadSavedAttachments()
}, 50)
}
}
export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
const ops = getClient().apply(generateId(), 'removeActivityChannels')
try {
for (const context of contexts) {
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()
}
}
export async function readActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
const client = InboxNotificationsClientImpl.getClient()
const notificationsByContext = get(client.inboxNotificationsByContext)
const ops = getClient().apply(generateId(), 'readActivityChannels')
try {
for (const context of contexts) {
const notifications = notificationsByContext.get(context._id) ?? []
await client.archiveNotifications(
ops,
notifications
.filter(({ _class }) => _class === notification.class.ActivityInboxNotification)
.map(({ _id }) => _id)
)
await ops.update(context, { lastViewedTimestamp: Date.now() })
}
} finally {
await ops.commit()
}
}