Fix message links (#10660)

* Merge with develop

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix errors

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix formatting

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
Co-authored-by: Kristina <kristin.fefelova@gmail.com>
This commit is contained in:
Artyom Savchenko
2026-03-23 14:56:03 +07:00
committed by GitHub
co-authored by Kristina
parent f9cb97007e
commit d26924408c
17 changed files with 211 additions and 46 deletions
+6
View File
@@ -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
+4
View File
@@ -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)
}
@@ -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<Array<WithLookup<SavedMessage>>>([])
export const messageInFocus = writable<Ref<ActivityMessage> | undefined>(undefined)
@@ -23,6 +24,14 @@ export const editingMessageStore = writable<Ref<ActivityMessage> | 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,
@@ -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}
<div class="notify" />
@@ -0,0 +1,21 @@
<script lang="ts">
import { ActivityMessage } from '@hcengineering/activity'
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
export let value: ActivityMessage
</script>
<div class="hulyPopup-container autoWidth scroll" style:minWidth="20rem" style:maxWidth="40rem">
<ActivityMessagePresenter {value} withActions={false} hoverable={false} withShowMore={false} skipLabel />
</div>
<style lang="scss">
.scroll {
flex-grow: 1;
min-height: 0;
height: max-content;
overflow-x: hidden;
overflow-y: auto;
}
</style>
+4 -2
View File
@@ -38,7 +38,8 @@ import {
canSaveForLater,
canUnpinMessage,
removeFromSaved,
shouldScrollToActivity
shouldScrollToActivity,
activityMessageTooltipProvider
} from './utils'
export * from './types'
@@ -88,7 +89,8 @@ export default async (): Promise<Resources> => ({
CanRemoveFromSaved: canRemoveFromSaved,
CanPinMessage: canPinMessage,
CanUnpinMessage: canUnpinMessage,
ShouldScrollToActivity: shouldScrollToActivity
ShouldScrollToActivity: shouldScrollToActivity,
ActivityMessageTooltipProvider: activityMessageTooltipProvider
},
backreference: {
Update: updateReferences
+27 -2
View File
@@ -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<LabelAndProps | undefined> {
if (doc == null) return undefined
return {
component: ActivityMessageTooltip,
props: { value: doc },
timeout: 300,
style: 'modern',
noArrow: true
}
}
+7 -3
View File
@@ -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<LabelAndProps | undefined>
>
},
backreference: {
// Update list of back references
@@ -99,6 +99,7 @@
const urlSet = new Set<string>()
let progress = false
let loadingLinks = false
let refContainer: HTMLElement
@@ -380,7 +381,7 @@
}
async function loadLinks (urls: string[]): Promise<void> {
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<void> {
@@ -492,7 +493,7 @@
{showSend}
{showActions}
autofocus={autofocus ? 'end' : false}
loading={loading || progress}
loading={loading || progress || loadingLinks}
{boundary}
{docClass}
extraActions={[
@@ -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<ActivityMessage>): void {
@@ -147,9 +148,6 @@
} else {
scrollToMessage()
}
} else if (selectedMessageId === undefined) {
provider.jumpToEnd()
reinitializeScroll()
}
}
@@ -392,6 +390,7 @@
async function handleScrollToLatestMessage (): Promise<void> {
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
@@ -119,6 +119,6 @@
on:click
/>
{:else}
<ActivityMessagePreview value={displayMessage} {actions} {space} doc={object} />
<ActivityMessagePreview value={displayMessage} {actions} {space} doc={object} on:click />
{/if}
{/if}
@@ -54,11 +54,17 @@
</script>
{#if hierarchy.isDerived(value._class, notification.class.ActivityInboxNotification)}
<ActivityInboxNotificationPresenter value={asDisplayActivityNotification(value)} {object} {viewlets} {space} />
<ActivityInboxNotificationPresenter
value={asDisplayActivityNotification(value)}
{object}
{viewlets}
{space}
on:click
/>
{:else if hierarchy.isDerived(value._class, notification.class.MentionInboxNotification)}
<MentionInboxNotificationPresenter value={asMentionNotification(value)} {object} {space} />
<MentionInboxNotificationPresenter value={asMentionNotification(value)} {object} {space} on:click />
{:else if hierarchy.isDerived(value._class, notification.class.ReactionInboxNotification)}
<ReactionInboxNotificationPresenter value={asReactionNotification(value)} {object} />
<ReactionInboxNotificationPresenter value={asReactionNotification(value)} {object} on:click />
{:else if hierarchy.isDerived(value._class, notification.class.CommonInboxNotification)}
<CommonInboxNotificationPresenter value={asCommonNotification(value)} />
{/if}
@@ -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",
@@ -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<T extends Doc> (
return { label: hierarchy.getClass(objectclass).label }
}
async function getDocLabel (_id: Ref<Doc>, _class: Ref<Class<Doc>>, doc?: Doc): Promise<string> {
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<T extends Doc> (
objectclass: Ref<Class<T>>,
id: Ref<T>,
@@ -430,23 +450,21 @@ export async function getReferenceLabel<T extends Doc> (
const client = getClient()
const hierarchy = client.getHierarchy()
const labelProvider = hierarchy.classHierarchyMixin(objectclass as Ref<Class<Doc>>, 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<ActivityMessage> }))
if (message === undefined) return ''
const titleMixin = hierarchy.classHierarchyMixin(objectclass as Ref<Class<Doc>>, 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<T extends Doc> (
@@ -457,6 +475,14 @@ export async function getReferenceObject<T extends Doc> (
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<ActivityMessage> }))
if (message === undefined) return undefined
return message
}
const referenceObjectProvider = hierarchy.classHierarchyMixin(
objectclass as Ref<Class<Doc>>,
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<ActivityMessage>, _class: activity.class.ActivityMessage }
}
if ((location.fragment ?? '') !== '') {
const obj = await getObjectFromFragment(location.fragment ?? '')
+1
View File
@@ -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",
@@ -36,6 +36,7 @@
export let inlineBlock = false
export let noSelect: boolean = true
export let title: string | undefined = undefined
export let query: Record<string, string> | 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)
@@ -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<Class<Doc>> | undefined): Promise<void> {
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<Class<Doc>>): 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}
>
<DocNavLink object={doc} component={docComponent} {disabled} inlineReference {onClick} {transparent}>
<DocNavLink
object={parentDoc ?? doc}
component={docComponent}
{disabled}
inlineReference
{onClick}
{transparent}
query={docClass && docId && hierarchy.isDerived(docClass, activity.class.ActivityMessage)
? { message: docId }
: undefined}
>
{#if icon}{#if icon === view.ids.IconWithEmoji}<IconWithEmoji
icon={clazz?.color ?? 0}
size={'smaller'}