From d26924408c6c2a461a7e54da4d861a21d92319d6 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 23 Mar 2026 14:56:03 +0700 Subject: [PATCH] Fix message links (#10660) * Merge with develop Signed-off-by: Artem Savchenko * Fix errors Signed-off-by: Artem Savchenko * Fix formatting Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko Co-authored-by: Kristina --- common/config/rush/pnpm-lock.yaml | 6 ++ models/activity/src/index.ts | 4 ++ plugins/activity-resources/src/activity.ts | 9 +++ .../ActivityMessageTemplate.svelte | 10 ++- .../ActivityMessageTooltip.svelte | 21 +++++++ plugins/activity-resources/src/index.ts | 6 +- plugins/activity-resources/src/utils.ts | 29 ++++++++- plugins/activity/src/index.ts | 10 ++- .../src/components/AttachmentRefInput.svelte | 7 ++- .../ReverseChannelScrollView.svelte | 13 ++-- .../ActivityInboxNotificationPresenter.svelte | 2 +- .../inbox/InboxNotificationPresenter.svelte | 12 +++- plugins/text-editor-resources/package.json | 1 + .../src/components/extension/reference.ts | 58 ++++++++++++----- plugins/view-resources/package.json | 1 + .../src/components/DocNavLink.svelte | 6 +- .../src/components/ObjectMention.svelte | 62 ++++++++++++++++--- 17 files changed, 211 insertions(+), 46 deletions(-) create mode 100644 plugins/activity-resources/src/components/activity-message/ActivityMessageTooltip.svelte diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index f2566db58f..a8b3dfc6b8 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -28288,6 +28288,9 @@ importers: ../../plugins/text-editor-resources: dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../activity '@hcengineering/analytics': specifier: workspace:^0.7.17 version: link:../../foundations/core/packages/analytics @@ -29701,6 +29704,9 @@ importers: ../../plugins/view-resources: dependencies: + '@hcengineering/activity': + specifier: workspace:^0.7.0 + version: link:../activity '@hcengineering/analytics': specifier: workspace:^0.7.17 version: link:../../foundations/core/packages/analytics diff --git a/models/activity/src/index.ts b/models/activity/src/index.ts index 82b32c7fed..5bc99c74b3 100644 --- a/models/activity/src/index.ts +++ b/models/activity/src/index.ts @@ -390,6 +390,10 @@ export function createModel (builder: Builder): void { txClasses: [core.class.TxCreateDoc] }) + builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.ObjectTooltip, { + provider: activity.function.ActivityMessageTooltipProvider + }) + buildActions(builder) buildNotifications(builder) } diff --git a/plugins/activity-resources/src/activity.ts b/plugins/activity-resources/src/activity.ts index 5cf6e364e1..2b984737ce 100644 --- a/plugins/activity-resources/src/activity.ts +++ b/plugins/activity-resources/src/activity.ts @@ -16,6 +16,7 @@ import activity, { type ActivityMessage, type SavedMessage } from '@hcengineerin import core, { type Ref, SortingOrder, type WithLookup } from '@hcengineering/core' import { createQuery, onClient } from '@hcengineering/presentation' import { writable } from 'svelte/store' +import { getCurrentLocation, navigate } from '@hcengineering/ui' export const savedMessagesStore = writable>>([]) export const messageInFocus = writable | undefined>(undefined) @@ -23,6 +24,14 @@ export const editingMessageStore = writable | undefined>(un const savedMessagesQuery = createQuery(true) +export function clearMessageInLocation (): void { + const loc = getCurrentLocation() + if (loc.query?.message != null) { + delete loc.query.message + navigate(loc, true) + } +} + onClient(() => { savedMessagesQuery.query( activity.class.SavedMessage, diff --git a/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte b/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte index 7dea1f37c7..34e98a6af9 100644 --- a/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte +++ b/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte @@ -29,7 +29,7 @@ import { Action as ViewAction } from '@hcengineering/view' import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources' - import { savedMessagesStore } from '../../activity' + import { clearMessageInLocation, savedMessagesStore } from '../../activity' import { MessageInlineAction } from '../../types' import ActivityMessageActions from '../ActivityMessageActions.svelte' import MessageTimestamp from '../MessageTimestamp.svelte' @@ -106,6 +106,13 @@ isActionsOpened = false } + function handleAnimationEnd (event: AnimationEvent): void { + const name = event.animationName.split('-').pop() + if (name === 'highlight') { + clearMessageInLocation() + } + } + $: key = parentMessage != null ? `${message._id}_${parentMessage._id}` : message._id $: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined @@ -180,6 +187,7 @@ class:stale on:click={onClick} on:contextmenu={handleContextMenu} + on:animationend={handleAnimationEnd} > {#if showNotify && !embedded && !isShort}
diff --git a/plugins/activity-resources/src/components/activity-message/ActivityMessageTooltip.svelte b/plugins/activity-resources/src/components/activity-message/ActivityMessageTooltip.svelte new file mode 100644 index 0000000000..6831edf506 --- /dev/null +++ b/plugins/activity-resources/src/components/activity-message/ActivityMessageTooltip.svelte @@ -0,0 +1,21 @@ + + +
+ +
+ + diff --git a/plugins/activity-resources/src/index.ts b/plugins/activity-resources/src/index.ts index 3214143f1f..ae881318d9 100644 --- a/plugins/activity-resources/src/index.ts +++ b/plugins/activity-resources/src/index.ts @@ -38,7 +38,8 @@ import { canSaveForLater, canUnpinMessage, removeFromSaved, - shouldScrollToActivity + shouldScrollToActivity, + activityMessageTooltipProvider } from './utils' export * from './types' @@ -88,7 +89,8 @@ export default async (): Promise => ({ CanRemoveFromSaved: canRemoveFromSaved, CanPinMessage: canPinMessage, CanUnpinMessage: canUnpinMessage, - ShouldScrollToActivity: shouldScrollToActivity + ShouldScrollToActivity: shouldScrollToActivity, + ActivityMessageTooltipProvider: activityMessageTooltipProvider }, backreference: { Update: updateReferences diff --git a/plugins/activity-resources/src/utils.ts b/plugins/activity-resources/src/utils.ts index 0f1c154667..12d18d2bbb 100644 --- a/plugins/activity-resources/src/utils.ts +++ b/plugins/activity-resources/src/utils.ts @@ -1,12 +1,21 @@ import type { ActivityMessage, Reaction } from '@hcengineering/activity' -import core, { getCurrentAccount, isOtherHour, type Doc, type Ref, type Space, type Blob } from '@hcengineering/core' +import core, { + getCurrentAccount, + isOtherHour, + type Doc, + type Ref, + type Space, + type Blob, + type TxOperations +} from '@hcengineering/core' import { getClient, isSpace } from '@hcengineering/presentation' import { closePopup, getCurrentResolvedLocation, getEventPositionElement, showPopup, - type Location + type Location, + type LabelAndProps } from '@hcengineering/ui' import { type AttributeModel } from '@hcengineering/view' import emojiPlugin from '@hcengineering/emoji' @@ -14,6 +23,7 @@ import { get } from 'svelte/store' import { savedMessagesStore } from './activity' import activity from './plugin' +import ActivityMessageTooltip from './components/activity-message/ActivityMessageTooltip.svelte' export async function updateDocReactions ( reactions: Reaction[], @@ -194,3 +204,18 @@ export function getActivityNewestFirst (): boolean { export function setActivityNewestFirst (value: boolean): void { localStorage.setItem(activityNewestFirstLocalStorageKey, JSON.stringify(value)) } + +export async function activityMessageTooltipProvider ( + _client: TxOperations, + doc?: ActivityMessage | null +): Promise { + if (doc == null) return undefined + + return { + component: ActivityMessageTooltip, + props: { value: doc }, + timeout: 300, + style: 'modern', + noArrow: true + } +} diff --git a/plugins/activity/src/index.ts b/plugins/activity/src/index.ts index f3a7892037..92ff57b0cb 100644 --- a/plugins/activity/src/index.ts +++ b/plugins/activity/src/index.ts @@ -26,12 +26,13 @@ import { Timestamp, Tx, TxCUD, - Blob + Blob, + Client } from '@hcengineering/core' import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { Preference } from '@hcengineering/preference' -import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui' +import type { AnyComponent, ComponentExtensionId, LabelAndProps } from '@hcengineering/ui' import type { Action } from '@hcengineering/view' /** @@ -348,7 +349,10 @@ export default plugin(activityId, { ActivityEmployeePresenter: '' as ComponentExtensionId }, function: { - ShouldScrollToActivity: '' as Resource<() => boolean> + ShouldScrollToActivity: '' as Resource<() => boolean>, + ActivityMessageTooltipProvider: '' as Resource< + (client: Client, doc?: Doc | null) => Promise + > }, backreference: { // Update list of back references diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte index f60068fc63..d97660caa0 100644 --- a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte +++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte @@ -99,6 +99,7 @@ const urlSet = new Set() let progress = false + let loadingLinks = false let refContainer: HTMLElement @@ -380,7 +381,7 @@ } async function loadLinks (urls: string[]): Promise { - progress = true + loadingLinks = true for (const url of urls) { try { const meta = await fetchLinkPreviewDetails(url) @@ -401,7 +402,7 @@ void setPlatformStatus(unknownError(err)) } } - progress = false + loadingLinks = false } async function loadFiles (evt: ClipboardEvent): Promise { @@ -492,7 +493,7 @@ {showSend} {showActions} autofocus={autofocus ? 'end' : false} - loading={loading || progress} + loading={loading || progress || loadingLinks} {boundary} {docClass} extraActions={[ diff --git a/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte b/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte index 470217dbae..fd878caece 100644 --- a/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte +++ b/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte @@ -18,7 +18,8 @@ ActivityMessagePresenter, canGroupMessages, messageInFocus, - editingMessageStore + editingMessageStore, + clearMessageInLocation } from '@hcengineering/activity-resources' import core, { Doc, generateId, getCurrentAccount, Ref, Space, Timestamp, Tx, TxCUD } from '@hcengineering/core' import { DocNotifyContext } from '@hcengineering/notification' @@ -131,8 +132,8 @@ } }) - $: void initializeScroll($isLoadingStore, separatorDiv, separatorIndex) $: adjustScrollPosition(selectedMessageId) + $: void initializeScroll($isLoadingStore, separatorDiv, separatorIndex) $: void handleMessagesUpdated(messages.length) function adjustScrollPosition (selectedMessageId?: Ref): void { @@ -147,9 +148,6 @@ } else { scrollToMessage() } - } else if (selectedMessageId === undefined) { - provider.jumpToEnd() - reinitializeScroll() } } @@ -392,6 +390,7 @@ async function handleScrollToLatestMessage (): Promise { selectedMessageId = undefined messageInFocus.set(undefined) + clearMessageInLocation() const metadata = $metadataStore const lastMetadata = metadata[metadata.length - 1] @@ -579,7 +578,9 @@ $: showBlankView = !$isLoadingStore && messages.length === 0 && !isThread export function editLastMessage (): void { - if ($isLoadingStore || !isScrollInitialized || !$isTailLoadedStore || scrollDiv == null) return + if ($isLoadingStore || !isScrollInitialized || !$isTailLoadedStore || scrollDiv == null) { + return + } if (!isScrollAtBottom) return const me = getCurrentAccount() let lastMessage: ChatMessage | undefined = undefined diff --git a/plugins/notification-resources/src/components/inbox/ActivityInboxNotificationPresenter.svelte b/plugins/notification-resources/src/components/inbox/ActivityInboxNotificationPresenter.svelte index 6c33a6f013..10f557ff71 100644 --- a/plugins/notification-resources/src/components/inbox/ActivityInboxNotificationPresenter.svelte +++ b/plugins/notification-resources/src/components/inbox/ActivityInboxNotificationPresenter.svelte @@ -119,6 +119,6 @@ on:click /> {:else} - + {/if} {/if} diff --git a/plugins/notification-resources/src/components/inbox/InboxNotificationPresenter.svelte b/plugins/notification-resources/src/components/inbox/InboxNotificationPresenter.svelte index 22f99b500f..4b2e90e4a1 100644 --- a/plugins/notification-resources/src/components/inbox/InboxNotificationPresenter.svelte +++ b/plugins/notification-resources/src/components/inbox/InboxNotificationPresenter.svelte @@ -54,11 +54,17 @@ {#if hierarchy.isDerived(value._class, notification.class.ActivityInboxNotification)} - + {:else if hierarchy.isDerived(value._class, notification.class.MentionInboxNotification)} - + {:else if hierarchy.isDerived(value._class, notification.class.ReactionInboxNotification)} - + {:else if hierarchy.isDerived(value._class, notification.class.CommonInboxNotification)} {/if} diff --git a/plugins/text-editor-resources/package.json b/plugins/text-editor-resources/package.json index 653283bf0e..94dfc604c0 100644 --- a/plugins/text-editor-resources/package.json +++ b/plugins/text-editor-resources/package.json @@ -99,6 +99,7 @@ "mermaid": "^11.12.0", "@hcengineering/theme": "workspace:^0.7.0", "tippy.js": "~6.3.7", + "@hcengineering/activity": "workspace:^0.7.0", "@hcengineering/chunter": "workspace:^0.7.0", "@tiptap/extension-text-align": "~2.11.0", "@tiptap/extension-mathematics": "^2.11.7", diff --git a/plugins/text-editor-resources/src/components/extension/reference.ts b/plugins/text-editor-resources/src/components/extension/reference.ts index 64f6a1dff6..d927118cbc 100644 --- a/plugins/text-editor-resources/src/components/extension/reference.ts +++ b/plugins/text-editor-resources/src/components/extension/reference.ts @@ -25,7 +25,7 @@ import { type Blob, type Class, type Doc, type Ref } from '@hcengineering/core' import { getMetadata, getResource, translate } from '@hcengineering/platform' import presentation, { createQuery, getBlobRef, getClient, MessageBox } from '@hcengineering/presentation' import view from '@hcengineering/view' - +import activity, { type ActivityMessage } from '@hcengineering/activity' import contact from '@hcengineering/contact' import { parseLocation, showPopup, tooltip, type LabelAndProps, type Location, fromCodePoint } from '@hcengineering/ui' import workbench, { type Application } from '@hcengineering/workbench' @@ -422,6 +422,26 @@ async function getReferenceTooltip ( return { label: hierarchy.getClass(objectclass).label } } +async function getDocLabel (_id: Ref, _class: Ref>, doc?: Doc): Promise { + const client = getClient() + const hierarchy = client.getHierarchy() + + const labelProvider = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIdentifier) + const labelProviderFn = labelProvider !== undefined ? await getResource(labelProvider.provider) : undefined + + const titleMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectTitle) + const titleProviderFn = titleMixin !== undefined ? await getResource(titleMixin.titleProvider) : undefined + + const identifier = (await labelProviderFn?.(client, _id, doc)) ?? '' + const title = (await titleProviderFn?.(client, _id, doc)) ?? '' + + return identifier !== '' && title !== '' && identifier !== title + ? `${identifier} ${title}` + : title !== '' + ? title + : identifier +} + export async function getReferenceLabel ( objectclass: Ref>, id: Ref, @@ -430,23 +450,21 @@ export async function getReferenceLabel ( const client = getClient() const hierarchy = client.getHierarchy() - const labelProvider = hierarchy.classHierarchyMixin(objectclass as Ref>, view.mixin.ObjectIdentifier) - const labelProviderFn = labelProvider !== undefined ? await getResource(labelProvider.provider) : undefined + if (hierarchy.isDerived(objectclass, activity.class.ActivityMessage)) { + const message = + (doc as any as ActivityMessage) ?? + (await client.findOne(activity.class.ActivityMessage, { _id: id as any as Ref })) + if (message === undefined) return '' - const titleMixin = hierarchy.classHierarchyMixin(objectclass as Ref>, view.mixin.ObjectTitle) - const titleProviderFn = titleMixin !== undefined ? await getResource(titleMixin.titleProvider) : undefined + const attachedToDoc = await client.findOne(message.attachedToClass, { _id: message.attachedTo }) + if (attachedToDoc === undefined) return '' - const identifier = (await labelProviderFn?.(client, id, doc)) ?? '' - const title = (await titleProviderFn?.(client, id, doc)) ?? '' + const label = await getDocLabel(attachedToDoc._id, attachedToDoc._class, attachedToDoc) - const label = - identifier !== '' && title !== '' && identifier !== title - ? `${identifier} ${title}` - : title !== '' - ? title - : identifier + return `${label}?message=${message._id}` + } - return label + return await getDocLabel(id, objectclass, doc) } export async function getReferenceObject ( @@ -457,6 +475,14 @@ export async function getReferenceObject ( const client = getClient() const hierarchy = client.getHierarchy() + if (objectclass === activity.class.ActivityMessage) { + const message: ActivityMessage | undefined = + (doc as any as ActivityMessage) ?? + (await client.findOne(activity.class.ActivityMessage, { _id: id as any as Ref })) + if (message === undefined) return undefined + return message + } + const referenceObjectProvider = hierarchy.classHierarchyMixin( objectclass as Ref>, view.mixin.ReferenceObjectProvider @@ -512,6 +538,10 @@ export async function getTargetObjectFromUrl ( const app = apps.find((p) => p.alias === appAlias) const locationResolver = app?.locationResolver const locationDataResolver = app?.locationDataResolver + const messageId = location?.query?.message ?? '' + if (messageId !== '') { + return { _id: messageId as Ref, _class: activity.class.ActivityMessage } + } if ((location.fragment ?? '') !== '') { const obj = await getObjectFromFragment(location.fragment ?? '') diff --git a/plugins/view-resources/package.json b/plugins/view-resources/package.json index 5bef5c43a1..455666e3e2 100644 --- a/plugins/view-resources/package.json +++ b/plugins/view-resources/package.json @@ -57,6 +57,7 @@ "@hcengineering/notification": "workspace:^0.7.0", "@hcengineering/presentation": "workspace:^0.7.0", "@hcengineering/card": "workspace:^0.7.0", + "@hcengineering/activity": "workspace:^0.7.0", "@hcengineering/setting": "workspace:^0.7.0", "@hcengineering/text-editor": "workspace:^0.7.0", "@hcengineering/text-editor-resources": "workspace:^0.7.0", diff --git a/plugins/view-resources/src/components/DocNavLink.svelte b/plugins/view-resources/src/components/DocNavLink.svelte index 298566f0a7..9ab21a0569 100644 --- a/plugins/view-resources/src/components/DocNavLink.svelte +++ b/plugins/view-resources/src/components/DocNavLink.svelte @@ -36,6 +36,7 @@ export let inlineBlock = false export let noSelect: boolean = true export let title: string | undefined = undefined + export let query: Record | undefined = undefined const docQuery = createQuery() const client = getClient() @@ -66,7 +67,10 @@ const comp = panelComponent?.component ?? component const loc = await getObjectLinkFragment(hierarchy, object, props, comp) const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin - href = concatLink(frontUrl, locationToUrl(loc)) + + href = query + ? `${concatLink(frontUrl, locationToUrl(loc))}?${new URLSearchParams(query).toString()}` + : concatLink(frontUrl, locationToUrl(loc)) } $: if (object !== undefined) getHref(object) diff --git a/plugins/view-resources/src/components/ObjectMention.svelte b/plugins/view-resources/src/components/ObjectMention.svelte index d7ad8804fe..1aec343bca 100644 --- a/plugins/view-resources/src/components/ObjectMention.svelte +++ b/plugins/view-resources/src/components/ObjectMention.svelte @@ -18,6 +18,7 @@ import { createQuery, getClient, IconWithEmoji } from '@hcengineering/presentation' import { AnyComponent, Icon, LabelAndProps, themeStore, tooltip } from '@hcengineering/ui' import view from '@hcengineering/view' + import activity, { ActivityMessage } from '@hcengineering/activity' import { getReferenceLabel } from '@hcengineering/text-editor-resources/src/components/extension/reference' import { classIcon } from '../utils' @@ -37,6 +38,7 @@ const hierarchy = client.getHierarchy() const docQuery = createQuery() + let parentDoc: Doc | undefined = undefined let doc: Doc | undefined = object ?? undefined let docLabel: string = '' @@ -49,7 +51,7 @@ let displayTitle = '' $: displayTitle = docTitle || title || docLabel - $: docComponent = getPanelComponent(doc, _class) + $: docComponent = getPanelComponent(parentDoc ?? doc, _class) $: if (object == null && _class != null && _id != null) { docQuery.query(_class, { _id }, (r) => { @@ -60,24 +62,54 @@ doc = object } - $: cl = doc?._class ?? _class + $: void updateParentDoc(doc, _class) + + async function updateParentDoc (doc: Doc | undefined, _class: Ref> | undefined): Promise { + const resultClass = doc?._class ?? _class + if (resultClass == null) { + parentDoc = undefined + return + } + + if (hierarchy.isDerived(resultClass, activity.class.ActivityMessage)) { + const message = doc as ActivityMessage + if (parentDoc?._id === message.attachedTo) return + parentDoc = await client.findOne(message.attachedToClass, { _id: message.attachedTo }) + } else { + parentDoc = undefined + } + } + + $: docClass = doc?._class ?? _class + $: docId = doc?._id ?? _id + + $: cl = parentDoc?._class ?? docClass $: clazz = cl ? hierarchy.findClass(cl) : undefined - $: icon = - doc !== undefined && !hierarchy.isDerived(doc._class, contact.class.Contact) ? classIcon(client, doc._class) : null + $: icon = getIcon(doc) $: void updateDocTitle(doc) $: void updateDocTooltip(doc) - $: void updateDocLabel(doc, _class) + $: void updateDocLabel(parentDoc ?? doc, _class) + + function getIcon (doc: Doc | undefined): any { + if (doc == null) return undefined + if (hierarchy.isDerived(doc._class, contact.class.Contact)) return undefined + + return classIcon(client, doc._class) + } function getPanelComponent (doc?: Doc, _class?: Ref>): AnyComponent { - if (component !== undefined) { - return component - } - + if (component !== undefined) return component const resultClass = doc?._class ?? _class if (resultClass === undefined) { return view.component.EditDoc + } else if (hierarchy.isDerived(resultClass, activity.class.ActivityMessage)) { + if (doc == null) return view.component.EditDoc + const message = doc as ActivityMessage + const panelComponent = hierarchy.classHierarchyMixin(message.attachedToClass, view.mixin.ObjectPanel) + + return panelComponent?.component ?? view.component.EditDoc } else { const panelComponent = hierarchy.classHierarchyMixin(resultClass, view.mixin.ObjectPanel) @@ -127,7 +159,17 @@ data-label={displayTitle} use:tooltip={docTooltip} > - + {#if icon}{#if icon === view.ids.IconWithEmoji}