diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index 869cd0dd7da..3ffc48f8699 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/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 23e83112ee0..cfee6d53965 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -1,5 +1,5 @@ // -// Copyright © 2022, 2023 Hardcore Engineering Inc. +// Copyright © 2022, 2023, 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 @@ -157,6 +157,7 @@ export interface Config { TELEGRAM_BOT_URL?: string AI_URL?:string DISABLE_SIGNUP?: string + LINK_PREVIEW_URL?: string // Could be defined for dev environment FRONT_URL?: string PREVIEW_CONFIG?: string @@ -314,7 +315,7 @@ export async function configurePlatform() { setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL)) setMetadata(presentation.metadata.StatsUrl, config.STATS_URL) - + setMetadata(presentation.metadata.LinkPreviewUrl, config.LINK_PREVIEW_URL) setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR) if (config.MODEL_VERSION != null) { @@ -340,7 +341,7 @@ export async function configurePlatform() { setMetadata(rekoni.metadata.RekoniUrl, config.REKONI_URL) setMetadata(uiPlugin.metadata.DefaultApplication, login.component.LoginApp) - 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/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 b526378f2b5..c5beacb19a2 100644 --- a/packages/presentation/src/file.ts +++ b/packages/presentation/src/file.ts @@ -284,3 +284,13 @@ async function uploadFileWithSignedUrl (file: File, uuid: string, uploadUrl: str }) } } + +export async function getJsonOrEmpty (file: string, name: string): Promise { + try { + const fileUrl = getFileUrl(file, name) + const resp = await fetch(fileUrl) + return await resp.json() + } catch { + return {} + } +} 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..580f18ae911 --- /dev/null +++ b/packages/presentation/src/link-preview.ts @@ -0,0 +1,60 @@ +// +// 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 plugin from './plugin' + +export function isLinkPreviewEnabled (): boolean { + return getMetadata(plugin.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) { + return false + } + if (val.image === undefined && val.description === undefined) { + return false + } + if (val.title === undefined && val.description === undefined) { + return false + } + return true +} + +export async function fetchLinkPreviewDetails (url: string, timeoutMs = 15000): Promise { + try { + const linkPreviewUrl = getMetadata(plugin.metadata.LinkPreviewUrl) + let token: string = '' + if (getMetadata(plugin.metadata.Token) !== undefined) { + token = getMetadata(plugin.metadata.Token) as string + } + const response = await fetch(`${linkPreviewUrl}/details?q=${url}`, { + headers: { Authorization: 'Bearer ' + token }, + signal: AbortSignal.timeout(timeoutMs) + }) + return response.json() as LinkPreviewDetails + } catch { + 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..474372c8bc7 100644 --- a/plugins/attachment-resources/src/components/AttachmentDocList.svelte +++ b/plugins/attachment-resources/src/components/AttachmentDocList.svelte @@ -14,12 +14,12 @@ --> - + diff --git a/plugins/attachment-resources/src/components/AttachmentGalleryPresenter.svelte b/plugins/attachment-resources/src/components/AttachmentGalleryPresenter.svelte index c12836f6928..666e4d3767c 100644 --- a/plugins/attachment-resources/src/components/AttachmentGalleryPresenter.svelte +++ b/plugins/attachment-resources/src/components/AttachmentGalleryPresenter.svelte @@ -42,6 +42,7 @@ function openAttachment (): void { showAttachmentPreviewPopup(value) } + $: src = getFileUrl(value.file, value.name) diff --git a/plugins/attachment-resources/src/components/AttachmentGroup.svelte b/plugins/attachment-resources/src/components/AttachmentGroup.svelte new file mode 100644 index 00000000000..96d4295239f --- /dev/null +++ b/plugins/attachment-resources/src/components/AttachmentGroup.svelte @@ -0,0 +1,51 @@ + + + + +
+ + +
diff --git a/plugins/attachment-resources/src/components/AttachmentList.svelte b/plugins/attachment-resources/src/components/AttachmentList.svelte index c71622a2a46..84ac7baaf2b 100644 --- a/plugins/attachment-resources/src/components/AttachmentList.svelte +++ b/plugins/attachment-resources/src/components/AttachmentList.svelte @@ -16,7 +16,6 @@ import { Attachment } from '@hcengineering/attachment' import { Ref, type WithLookup } from '@hcengineering/core' import { Scroller } from '@hcengineering/ui' - import { AttachmentImageSize } from '../types' import AttachmentPreview from './AttachmentPreview.svelte' diff --git a/plugins/attachment-resources/src/components/AttachmentPopup.svelte b/plugins/attachment-resources/src/components/AttachmentPopup.svelte index a37f5ebd205..41b11006cfb 100644 --- a/plugins/attachment-resources/src/components/AttachmentPopup.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPopup.svelte @@ -19,7 +19,7 @@ import { ActionIcon, IconAdd, Label, Loading } from '@hcengineering/ui' import type { Doc, WithLookup } from '@hcengineering/core' - import core from '@hcengineering/core' + import { setPlatformStatus, unknownError } from '@hcengineering/platform' import { AttachmentPresenter } from '..' import attachment from '../plugin' diff --git a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte index b199fddf5c3..0b026730661 100644 --- a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte @@ -21,14 +21,15 @@ getBlobRef, getFileUrl, previewTypes, + getJsonOrEmpty, sizeToWidth } from '@hcengineering/presentation' - import { Label } from '@hcengineering/ui' + import { Label, Spinner } from '@hcengineering/ui' import { permissionsStore } from '@hcengineering/view-resources' + import WebIcon from './icons/Web.svelte' import filesize from 'filesize' import { createEventDispatcher } from 'svelte' import { getType, openAttachmentInSidebar, showAttachmentPreviewPopup } from '../utils' - import AttachmentName from './AttachmentName.svelte' export let value: WithLookup | undefined @@ -38,9 +39,10 @@ const dispatch = createEventDispatcher() - const maxLenght: number = 30 + const maxLength: number = 30 + const trimFilename = (fname: string): string => - fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname + fname.length > maxLength ? fname.substr(0, (maxLength - 1) / 2) + '...' + fname.substr(-(maxLength - 1) / 2) : fname $: canRemove = removable && @@ -58,7 +60,10 @@ return getType(contentType) === 'image' } - let canPreview: boolean = false + let canPreview = false + let useDefaultIcon = false + const canLinkPreview = value?.type.includes('link-preview') ?? false + $: if (value !== undefined) { void canPreviewFile(value.type, $previewTypes).then((res) => { canPreview = res @@ -106,49 +111,36 @@ {:else}
{#if value} - {#await getBlobRef(value.file, value.name, sizeToWidth('large')) then valueRef} - - {#if showPreview && isImage(value.type)} - {value.name} - {:else} -
- {iconLabel(value.name)} -
- {/if} -
-
-
- - {trimFilename(value.name)} - + {#if canLinkPreview} + {#await getJsonOrEmpty(value.file, value.name)} + + {:then linkPreviewDetails} +
+ {#if linkPreviewDetails.icon !== undefined && !useDefaultIcon} + link-preview { + useDefaultIcon = true + }} + /> + {:else} + + {/if}
-
- {filesize(value.size, { spacer: '' })} - - - - - {#if canRemove} - - - +
+ +
+ + {#if linkPreviewDetails.description} + {trimFilename(linkPreviewDetails.description)} + + {/if} { @@ -159,16 +151,79 @@ > - {/if} - + +
-
- {/await} + {/await} + {:else} + {#await getBlobRef(value.file, value.name, sizeToWidth('large')) then valueRef} + + {#if showPreview && isImage(value.type)} + {value.name} + {:else} +
+ {iconLabel(value.name)} +
+ {/if} +
+
+ +
+ {filesize(value.size, { spacer: '' })} + + + + + {#if canRemove} + + + + { + ev.stopPropagation() + ev.preventDefault() + dispatch('remove', value) + }} + > + + {/if} + +
+
+ {/await} + {/if} {/if}
{/if} diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte index 74fa2b12349..06d19dacfb0 100644 --- a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte +++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte @@ -1,5 +1,5 @@ + + + +
+ {#each attachments as attachment} + + {/each} +
diff --git a/plugins/attachment-resources/src/components/LinkPreviewPresenter.svelte b/plugins/attachment-resources/src/components/LinkPreviewPresenter.svelte new file mode 100644 index 00000000000..6a24c3bee97 --- /dev/null +++ b/plugins/attachment-resources/src/components/LinkPreviewPresenter.svelte @@ -0,0 +1,95 @@ + + + +
+ {#if canDisplay} +
+
+ {#if viewModel.icon !== undefined && !useDefaultIcon} + link-preview-icon { + useDefaultIcon = true + }} + /> + {:else} + + {/if} + {viewModel.hostname} +
+
+
+ {#if viewModel.title?.toLowerCase() !== viewModel.hostname?.toLowerCase()} + {viewModel.title} + {/if} +
+
+ {#if viewModel.description} + {viewModel.description} + {/if} + {#if viewModel.image} + + link-preview + + {/if} +
+
+
+ {:else} +
+ +
+ {/if} +
+ + diff --git a/plugins/attachment-resources/src/components/icons/Web.svelte b/plugins/attachment-resources/src/components/icons/Web.svelte new file mode 100644 index 00000000000..56d73a2be4f --- /dev/null +++ b/plugins/attachment-resources/src/components/icons/Web.svelte @@ -0,0 +1,25 @@ + + + + + + + diff --git a/plugins/attachment-resources/src/index.ts b/plugins/attachment-resources/src/index.ts index 972ceaaf187..eecca099903 100644 --- a/plugins/attachment-resources/src/index.ts +++ b/plugins/attachment-resources/src/index.ts @@ -25,6 +25,7 @@ import AttachmentDocList from './components/AttachmentDocList.svelte' import AttachmentDroppable from './components/AttachmentDroppable.svelte' import AttachmentGalleryPresenter from './components/AttachmentGalleryPresenter.svelte' import AttachmentList from './components/AttachmentList.svelte' +import AttachmentGroup from './components/AttachmentGroup.svelte' import AttachmentPresenter from './components/AttachmentPresenter.svelte' import AttachmentPreview from './components/AttachmentPreview.svelte' import AttachmentRefInput from './components/AttachmentRefInput.svelte' @@ -53,6 +54,7 @@ export { AttachmentDroppable, AttachmentGalleryPresenter, AttachmentList, + AttachmentGroup, AttachmentPresenter, AttachmentPreview, AttachmentRefInput, diff --git a/plugins/attachment-resources/src/utils.ts b/plugins/attachment-resources/src/utils.ts index d48476e0ac4..147373c1038 100644 --- a/plugins/attachment-resources/src/utils.ts +++ b/plugins/attachment-resources/src/utils.ts @@ -89,7 +89,9 @@ 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' } @@ -102,13 +104,15 @@ export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'a if (type.includes('application/pdf')) { return 'pdf' } - if (type === 'application/json') { + if (type.includes('application/json')) { return 'json' } if (type.startsWith('text/')) { return 'text' } - + if (type.includes('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 @@ diff --git a/plugins/telegram-resources/src/components/Message.svelte b/plugins/telegram-resources/src/components/Message.svelte index 5e76c1e5296..146cfd207eb 100644 --- a/plugins/telegram-resources/src/components/Message.svelte +++ b/plugins/telegram-resources/src/components/Message.svelte @@ -15,7 +15,7 @@ -->