diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index 8785b3d8b6d..3a75958665c 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -40,7 +40,7 @@ import { taskId } from '@hcengineering/task' import telegram, { telegramId } from '@hcengineering/telegram' import { templatesId } from '@hcengineering/templates' import tracker, { trackerId } from '@hcengineering/tracker' -import uiPlugin, { getCurrentLocation, locationStorageKeyId, locationToUrl, navigate, parseLocation, setLocationStorageKey } from '@hcengineering/ui' +import uiPlugin, { getCurrentLocation, locationStorageKeyId, navigate, setLocationStorageKey } from '@hcengineering/ui' import { uploaderId } from '@hcengineering/uploader' import { viewId } from '@hcengineering/view' import workbench, { workbenchId } from '@hcengineering/workbench' @@ -215,6 +215,7 @@ export async function configurePlatform (): Promise { setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL)) setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) + setMetadata(presentation.metadata.LinkPreviewUrl, config.LINK_PREVIEW_URL ?? '') setMetadata(presentation.metadata.StatsUrl, config.STATS_URL) setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR ?? '') @@ -238,7 +239,7 @@ export async function configurePlatform (): Promise { setMetadata(notification.metadata.PushPublicKey, config.PUSH_PUBLIC_KEY) setMetadata(rekoni.metadata.RekoniUrl, config.REKONI_URL) - setMetadata(contactPlugin.metadata.LastNameFirst, myBranding.lastNameFirst === 'true' ?? false) + setMetadata(contactPlugin.metadata.LastNameFirst, myBranding.lastNameFirst === 'true') setMetadata(love.metadata.ServiceEnpdoint, config.LOVE_ENDPOINT) setMetadata(love.metadata.WebSocketURL, config.LIVEKIT_WS) setMetadata(print.metadata.PrintURL, config.PRINT_URL) diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts index 195ac20381e..30f4daef1f9 100644 --- a/desktop/src/ui/types.ts +++ b/desktop/src/ui/types.ts @@ -5,39 +5,39 @@ import { ScreenSource } from '@hcengineering/love' */ export interface Config { ACCOUNTS_URL: string + AI_URL?: string + ANALYTICS_COLLECTOR_URL?: string + BRANDING_URL?: string + CALENDAR_URL: string COLLABORATOR?: string COLLABORATOR_URL: string - FRONT_URL: string + CONFIG_URL: string + DESKTOP_UPDATES_CHANNEL?: string + DESKTOP_UPDATES_URL?: string + DISABLE_SIGNUP?: string FILES_URL: string - UPLOAD_URL: string - MODEL_VERSION?: string - VERSION?: string - TELEGRAM_URL: string - GMAIL_URL: string - CALENDAR_URL: string - REKONI_URL: string - INITIAL_URL: string + FRONT_URL: string GITHUB_APP: string GITHUB_CLIENTID: string GITHUB_URL: string - CONFIG_URL: string - LOVE_ENDPOINT?: string + GMAIL_URL: string + INITIAL_URL: string + LINK_PREVIEW_URL?: string LIVEKIT_WS?: string - SIGN_URL?: string + LOVE_ENDPOINT?: string + MODEL_VERSION?: string + PRESENCE_URL?: string + PREVIEW_CONFIG: string PRINT_URL?: string PUSH_PUBLIC_KEY: string - ANALYTICS_COLLECTOR_URL?: string - AI_URL?:string - DISABLE_SIGNUP?: string - BRANDING_URL?: string - PREVIEW_CONFIG: string - UPLOAD_CONFIG: string - DESKTOP_UPDATES_URL?: string - DESKTOP_UPDATES_CHANNEL?: string - TELEGRAM_BOT_URL?: string - PRESENCE_URL?: string - + REKONI_URL: string + SIGN_URL?: string STATS_URL?: string + TELEGRAM_BOT_URL?: string + TELEGRAM_URL: string + UPLOAD_CONFIG: string + UPLOAD_URL: string + VERSION?: string } export interface Branding { diff --git a/packages/presentation/src/drawing.ts b/packages/presentation/src/drawing.ts index 59e20f12327..f5c2a1dabec 100644 --- a/packages/presentation/src/drawing.ts +++ b/packages/presentation/src/drawing.ts @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // - export interface DrawingData { content?: string } - export interface DrawingProps { readonly: boolean autoSize?: boolean diff --git a/packages/presentation/src/file.ts b/packages/presentation/src/file.ts index 9e18569b389..477cf374e8e 100644 --- a/packages/presentation/src/file.ts +++ b/packages/presentation/src/file.ts @@ -284,3 +284,21 @@ async function uploadFileWithSignedUrl (file: File, uuid: string, uploadUrl: str }) } } + +export async function fetchJson (file: string, name: string): Promise { + const resp = await fetch( + getFileUrl(file, name), + { signal: AbortSignal.timeout(5 * 1000) } + ) + if (!resp.ok) { + console.error({ error: `failed to process request: ${resp.status}` }) + return undefined + } + try { + return await resp.json() + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + console.error({ error: 'failed to parse json:' + message }) + } + return undefined +} diff --git a/packages/presentation/src/index.ts b/packages/presentation/src/index.ts index 62a913052dd..0a6f29a548e 100644 --- a/packages/presentation/src/index.ts +++ b/packages/presentation/src/index.ts @@ -69,3 +69,4 @@ export * from './preview' export * from './sound' export * from './stats' export * from './drawing' +export * from './link-preview' diff --git a/packages/presentation/src/link-preview.ts b/packages/presentation/src/link-preview.ts new file mode 100644 index 00000000000..6e4bde8c84c --- /dev/null +++ b/packages/presentation/src/link-preview.ts @@ -0,0 +1,57 @@ +// +// 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 { getMetadata } from '@hcengineering/platform' +import presentation from './plugin' + +export function isLinkPreviewEnabled (): boolean { + return getMetadata(presentation.metadata.LinkPreviewUrl) !== undefined +} +export interface LinkPreviewDetails { + title?: string + description?: string + url?: string + icon?: string + image?: string + charset?: string + hostname?: string + host?: string +} + +export function canDisplayLinkPreview (val: LinkPreviewDetails): boolean { + if (val.hostname === undefined && val.title === undefined) { + return false + } + if (val.image === undefined && val.description === undefined) { + return false + } + return true +} + +export async function fetchLinkPreviewDetails (url: string): Promise { + try { + const linkPreviewUrl = getMetadata(presentation.metadata.LinkPreviewUrl) + const response = await fetch(`${linkPreviewUrl}?q=${url}`, { + signal: AbortSignal.timeout(5 * 1000) + }) + if (!response.ok) { + throw new Error(`status: ${response.status}`) + } + console.log(response) + return response.json() as LinkPreviewDetails + } catch (error) { + console.error(`An error occurced on fetching or parsing data by ${url}, error:`, error) + return {} + } +} diff --git a/packages/presentation/src/plugin.ts b/packages/presentation/src/plugin.ts index 84c592159e4..7737e4a6649 100644 --- a/packages/presentation/src/plugin.ts +++ b/packages/presentation/src/plugin.ts @@ -144,6 +144,7 @@ export default plugin(presentationId, { Workspace: '' as Metadata, WorkspaceId: '' as Metadata, FrontUrl: '' as Asset, + LinkPreviewUrl: '' as Metadata, UploadConfig: '' as Metadata, PreviewConfig: '' as Metadata, ClientHook: '' as Metadata, diff --git a/plugins/attachment-resources/src/components/AttachmentDocList.svelte b/plugins/attachment-resources/src/components/AttachmentDocList.svelte index 335893196f1..4a557c43e40 100644 --- a/plugins/attachment-resources/src/components/AttachmentDocList.svelte +++ b/plugins/attachment-resources/src/components/AttachmentDocList.svelte @@ -14,7 +14,7 @@ --> diff --git a/plugins/attachment-resources/src/components/AttachmentList.svelte b/plugins/attachment-resources/src/components/AttachmentList.svelte index c71622a2a46..58d0e15eb74 100644 --- a/plugins/attachment-resources/src/components/AttachmentList.svelte +++ b/plugins/attachment-resources/src/components/AttachmentList.svelte @@ -29,12 +29,25 @@ {#if attachments.length} {#each attachments as attachment} + {#if attachment !== undefined && attachment.type !== 'application/link-preview'} + {/if} {/each} + {#each attachments as attachment} + {#if attachment !== undefined && attachment.type === 'application/link-preview'} +
+ + {/if} + {/each} {/if} diff --git a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte index de0c89ab3dc..759db03e31c 100644 --- a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte @@ -21,6 +21,7 @@ getBlobRef, getFileUrl, previewTypes, + fetchJson, sizeToWidth } from '@hcengineering/presentation' import { Label } from '@hcengineering/ui' @@ -28,8 +29,9 @@ import filesize from 'filesize' import { createEventDispatcher } from 'svelte' import { getType, openAttachmentInSidebar, showAttachmentPreviewPopup } from '../utils' - + import { type LinkPreviewDetails } from '@hcengineering/presentation' import AttachmentName from './AttachmentName.svelte' + import Spinner from '@hcengineering/ui/src/components/Spinner.svelte' export let value: WithLookup | undefined export let removable: boolean = false @@ -39,7 +41,7 @@ const dispatch = createEventDispatcher() - const maxLenght: number = 30 + const maxLenght: number = 20 const trimFilename = (fname: string): string => fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname @@ -83,6 +85,16 @@ await openAttachmentInSidebar(value) } } + async function fetchLinkPreviewDetails (): Promise { + if (value === undefined) { + return {} + } + try { + return await fetchJson(value.file, value.name) as LinkPreviewDetails + } catch { + return {} + } + } function middleClickHandler (e: MouseEvent): void { if (e.button !== 1) return @@ -106,7 +118,7 @@ {:else}
- {#if value} + {#if value !== undefined && value.type !== 'application/link-preview'} {#await getBlobRef(value.file, value.name, sizeToWidth('large')) then valueRef}
{/await} + {:else if value !== undefined && value.type === 'application/link-preview'} + {#await fetchLinkPreviewDetails() } + + {:then linkPreviewDetails } +
+ {#if linkPreviewDetails.icon} + link-preview + {:else} + URL + {/if} +
+
+ +
+ + {#if linkPreviewDetails.description} + {trimFilename(linkPreviewDetails.description)} + + {/if} + { + ev.stopPropagation() + ev.preventDefault() + dispatch('remove', value) + }} + > + + +
+
+ {/await} {/if} {/if} diff --git a/plugins/attachment-resources/src/components/AttachmentPreview.svelte b/plugins/attachment-resources/src/components/AttachmentPreview.svelte index 6a5984563e3..9da6222e308 100644 --- a/plugins/attachment-resources/src/components/AttachmentPreview.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPreview.svelte @@ -18,11 +18,11 @@ import { ListSelectionProvider } from '@hcengineering/view-resources' import { createEventDispatcher } from 'svelte' import { WithLookup } from '@hcengineering/core' - import { AttachmentImageSize } from '../types' import { getType, showAttachmentPreviewPopup } from '../utils' import AttachmentActions from './AttachmentActions.svelte' import AttachmentImagePreview from './AttachmentImagePreview.svelte' + import LinkPreviewPresenter from './LinkPreviewPresenter.svelte' import AttachmentPresenter from './AttachmentPresenter.svelte' import AttachmentVideoPreview from './AttachmentVideoPreview.svelte' import AudioPlayer from './AudioPlayer.svelte' @@ -37,9 +37,11 @@ const dispatch = createEventDispatcher() $: type = getType(value.type) - -{#if type === 'image'} + +{#if type === 'link-preview'} + +{:else if type === 'image'}
diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte index 74fa2b12349..89ebff5aa6d 100644 --- a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte +++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte @@ -15,7 +15,7 @@ diff --git a/plugins/attachment-resources/src/components/LinkPreviewAttachmentPresenter.svelte b/plugins/attachment-resources/src/components/LinkPreviewAttachmentPresenter.svelte new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/attachment-resources/src/components/LinkPreviewPresenter.svelte b/plugins/attachment-resources/src/components/LinkPreviewPresenter.svelte new file mode 100644 index 00000000000..1f4604fe077 --- /dev/null +++ b/plugins/attachment-resources/src/components/LinkPreviewPresenter.svelte @@ -0,0 +1,74 @@ + + +
+ {#await fetchViewModel()} +
+ +
+ {:then md } +
+
+ {#if md.icon} + link-preview-icon +   + {/if} + {md.hostname} +
+ {#if md.title !== md.hostname} + {md.title} +
+ {/if} + {#if md.description} + {md.description} + {/if} + {#if md.image} + link-preview + {/if} +
+ {/await} +
+ + diff --git a/plugins/attachment-resources/src/utils.ts b/plugins/attachment-resources/src/utils.ts index d48476e0ac4..1551db14618 100644 --- a/plugins/attachment-resources/src/utils.ts +++ b/plugins/attachment-resources/src/utils.ts @@ -89,7 +89,7 @@ export async function createAttachment ( } } -export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'audio' | 'pdf' | 'other' { +export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'audio' | 'pdf' | 'link-preview' | 'other' { if (type.startsWith('image/')) { return 'image' } @@ -99,7 +99,7 @@ export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'a if (type.startsWith('video/')) { return 'video' } - if (type.includes('application/pdf')) { + if (type === 'application/pdf') { return 'pdf' } if (type === 'application/json') { @@ -108,7 +108,9 @@ export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'a if (type.startsWith('text/')) { return 'text' } - + if (type === 'application/link-preview') { + return 'link-preview' + } return 'other' } diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessageHeader.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessageHeader.svelte index 4c82e871222..e6dbacd49f4 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessageHeader.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessageHeader.svelte @@ -15,8 +15,6 @@