Init poll applet (#9586)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-07-23 19:05:48 +07:00
committed by GitHub
parent fed9ef4e5a
commit 679a3df09f
56 changed files with 2500 additions and 307 deletions
@@ -18,17 +18,21 @@
import cardPlugin from '@hcengineering/card'
import { getCurrentAccount } from '@hcengineering/core'
import { AttachmentPreview, LinkPreview } from '@hcengineering/attachment-resources'
import { LinkPreviewID, Message, MessageType } from '@hcengineering/communication-types'
import { AttachmentID, Message, MessageType } from '@hcengineering/communication-types'
import { getResource } from '@hcengineering/platform'
import { isAppletAttachment, isBlobAttachment, isLinkPreviewAttachment } from '@hcengineering/communication-shared'
import { Component } from '@hcengineering/ui'
import ReactionsList from '../ReactionsList.svelte'
import MessageThread from '../thread/Thread.svelte'
import { toggleReaction } from '../../utils'
import communication from '../../plugin'
export let message: Message
const me = getCurrentAccount()
const communicationClient = getCommunicationClient()
const client = getClient()
function canReply (): boolean {
return message.type !== MessageType.Activity && message.extra?.threadRoot !== true
@@ -53,49 +57,69 @@
await r(_id, c)
}
async function removeLinkPreview (id: LinkPreviewID): Promise<void> {
await communicationClient.linkPreviewPatch(message.cardId, message.id, {
detach: [id]
async function removeLinkPreview (id: AttachmentID): Promise<void> {
await communicationClient.attachmentPatch(message.cardId, message.id, {
remove: [id]
})
}
const appletsModels = client.getModel().findAllSync(communication.class.Applet, {})
$: blobs = message.attachments.filter(isBlobAttachment) ?? []
$: links = message.attachments.filter(isLinkPreviewAttachment) ?? []
$: applets = message.attachments.filter(isAppletAttachment) ?? []
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if message.blobs.length > 0 && !message.removed}
{#if applets.length > 0 && !message.removed}
<div class="message__applets">
{#each applets as applet (applet.id)}
{@const appletModel = appletsModels.find((it) => it.type === applet.type)}
{#if appletModel}
<Component
is={appletModel.component}
props={{
applet: appletModel,
attachment: applet
}}
/>
{/if}
{/each}
</div>
{/if}
{#if blobs.length > 0 && !message.removed}
<div class="message__files">
{#each message.blobs as blob (blob.blobId)}
{#each blobs as blob (blob.id)}
<AttachmentPreview
value={{
file: blob.blobId,
type: blob.mimeType,
name: blob.fileName,
size: blob.size,
metadata: blob.metadata
file: blob.params.blobId,
type: blob.params.mimeType,
name: blob.params.fileName,
size: blob.params.size,
metadata: blob.params.metadata
}}
imageSize="x-large"
/>
{/each}
</div>
{/if}
{#if (message.linkPreviews ?? []).length > 0 && !message.removed}
{#if links.length > 0 && !message.removed}
<div class="message__links">
{#each message.linkPreviews as link (link.id)}
{#each links as link (link.id)}
<LinkPreview
isOwn={me.socialIds.includes(message.creator)}
on:delete={() => {
void removeLinkPreview(link.id)
}}
linkPreview={{
url: link.url,
host: link.host,
title: link.title,
description: link.description,
hostname: link.siteName,
image: link.previewImage?.url,
imageWidth: link.previewImage?.width,
imageHeight: link.previewImage?.height,
icon: link.iconUrl
url: link.params.url,
host: link.params.host,
title: link.params.title,
description: link.params.description,
hostname: link.params.siteName,
image: link.params.previewImage?.url,
imageWidth: link.params.previewImage?.width,
imageHeight: link.params.previewImage?.height,
icon: link.params.iconUrl
}}
/>
{/each}
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Markup, RateLimiter, Ref } from '@hcengineering/core'
import { generateId, Markup, RateLimiter, Ref } from '@hcengineering/core'
import { tick, createEventDispatcher } from 'svelte'
import {
uploadFile,
@@ -24,20 +24,30 @@
getFileMetadata,
isLinkPreviewEnabled
} from '@hcengineering/presentation'
import { Message, MessageID, BlobData, LinkPreviewData } from '@hcengineering/communication-types'
import {
Message,
MessageID,
LinkPreviewParams,
BlobParams,
AttachmentID,
linkPreviewType,
BlobAttachment,
AppletParams
} from '@hcengineering/communication-types'
import { AttachmentPresenter, LinkPreviewCard } from '@hcengineering/attachment-resources'
import { areEqualMarkups, isEmptyMarkup } from '@hcengineering/text'
import { updateMyPresence } from '@hcengineering/presence-resources'
import { ThrottledCaller } from '@hcengineering/ui'
import { Component, showPopup, ThrottledCaller } from '@hcengineering/ui'
import { getCurrentEmployee } from '@hcengineering/contact'
import { Card } from '@hcengineering/card'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { isAppletAttachment, isBlobAttachment, isLinkPreviewAttachment } from '@hcengineering/communication-shared'
import TextInput from '../TextInput.svelte'
import IconAttach from '../icons/Attach.svelte'
import { defaultMessageInputActions, toMarkdown, toMarkup, loadLinkPreviewData } from '../../utils'
import { defaultMessageInputActions, toMarkdown, toMarkup, loadLinkPreviewParams } from '../../utils'
import communication from '../../plugin'
import { type TextInputAction, type PresenceTyping, MessageDraft } from '../../types'
import { type TextInputAction, type PresenceTyping, MessageDraft, AppletDraft } from '../../types'
import TypingPresenter from '../TypingPresenter.svelte'
import { getDraft, messageToDraft, saveDraft, getEmptyDraft, removeDraft } from '../../draft'
@@ -45,7 +55,7 @@
export let message: Message | undefined = undefined
export let title: string = ''
export let onCancel: (() => void) | undefined = undefined
export let onSubmit: ((markdown: string, blobs: BlobData[]) => Promise<void>) | undefined = undefined
export let onSubmit: ((markdown: string, blobs: BlobParams[]) => Promise<void>) | undefined = undefined
const throttle = new ThrottledCaller(500)
const dispatch = createEventDispatcher()
@@ -55,7 +65,7 @@
const maxLinkPreviewCount = 4
const previewUrls = new Map<string, boolean>()
const linksData = new Map<string, LinkPreviewData | null>()
const linksData = new Map<string, LinkPreviewParams | null>()
let prevCard: Ref<Card> | undefined = card._id
let prevMessage: MessageID | undefined = message?.id
@@ -99,6 +109,7 @@
const markup = event.detail
const blobsToLoad = draft.blobs
const linksToLoad = draft.links
const appletsToLoad = draft.applets
const urlsToLoad = Array.from(previewUrls.keys()).filter((it) => previewUrls.get(it) === true)
draft = getEmptyDraft()
@@ -115,31 +126,69 @@
}
if (message === undefined) {
await createMessage(markdown, blobsToLoad, linksToLoad, urlsToLoad)
await createMessage(markdown, blobsToLoad, linksToLoad, urlsToLoad, appletsToLoad)
dispatch('sent')
} else {
await editMessage(message, markdown, blobsToLoad, linksToLoad)
await editMessage(message, markdown, blobsToLoad, linksToLoad, appletsToLoad)
}
}
async function attachApplets (messageId: MessageID, appletDrafts: AppletDraft[]): Promise<void> {
if (appletDrafts.length === 0) return
const toAttach: AppletDraft[] = []
for (const appletDraft of appletDrafts) {
try {
const ap = applets.find((it) => it._id === appletDraft.appletId)
if (ap?.createFn == null) {
toAttach.push(appletDraft)
continue
}
const r = await getResource(ap.createFn)
await r(card, messageId, appletDraft.params)
toAttach.push(appletDraft)
} catch (err) {
console.error(err)
}
}
if (toAttach.length > 0) {
void communicationClient.attachmentPatch<AppletParams>(card._id, messageId, {
add: toAttach.map((it) => ({
type: it.type,
params: it.params
}))
})
}
}
async function createMessage (
markdown: string,
blobs: BlobData[],
links: LinkPreviewData[],
urlsToLoad: string[]
blobs: BlobParams[],
links: LinkPreviewParams[],
urlsToLoad: string[],
appletDrafts: AppletDraft[]
): Promise<void> {
const { messageId } = await communicationClient.createMessage(card._id, card._class, markdown)
void client.update(card, {}, false, Date.now())
void attachApplets(messageId, appletDrafts)
if (blobs.length > 0) {
void communicationClient.blobPatch(card._id, messageId, {
attach: blobs
void communicationClient.attachmentPatch<BlobParams>(card._id, messageId, {
add: blobs.map((it) => ({
id: it.blobId as any as AttachmentID,
type: it.mimeType,
params: it
}))
})
}
if (links.length > 0) {
void communicationClient.linkPreviewPatch(card._id, messageId, {
attach: links
void communicationClient.attachmentPatch<LinkPreviewParams>(card._id, messageId, {
add: links.map((it) => ({
type: linkPreviewType,
params: it
}))
})
}
@@ -147,10 +196,15 @@
if (links.some((it) => it.url === url)) continue
const fetchedData = linksData.get(url)
if (fetchedData === null) continue
const data = fetchedData ?? (await loadLinkPreviewData(url))
if (data === undefined) continue
void communicationClient.linkPreviewPatch(card._id, messageId, {
attach: [data]
const params = fetchedData ?? (await loadLinkPreviewParams(url))
if (params === undefined) continue
void communicationClient.attachmentPatch<LinkPreviewParams>(card._id, messageId, {
add: [
{
type: linkPreviewType,
params
}
]
})
}
}
@@ -158,31 +212,62 @@
async function editMessage (
message: Message,
markdown: string,
blobs: BlobData[],
links: LinkPreviewData[]
blobs: BlobParams[],
links: LinkPreviewParams[],
appletDrafts: AppletDraft[]
): Promise<void> {
await communicationClient.updateMessage(card._id, message.id, markdown)
const attachBlobs = blobs.filter((it) => !message.blobs.some((b) => b.blobId === it.blobId))
const attachBlobs = blobs.filter(
(b) => !message.attachments.some((it) => isBlobAttachment(it) && it.params.blobId === b.blobId)
)
void attachApplets(
message.id,
appletDrafts.filter((a) => !message.attachments.some((it) => it.id === a.id))
)
if (attachBlobs.length > 0) {
void communicationClient.blobPatch(card._id, message.id, {
attach: attachBlobs
void communicationClient.attachmentPatch<BlobParams>(card._id, message.id, {
add: attachBlobs.map((it) => ({
id: it.blobId as any as AttachmentID,
type: it.mimeType,
params: it
}))
})
}
const detachBlobs = message.blobs.filter((it) => !blobs.some((b) => b.blobId === it.blobId))
const detachBlobs = message.attachments.filter(
(it) => isBlobAttachment(it) && !blobs.some((b) => b.blobId === it.params.blobId)
) as BlobAttachment[]
if (detachBlobs.length > 0) {
detachBlobs.forEach((it) => {
void deleteFile(it.blobId)
void deleteFile(it.params.blobId)
})
void communicationClient.blobPatch(card._id, message.id, {
detach: detachBlobs.map((it) => it.blobId)
void communicationClient.attachmentPatch(card._id, message.id, {
remove: detachBlobs.map((it) => it.id)
})
}
const attachLinks = links.filter((it) => !message.linkPreviews.some((b) => b.url === it.url))
void communicationClient.linkPreviewPatch(card._id, message.id, {
attach: attachLinks
const detachApplets = message.attachments.filter(
(it) => isAppletAttachment(it) && !appletDrafts.some((a) => a.id === it.id)
)
if (detachApplets.length > 0) {
void communicationClient.attachmentPatch(card._id, message.id, {
remove: detachApplets.map((it) => it.id)
})
}
const attachLinks = links.filter(
(l) => !message.attachments.some((it) => isLinkPreviewAttachment(it) && it.params.url === l.url)
)
void communicationClient.attachmentPatch(card._id, message.id, {
add: attachLinks.map((it) => ({
type: linkPreviewType,
params: it
}))
})
dispatch('edited')
@@ -237,7 +322,8 @@
async function handleCancel (): Promise<void> {
onCancel?.()
for (const blob of draft.blobs) {
const fromMessage = message?.blobs.some((it) => it.blobId === blob.blobId)
const fromMessage =
message?.attachments.some((it) => isBlobAttachment(it) && it.params.blobId === blob.blobId) ?? false
if (!fromMessage) {
void deleteFile(blob.blobId)
}
@@ -342,7 +428,7 @@
for (const url of urls) {
try {
const data = linksData.get(url) ?? (await loadLinkPreviewData(url))
const data = linksData.get(url) ?? (await loadLinkPreviewParams(url))
linksData.set(url, data ?? null)
if (data === undefined || !previewUrls.has(url)) continue
if (draftId !== draft._id) return
@@ -376,17 +462,41 @@
}
function isEmptyDraft (): boolean {
return isEmptyMarkup(draft.content) && draft.blobs.length === 0
return isEmptyMarkup(draft.content) && draft.blobs.length === 0 && draft.applets.length === 0
}
function hasChanges (blobs: BlobData[], message: Message | undefined): boolean {
function hasChanges (blobs: BlobParams[], message: Message | undefined, appletDrafts: any[]): boolean {
if (isEmptyDraft()) return false
if (message === undefined) return blobs.length > 0 || !isEmptyMarkup(draft.content)
if (message.blobs.length !== blobs.length) return true
if (message.blobs.some((it) => !blobs.some((f) => f.blobId === it.blobId))) return true
if (message === undefined) return blobs.length > 0 || !isEmptyMarkup(draft.content) || appletDrafts.length > 0
const messageBlobs = message?.attachments.filter(isBlobAttachment) ?? []
const messageApplets = message?.attachments.filter(isAppletAttachment) ?? []
if (messageBlobs.length !== blobs.length) return true
if (messageBlobs.some((it) => !blobs.some((b) => b.blobId === it.params.blobId))) return true
if (messageApplets.length !== appletDrafts.length) return true
if (messageApplets.some((it) => !appletDrafts.some((a) => a.id === it.id))) return true
return !areEqualMarkups(draft.content, toMarkup(message.content))
}
const applets = client.getModel().findAllSync(communication.class.Applet, {})
const appletActions: TextInputAction[] = applets.map((applet) => {
return {
label: applet.label,
icon: applet.icon,
action: () => {
showPopup(applet.createComponent, { applet }, 'center', (result) => {
if (result != null) {
draft = {
...draft,
applets: [...draft.applets, { id: generateId(), type: applet.type, appletId: applet._id, params: result }]
}
}
})
},
order: 99999
}
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -413,16 +523,50 @@
placeholder={title !== '' ? communication.string.MessageIn : undefined}
placeholderParams={title !== '' ? { title } : undefined}
loading={progress}
hasChanges={hasChanges(draft.blobs, message)}
actions={[...defaultMessageInputActions, attachAction]}
hasChanges={hasChanges(draft.blobs, message, draft.applets)}
actions={[...defaultMessageInputActions, attachAction, ...appletActions]}
on:submit={handleSubmit}
on:update={onUpdate}
onCancel={onCancel ? handleCancel : undefined}
onPaste={pasteAction}
>
<div slot="header" class="header">
{#if draft.blobs.length > 0 || draft.links.length > 0}
{#if draft.blobs.length > 0 || draft.links.length > 0 || draft.applets.length > 0}
<div class="flex-row-center files-list scroll-divider-color flex-gap-2 mt-2">
{#each draft.applets as appletDraft}
{@const applet = applets.find((it) => it._id === appletDraft.appletId)}
{#if applet}
<Component
is={applet.previewComponent}
props={{
applet,
params: appletDraft.params,
editing: message !== undefined && message.attachments.some((it) => it.id === appletDraft.id)
}}
on:change={() => {
showPopup(applet.createComponent, { applet, params: appletDraft.params }, 'center', (result) => {
if (result != null) {
draft = {
...draft,
applets: draft.applets.map((it) => {
if (it.id === appletDraft.id) {
return { ...it, params: result }
}
return it
})
}
}
})
}}
on:delete={() => {
draft = {
...draft,
applets: draft.applets.filter((it) => it.id !== appletDraft.id)
}
}}
/>
{/if}
{/each}
{#each draft.blobs as attachedBlob (attachedBlob.blobId)}
<div class="item flex">
<AttachmentPresenter
@@ -442,7 +586,11 @@
blobs: draft.blobs.filter((it) => it.blobId !== attachedBlob.blobId)
}
if (!message?.blobs?.some((it) => it.blobId === attachedBlob.blobId)) {
if (
!message?.attachments?.some(
(it) => isBlobAttachment(it) && it.params.blobId === attachedBlob.blobId
)
) {
void deleteFile(attachedBlob.blobId)
}
}
@@ -35,6 +35,8 @@
} from '../../stores'
import { getMessageActions } from '../../actions'
import communication from '../../plugin'
import { isBlobAttachment } from '@hcengineering/communication-shared'
import { getCommunicationClient } from '@hcengineering/presentation'
export let card: Card
export let message: Message
@@ -15,23 +15,39 @@
<script lang="ts">
import { AttributeModel } from '@hcengineering/view'
import { ActivityAttributeUpdate } from '@hcengineering/communication-types'
import { getClient } from '@hcengineering/presentation'
import { Class, Ref } from '@hcengineering/core'
import { Card } from '@hcengineering/card'
import { Component } from '@hcengineering/ui'
import ActivitySetAttributesViewer from './ActivitySetAttributeViewer.svelte'
import ActivityAddAttributeViewer from './ActivityAddAttributeViewer.svelte'
import ActivityRemoveAttributeViewer from './ActivityRemoveAttributeViewer.svelte'
import communication from '../../../plugin'
export let model: AttributeModel
export let model: AttributeModel | undefined
export let update: ActivityAttributeUpdate
export let cardType: Ref<Class<Card>>
const client = getClient()
$: customPresenter = client.getModel().findAllSync(communication.class.CustomActivityPresenter, {
attribute: update.attrKey,
type: cardType
})[0]
</script>
{#if update?.set !== undefined}
<ActivitySetAttributesViewer {model} value={update.set} />
{/if}
{#if customPresenter}
<Component is={customPresenter.component} props={{ update }} />
{:else}
{#if update?.set !== undefined && model}
<ActivitySetAttributesViewer {model} value={update.set} />
{/if}
{#if (update.added?.length ?? 0) > 0}
<ActivityAddAttributeViewer {model} value={update.added ?? []} />
{/if}
{#if model && (update.added?.length ?? 0) > 0}
<ActivityAddAttributeViewer {model} value={update.added ?? []} />
{/if}
{#if (update.removed?.length ?? 0) > 0}
<ActivityRemoveAttributeViewer {model} value={update.removed ?? []} />
{#if model && (update.removed?.length ?? 0) > 0}
<ActivityRemoveAttributeViewer {model} value={update.removed ?? []} />
{/if}
{/if}
@@ -30,8 +30,8 @@
export let author: Person | undefined
</script>
{#if update.type === ActivityUpdateType.Attribute && model}
<ActivityUpdateAttributeViewer {model} {update} />
{#if update.type === ActivityUpdateType.Attribute}
<ActivityUpdateAttributeViewer {model} {update} cardType={card._class} />
{:else if update.type === ActivityUpdateType.Tag}
<ActivityUpdateTagViewer {update} {content} />
{:else if update.type === ActivityUpdateType.Collaborators}