Files
huly-platform/plugins/communication-resources/src/actions.ts
T

290 lines
9.5 KiB
TypeScript

// Copyright © 2025 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 communication, {
type MessageAction,
type MessageActionFunction,
type MessageActionVisibilityTester
} from '@hcengineering/communication'
import { languageStore, showPopup } from '@hcengineering/ui'
import emojiPlugin from '@hcengineering/emoji'
import { type Message, MessageType, SortingOrder } from '@hcengineering/communication-types'
import cardPlugin, { type Card, type MasterTag } from '@hcengineering/card'
import { addRefreshListener, getClient, getCommunicationClient } from '@hcengineering/presentation'
import {
AccountRole,
fillDefaults,
generateId,
getCurrentAccount,
hasAccountRole,
type MarkupBlobRef,
type Ref
} from '@hcengineering/core'
import { getMetadata, getResource } from '@hcengineering/platform'
import { employeeByPersonIdStore } from '@hcengineering/contact-resources'
import { getEmployeeBySocialId } from '@hcengineering/contact'
import { makeRank } from '@hcengineering/rank'
import chat from '@hcengineering/chat'
import { markupToText } from '@hcengineering/text'
import { get } from 'svelte/store'
import { translate as aiTranslate } from '@hcengineering/ai-bot-resources'
import aiBot from '@hcengineering/ai-bot'
import CreateCardFromMessagePopup from './components/CreateCardFromMessagePopup.svelte'
import { isCardAllowedForCommunications, showForbidden, toggleReaction, toMarkup } from './utils'
import {
isMessageManualTranslating,
messageEditingStore,
showOriginalMessagesStore,
threadCreateMessageStore,
translateMessagesStore,
translateToStore
} from './stores'
export const addReaction: MessageActionFunction = async (message, card: Card, evt, onOpen, onClose) => {
if (!isCardAllowedForCommunications(card)) {
await showForbidden()
return
}
if (onOpen !== undefined) onOpen()
showPopup(
emojiPlugin.component.EmojiPopup,
{},
evt?.target as HTMLElement,
async (result) => {
if (onClose !== undefined) onClose()
const emoji = result?.text
if (emoji == null) return
await toggleReaction(message, emoji)
},
() => {}
)
}
export const replyInThread: MessageActionFunction = async (message: Message, parentCard: Card): Promise<void> => {
if (!isCardAllowedForCommunications(parentCard)) {
await showForbidden()
return
}
const thread = message.threads[0]
if (thread != null) {
const client = getClient()
const _id = thread.threadId
const card = await client.findOne(cardPlugin.class.Card, { _id: _id as Ref<Card> })
if (card === undefined) return
const r = await getResource(cardPlugin.function.OpenCardInSidebar)
await r(_id, card)
return
}
await attachCardToMessage(
message,
parentCard,
createThreadTitle(message, parentCard),
chat.masterTag.Thread,
`${parentCard._id}_${message.id}` as Ref<Card>
)
}
export async function attachCardToMessage (
message: Message,
parentCard: Card,
title: string,
type: Ref<MasterTag>,
_id?: Ref<Card>
): Promise<void> {
const client = getClient()
const communicationClient = getCommunicationClient()
const hierarchy = client.getHierarchy()
const threadCardID = _id ?? generateId<Card>()
const author =
get(employeeByPersonIdStore).get(message.creator) ?? (await getEmployeeBySocialId(client, message.creator))
const lastOne = await client.findOne(cardPlugin.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
const data = fillDefaults<Card>(
hierarchy,
{
title,
rank: makeRank(lastOne?.rank, undefined),
content: '' as MarkupBlobRef,
blobs: {},
parentInfo: [
{
_id: parentCard._id,
_class: parentCard._class,
title: parentCard.title
}
]
},
type
)
await client.createDoc(type, parentCard.space, data, threadCardID)
await communicationClient.attachThread(parentCard._id, message.id, threadCardID, type)
if (author?.active === true && author?.personUuid !== undefined) {
await communicationClient.addCollaborators(threadCardID, type, [author.personUuid])
}
const threadCard = await client.findOne(cardPlugin.class.Card, { _id: threadCardID })
if (threadCard === undefined) return
const r = await getResource(cardPlugin.function.OpenCardInSidebar)
await r(threadCard._id, threadCard)
}
function createThreadTitle (message: Message, parent: Card): string {
const markup = toMarkup(message.content)
const messageText = markupToText(markup).trim()
return messageText.length > 0 ? messageText : `Thread from ${parent.title}`
}
export const canReplyInThread: MessageActionVisibilityTester = (message: Message): boolean => {
return message.type === MessageType.Text && message.extra?.threadRoot !== true
}
export const translateMessage: MessageActionFunction = async (message: Message): Promise<void> => {
const language = get(translateToStore) ?? get(languageStore)
if (isMessageManualTranslating(message.cardId, message.id)) return
const result = get(translateMessagesStore).find((it) => it.cardId === message.cardId && it.messageId === message.id)
showOriginalMessagesStore.update((store) =>
store.filter(([cId, mId]) => cId !== message.cardId || mId !== message.id)
)
if (result != null) return
translateMessagesStore.update((store) => {
store.push({ inProgress: true, messageId: message.id, cardId: message.cardId })
return store
})
const markup = toMarkup(message.content)
const currentTranslate = message?.translates?.[language] ?? ''
const response = currentTranslate !== '' ? toMarkup(currentTranslate) : (await aiTranslate(markup, language))?.text
if (response !== undefined) {
translateMessagesStore.update((store) => {
return store.map((it) => {
if (it.cardId === message.cardId && it.messageId === message.id) {
return {
...it,
inProgress: false,
result: response
}
}
return it
})
})
} else {
translateMessagesStore.update((store) => {
return store.filter((it) => it.cardId !== message.cardId || it.messageId !== message.id)
})
}
}
export const canTranslateMessage: MessageActionVisibilityTester = (message: Message): boolean => {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
if (url === '') return false
return message.type === MessageType.Text
}
export const showOriginalMessage: MessageActionFunction = async (message: Message): Promise<void> => {
const messageId = message.id
showOriginalMessagesStore.update((store) => {
store.push([message.cardId, messageId])
return store
})
}
export const canShowOriginalMessage: MessageActionVisibilityTester = (message: Message): boolean => {
return canTranslateMessage(message)
}
export const editMessage: MessageActionFunction = async (message: Message): Promise<void> => {
messageEditingStore.set(message.id)
}
export const canEditMessage: MessageActionVisibilityTester = (message: Message): boolean => {
if (message.type !== MessageType.Text) return false
const me = getCurrentAccount()
return me.socialIds.includes(message.creator)
}
export const removeMessage: MessageActionFunction = async (message: Message): Promise<void> => {
const communicationClient = getCommunicationClient()
await communicationClient.removeMessage(message.cardId, message.id)
}
export const canRemoveMessage: MessageActionVisibilityTester = (message: Message): boolean => {
if (message.type !== MessageType.Text) return false
const me = getCurrentAccount()
return me.socialIds.includes(message.creator)
}
export const createCard: MessageActionFunction = async (message: Message, card: Card): Promise<void> => {
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
await showForbidden()
return
}
threadCreateMessageStore.set(message)
showPopup(CreateCardFromMessagePopup, { message, card }, undefined, () => {
threadCreateMessageStore.set(undefined)
})
}
export const canCreateCard: MessageActionVisibilityTester = (message: Message): boolean => {
return canReplyInThread(message)
}
let allMessageActions: MessageAction[] | undefined
addRefreshListener(() => {
allMessageActions = undefined
})
export async function getMessageActions (message: Message): Promise<MessageAction[]> {
const client = getClient()
const actions: MessageAction[] =
allMessageActions ?? client.getModel().findAllSync(communication.class.MessageAction, {})
if (allMessageActions === undefined) {
allMessageActions = actions
}
const filteredActions = await filterActions(message, actions)
return filteredActions.sort((a, b) => a.order - b.order)
}
async function filterActions (message: Message, actions: MessageAction[]): Promise<MessageAction[]> {
const result: MessageAction[] = []
for (const action of actions) {
if (action.visibilityTester == null) {
result.push(action)
} else {
const visibilityTester = await getResource(action.visibilityTester)
if (visibilityTester(message)) {
result.push(action)
}
}
}
return result
}