UBERF-5595: set up attachments sizes (#4746)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-02-23 21:03:59 +04:00 committed by GitHub
parent 0349cec3a2
commit 3e782658c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 485 additions and 131 deletions

View File

@ -20609,12 +20609,13 @@ packages:
dev: false
file:projects/presentation.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-aIs1NWqMRjF8+bpEPUJW3nkQFkxnv0GvllOQk4DWbDdfxuAlGkvBKiKaDwlx3cku9hulZqpwr6EyIbkotn1Rsw==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-afwb+Kuc6Gu/8xgzxYSMNmMewCvsAX46bAkIMVDBO71momZ1zBKAhLM1kFXIp1UnaOKH2vnq3myx4ZpOrjBAZw==, tarball: file:projects/presentation.tgz}
id: file:projects/presentation.tgz
name: '@rush-temp/presentation'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@types/png-chunks-extract': 1.0.2
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
eslint: 8.56.0
@ -20625,6 +20626,7 @@ packages:
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.11)(ts-node@10.9.2)
fast-equals: 2.0.4
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
png-chunks-extract: 1.0.0
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
sass: 1.71.1
@ -23328,7 +23330,7 @@ packages:
dev: false
file:projects/text-editor.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(prosemirror-model@1.19.4)(ts-node@10.9.2):
resolution: {integrity: sha512-/TK8U02uYlCsPKiYVNS7t+UMmDxEPWLwY05soaV+Yuu3nXRGt2m2CkEFnv8I5IAqsEzgogKhqFXAgcQHcjfG+A==, tarball: file:projects/text-editor.tgz}
resolution: {integrity: sha512-obPE9MHV6S63InShtdrvDbNa5/A9BgF49k6zgO3T4C+niVTKsmsKz4/Pt+aew2X5eAAxehSq69bWIjBZidydNA==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor'
version: 0.0.0

View File

@ -14,7 +14,7 @@
//
import activity from '@hcengineering/activity'
import type { Attachment, Photo, SavedAttachments } from '@hcengineering/attachment'
import type { Attachment, AttachmentMetadata, Photo, SavedAttachments } from '@hcengineering/attachment'
import { type Domain, IndexKind, type Ref } from '@hcengineering/core'
import {
type Builder,
@ -64,6 +64,8 @@ export class TAttachment extends TAttachedDoc implements Attachment {
@Prop(TypeBoolean(), attachment.string.Pinned)
pinned!: boolean
metadata?: AttachmentMetadata
}
@Model(attachment.class.Photo, attachment.class.Attachment)

View File

@ -35,7 +35,8 @@
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
"svelte-eslint-parser": "^0.33.1",
"@types/png-chunks-extract": "^1.0.2"
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
@ -46,6 +47,7 @@
"svelte": "^4.2.5",
"@hcengineering/client": "^0.6.14",
"@hcengineering/collaborator-client": "^0.6.0",
"fast-equals": "^2.0.3"
"fast-equals": "^2.0.3",
"png-chunks-extract": "^1.0.0"
}
}

View File

@ -0,0 +1,101 @@
//
// Copyright © 2024 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 extract from 'png-chunks-extract'
export async function getImageSize (
file: File,
src: string
): Promise<{ width: number, height: number, pixelRatio: number }> {
const size = isPng(file) ? await getPngImageSize(file) : undefined
const promise = new Promise<{ width: number, height: number, pixelRatio: number }>((resolve, reject) => {
const img = new Image()
img.onload = () => {
resolve({
width: size?.width ?? img.naturalWidth,
height: size?.height ?? img.naturalHeight,
pixelRatio: size?.pixelRatio ?? 1
})
}
img.onerror = reject
img.src = src
})
return await promise
}
function isPng (file: File): boolean {
return file.type === 'image/png'
}
async function getPngImageSize (file: File): Promise<{ width: number, height: number, pixelRatio: number } | undefined> {
if (!isPng(file)) {
return undefined
}
try {
const buffer = await file.arrayBuffer()
const chunks = extract(new Uint8Array(buffer))
const pHYsChunk = chunks.find((chunk) => chunk.name === 'pHYs')
const iHDRChunk = chunks.find((chunk) => chunk.name === 'IHDR')
if (pHYsChunk === undefined || iHDRChunk === undefined) {
return undefined
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// Section 4.1.1. IHDR Image header
// Section 4.2.4.2. pHYs Physical pixel dimensions
const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer))
const physData = parsePhys(new DataView(pHYsChunk.data.buffer))
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
const pixelRatio = Math.round(physData.ppux / 2834.5)
return {
width: idhrData.width,
height: idhrData.height,
pixelRatio
}
}
} catch (err) {
console.error(err)
return undefined
}
return undefined
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// Section 4.1.1. IHDR Image header
function parseIHDR (view: DataView): { width: number, height: number } {
return {
width: view.getUint32(0),
height: view.getUint32(4)
}
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// Section 4.2.4.2. pHYs Physical pixel dimensions
function parsePhys (view: DataView): { ppux: number, ppuy: number, unit: number } {
return {
ppux: view.getUint32(0),
ppuy: view.getUint32(4),
unit: view.getUint8(4)
}
}

View File

@ -56,3 +56,4 @@ export * from './pipeline'
export * from './components/extensions/manager'
export * from './rules'
export * from './search'
export * from './image'

View File

@ -34,8 +34,7 @@
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1",
"@types/png-chunks-extract": "^1.0.2"
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/presentation": "^0.6.2",
@ -80,7 +79,6 @@
"rfc6902": "^5.0.1",
"diff": "^5.1.0",
"slugify": "^1.6.6",
"lib0": "^0.2.88",
"png-chunks-extract": "^1.0.0"
"lib0": "^0.2.88"
}
}

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { PDFViewer } from '@hcengineering/presentation'
import { PDFViewer, getImageSize } from '@hcengineering/presentation'
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui'
import { mergeAttributes, nodeInputRule } from '@tiptap/core'
import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { type EditorView } from '@tiptap/pm/view'
import extract from 'png-chunks-extract'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
/**
* @public
@ -318,76 +318,27 @@ async function handleImageUpload (
attachFile: FileAttachFunction,
uploadUrl: string
): Promise<void> {
const size = await getImageSize(file)
const attached = await attachFile(file)
if (attached !== undefined) {
if (attached.type.includes('image')) {
const image = new Image()
image.onload = () => {
const node = view.state.schema.nodes.image.create({
'file-id': attached.file,
width: size?.width ?? image.naturalWidth
})
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
view.dispatch(transaction)
}
image.src = getFileUrl(attached.file, 'full', uploadUrl)
}
}
}
async function getImageSize (file: File): Promise<{ width: number, height: number } | undefined> {
if (file.type !== 'image/png') {
return undefined
if (attached === undefined) {
return
}
if (!attached.type.includes('image')) {
return
}
try {
const buffer = await file.arrayBuffer()
const chunks = extract(new Uint8Array(buffer))
const size = await getImageSize(file, getFileUrl(attached.file, 'full', uploadUrl))
const node = view.state.schema.nodes.image.create({
'file-id': attached.file,
width: Math.round(size.width / size.pixelRatio)
})
const pHYsChunk = chunks.find((chunk) => chunk.name === 'pHYs')
const iHDRChunk = chunks.find((chunk) => chunk.name === 'IHDR')
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
if (pHYsChunk === undefined || iHDRChunk === undefined) {
return undefined
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// Section 4.1.1. IHDR Image header
// Section 4.2.4.2. pHYs Physical pixel dimensions
const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer))
const physData = parsePhys(new DataView(pHYsChunk.data.buffer))
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
const pixelRatio = Math.round(physData.ppux / 2834.5)
return {
width: Math.round(idhrData.width / pixelRatio),
height: Math.round(idhrData.height / pixelRatio)
}
}
} catch (err) {
console.error(err)
return undefined
}
return undefined
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// Section 4.1.1. IHDR Image header
function parseIHDR (view: DataView): { width: number, height: number } {
return {
width: view.getUint32(0),
height: view.getUint32(4)
}
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// Section 4.2.4.2. pHYs Physical pixel dimensions
function parsePhys (view: DataView): { ppux: number, ppuy: number, unit: number } {
return {
ppux: view.getUint32(0),
ppuy: view.getUint32(4),
unit: view.getUint8(4)
view.dispatch(transaction)
} catch (e) {
void setPlatformStatus(unknownError(e))
}
}

View File

@ -35,6 +35,7 @@
export let hoverable = true
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let withShowMore: boolean = true
export let attachmentImageSize: 'x-large' | undefined = undefined
export let showLinksPreview = true
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
@ -65,6 +66,7 @@
hoverable,
hoverStyles,
withShowMore,
attachmentImageSize,
showLinksPreview,
onClick,
onReply

View File

@ -19,9 +19,11 @@
import attachment from '../plugin'
import AttachmentList from './AttachmentList.svelte'
import { AttachmentImageSize } from '../types'
export let value: Doc & { attachments?: number }
export let attachments: Attachment[] | undefined = undefined
export let imageSize: AttachmentImageSize = 'auto'
const query = createQuery()
const savedAttachmentsQuery = createQuery()
@ -57,4 +59,4 @@
})
</script>
<AttachmentList attachments={resAttachments} {savedAttachmentsIds} />
<AttachmentList attachments={resAttachments} {savedAttachmentsIds} {imageSize} />

View File

@ -0,0 +1,128 @@
<!--
// Copyright © 2024 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.
-->
<script lang="ts">
import { getIconSize2x, IconSize } from '@hcengineering/ui'
import { getFileUrl } from '@hcengineering/presentation'
import type { Attachment } from '@hcengineering/attachment'
import { AttachmentImageSize } from '../types'
export let value: Attachment
export let size: AttachmentImageSize = 'auto'
interface Dimensions {
width: 'auto' | number
height: 'auto' | number
}
const minSizeRem = 4
const maxSizeRem = 20
const preferredWidthMap = {
'x-large': 300
} as const
let dimensions: Dimensions
let urlSize: IconSize
$: dimensions = getDimensions(value, size)
$: urlSize = getUrlSize(size)
function getDimensions (value: Attachment, size: AttachmentImageSize): Dimensions {
if (size === 'auto') {
return {
width: 'auto',
height: 'auto'
}
}
const preferredWidth = preferredWidthMap[size]
const { metadata } = value
if (!metadata) {
return {
width: preferredWidth,
height: preferredWidth
}
}
const { originalWidth, originalHeight } = metadata
const maxSize = maxSizeRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
const width = Math.min(originalWidth, preferredWidth)
const ratio = originalHeight / originalWidth
const height = width * ratio
if (height > maxSize) {
return {
width: maxSize / ratio,
height: maxSize
}
}
return {
width,
height
}
}
function getObjectFit (size: Dimensions): 'contain' | 'cover' {
if (size.width === 'auto' || size.height === 'auto') {
return 'contain'
}
const minSize = minSizeRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
if (size.width < minSize || size.height < minSize) {
return 'cover'
}
return 'contain'
}
function getUrlSize (size: AttachmentImageSize): IconSize {
if (size === 'auto') {
return 'large'
}
return 'x-large'
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<img
src={getFileUrl(value.file, urlSize)}
style:object-fit={getObjectFit(dimensions)}
width={dimensions.width}
height={dimensions.height}
srcset={`${getFileUrl(value.file, urlSize, value.name)} 1x, ${getFileUrl(
value.file,
getIconSize2x(urlSize),
value.name
)} 2x`}
alt={value.name}
/>
<style lang="scss">
img {
max-width: 20rem;
max-height: 20rem;
border-radius: 0.75rem;
object-fit: contain;
min-height: 4rem;
min-width: 4rem;
}
</style>

View File

@ -16,16 +16,23 @@
import { Attachment } from '@hcengineering/attachment'
import { Ref } from '@hcengineering/core'
import { Scroller } from '@hcengineering/ui'
import AttachmentPreview from './AttachmentPreview.svelte'
import { AttachmentImageSize } from '../types'
export let attachments: Attachment[] = []
export let savedAttachmentsIds: Ref<Attachment>[] = []
export let imageSize: AttachmentImageSize | undefined = undefined
</script>
{#if attachments.length}
<Scroller contentDirection={'horizontal'} horizontal gap={'gap-3'}>
{#each attachments as attachment}
<AttachmentPreview value={attachment} isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false} />
<AttachmentPreview
value={attachment}
isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false}
{imageSize}
/>
{/each}
</Scroller>
{/if}

View File

@ -18,9 +18,11 @@
import { Attachment } from '@hcengineering/attachment'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ActionIcon, IconAdd, Label, Loading } from '@hcengineering/ui'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { AttachmentPresenter } from '..'
import attachment from '../plugin'
import { uploadFile } from '../utils'
import { getAttachmentMetadata, uploadFile } from '../utils'
// export let attachments: number
export let object: Doc
@ -51,14 +53,21 @@
}
async function createAttachment (file: File) {
const uuid = await uploadFile(file)
await client.addCollection(attachment.class.Attachment, object.space, object._id, object._class, 'attachments', {
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
})
try {
const uuid = await uploadFile(file)
const metadata = await getAttachmentMetadata(file, uuid)
await client.addCollection(attachment.class.Attachment, object.space, object._id, object._class, 'attachments', {
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified,
metadata
})
} catch (e) {
void setPlatformStatus(unknownError(e))
}
}
async function fileSelected (): Promise<void> {

View File

@ -15,19 +15,25 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { getFileUrl, PDFViewer } from '@hcengineering/presentation'
import { showPopup, closeTooltip, getIconSize2x } from '@hcengineering/ui'
import { PDFViewer } from '@hcengineering/presentation'
import { showPopup, closeTooltip } from '@hcengineering/ui'
import { ListSelectionProvider } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { getType } from '../utils'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import AttachmentActions from './AttachmentActions.svelte'
import AudioPlayer from './AudioPlayer.svelte'
import { ListSelectionProvider } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { AttachmentImageSize } from '../types'
import AttachmentImagePreview from './AttachmentImagePreview.svelte'
import AttachmentVideoPreview from './AttachmentVideoPreview.svelte'
export let value: Attachment
export let isSaved: boolean = false
export let listProvider: ListSelectionProvider | undefined = undefined
export let imageSize: AttachmentImageSize = 'auto'
export let removable: boolean = false
const dispatch = createEventDispatcher()
$: type = getType(value.type)
@ -49,15 +55,7 @@
dispatch('open', popupInfo.id)
}}
>
<img
src={getFileUrl(value.file, 'large')}
srcset={`${getFileUrl(value.file, 'large', value.name)} 1x, ${getFileUrl(
value.file,
getIconSize2x('large'),
value.name
)} 2x`}
alt={value.name}
/>
<AttachmentImagePreview {value} size={imageSize} />
<div class="actions conner">
<AttachmentActions attachment={value} {isSaved} {removable} />
</div>
@ -71,13 +69,7 @@
</div>
{:else if type === 'video'}
<div class="content buttonContainer flex-center">
<video controls>
<source src={getFileUrl(value.file, 'full', value.name)} />
<track kind="captions" label={value.name} />
<div class="container">
<AttachmentPresenter {value} />
</div>
</video>
<AttachmentVideoPreview {value} />
<div class="actions conner">
<AttachmentActions attachment={value} {isSaved} {removable} />
</div>
@ -129,17 +121,5 @@
.content {
max-width: 20rem;
max-height: 20rem;
img,
video {
max-width: 20rem;
max-height: 20rem;
border-radius: 0.75rem;
width: auto;
height: auto;
}
img {
object-fit: contain;
}
}
</style>

View File

@ -17,10 +17,17 @@
import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import {
createQuery,
DraftController,
draftsStore,
getClient,
getFileUrl,
getImageSize
} from '@hcengineering/presentation'
import textEditor, { AttachIcon, type RefAction, ReferenceInput } from '@hcengineering/text-editor'
import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
import { deleteFile, uploadFile } from '../utils'
import { deleteFile, getAttachmentMetadata, uploadFile } from '../utils'
import attachment from '../plugin'
import AttachmentPresenter from './AttachmentPresenter.svelte'
@ -103,6 +110,8 @@
async function createAttachment (file: File) {
try {
const uuid = await uploadFile(file)
const metadata = await getAttachmentMetadata(file, uuid)
const _id: Ref<Attachment> = generateId()
attachments.set(_id, {
_id,
@ -117,7 +126,8 @@
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
lastModified: file.lastModified,
metadata
})
newAttachments.add(_id)
attachments = attachments

View File

@ -20,8 +20,9 @@
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import textEditor, { AttachIcon, type RefAction, StyledTextBox } from '@hcengineering/text-editor'
import { ButtonSize } from '@hcengineering/ui'
import attachment from '../plugin'
import { deleteFile, uploadFile } from '../utils'
import { deleteFile, getAttachmentMetadata, uploadFile } from '../utils'
import AttachmentsGrid from './AttachmentsGrid.svelte'
export let objectId: Ref<Doc> | undefined = undefined
@ -133,7 +134,9 @@
if (space === undefined || objectId === undefined || _class === undefined) return
try {
const uuid = await uploadFile(file)
const metadata = await getAttachmentMetadata(file, uuid)
const _id: Ref<Attachment> = generateId()
attachments.set(_id, {
_id,
_class: attachment.class.Attachment,
@ -147,7 +150,8 @@
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
lastModified: file.lastModified,
metadata
})
newAttachments.add(_id)
attachments = attachments

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2024 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.
-->
<script lang="ts">
import { getFileUrl } from '@hcengineering/presentation'
import type { Attachment } from '@hcengineering/attachment'
import AttachmentPresenter from './AttachmentPresenter.svelte'
export let value: Attachment
const maxSizeRem = 20
const baseSizeRem = 12
const minSizeRem = 4
$: dimensions = getDimensions(value)
function getDimensions (value: Attachment): { width: number, height: number } {
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
if (!value.metadata) {
const baseSize = baseSizeRem * fontSize
return { width: baseSize, height: baseSize }
}
const { originalWidth, originalHeight } = value.metadata
const maxSize = maxSizeRem * fontSize
// For mp4 audio files, we don't have originalWidth, originalHeight
if (originalWidth === 0 || originalHeight === 0) {
return { width: maxSize, height: minSizeRem * fontSize }
}
const ratio = originalHeight / originalWidth
const width = Math.min(maxSize, originalWidth)
const height = width * ratio
if (height > maxSize) {
return { width: maxSize / ratio, height: maxSize }
}
return { width, height }
}
</script>
<video controls width={dimensions.width} height={dimensions.height}>
<source src={getFileUrl(value.file, 'full', value.name)} />
<track kind="captions" label={value.name} />
<div class="container">
<AttachmentPresenter {value} />
</div>
</video>
<style lang="scss">
video {
max-width: 20rem;
max-height: 20rem;
min-width: 4rem;
min-height: 4rem;
border-radius: 0.75rem;
}
</style>

View File

@ -43,6 +43,8 @@ import AttachmentPreview from './components/AttachmentPreview.svelte'
import AttachmentsUpdatedMessage from './components/activity/AttachmentsUpdatedMessage.svelte'
import { deleteFile, uploadFile } from './utils'
export * from './types'
export {
AddAttachment,
AttachmentDroppable,

View File

@ -0,0 +1 @@
export type AttachmentImageSize = 'x-large' | 'auto'

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import { type Attachment } from '@hcengineering/attachment'
import { type Attachment, type AttachmentMetadata } from '@hcengineering/attachment'
import {
type Class,
concatLink,
@ -24,7 +24,7 @@ import {
type Space,
type TxOperations as Client
} from '@hcengineering/core'
import presentation from '@hcengineering/presentation'
import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation'
import { PlatformError, Severity, Status, getMetadata, setPlatformStatus, unknownError } from '@hcengineering/platform'
import attachment from './plugin'
@ -87,13 +87,16 @@ export async function createAttachments (
const file = list.item(index)
if (file !== null) {
const uuid = await uploadFile(file)
const metadata = await getAttachmentMetadata(file, uuid)
await client.addCollection(attachmentClass, space, objectId, objectClass, 'attachments', {
...extraData,
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
lastModified: file.lastModified,
metadata
})
}
}
@ -118,3 +121,50 @@ export function getType (type: string): 'image' | 'video' | 'audio' | 'pdf' | 'o
return 'other'
}
export async function getAttachmentMetadata (file: File, uuid: string): Promise<AttachmentMetadata | undefined> {
const type = getType(file.type)
if (type === 'video') {
const size = await getVideoSize(uuid)
if (size === undefined) {
return undefined
}
return {
originalHeight: size.height,
originalWidth: size.width
}
}
if (type === 'image') {
const size = await getImageSize(file, getFileUrl(uuid, 'full'))
return {
originalHeight: size.height,
originalWidth: size.width,
pixelRatio: size.pixelRatio
}
}
return undefined
}
async function getVideoSize (uuid: string): Promise<{ width: number, height: number } | undefined> {
const promise = new Promise<{ width: number, height: number }>((resolve, reject) => {
const element = document.createElement('video')
element.onloadedmetadata = () => {
const height = element.videoHeight
const width = element.videoWidth
resolve({ height, width })
}
element.onerror = reject
element.src = getFileUrl(uuid, 'full')
})
return await promise
}

View File

@ -33,6 +33,30 @@ export interface Attachment extends AttachedDoc {
pinned?: boolean // If defined and true, will be shown in top of attachments collection
readonly?: boolean // If readonly, user will not be able to remove or modify this attachment
metadata?: AttachmentMetadata
}
/**
* @public
*/
export type AttachmentMetadata = ImageMetadata | VideoMetadata
/**
* @public
*/
export interface ImageMetadata {
originalWidth: number
originalHeight: number
pixelRatio: number
}
/**
* @public
*/
export interface VideoMetadata {
originalWidth: number
originalHeight: number
}
/**

View File

@ -512,6 +512,7 @@
isHighlighted={isSelected}
shouldScroll={isSelected}
withShowMore={false}
attachmentImageSize="x-large"
showLinksPreview={false}
/>
</div>

View File

@ -15,10 +15,9 @@
<script lang="ts">
import { Person, PersonAccount } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import { Account, Class, Doc, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import core, { Account, Class, Doc, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import core from '@hcengineering/core/src/component'
import { AttachmentDocList } from '@hcengineering/attachment-resources'
import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources'
import { getDocLinkTitle, LinkPresenter } from '@hcengineering/view-resources'
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
import view from '@hcengineering/view'
@ -47,6 +46,7 @@
export let inline = false
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let withShowMore: boolean = true
export let attachmentImageSize: AttachmentImageSize = 'auto'
export let showLinksPreview = true
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
@ -193,7 +193,7 @@
<ShowMore>
<div class="clear-mins">
<MessageViewer message={value.message} />
<AttachmentDocList {value} {attachments} />
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} />
{#each links as link}
<LinkPresenter {link} />
{/each}
@ -202,7 +202,7 @@
{:else}
<div class="clear-mins">
<MessageViewer message={value.message} />
<AttachmentDocList {value} {attachments} />
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} />
{#each links as link}
<LinkPresenter {link} />
{/each}

View File

@ -18,6 +18,7 @@
import { getDocLinkTitle } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import activity from '@hcengineering/activity'
import { AttachmentImageSize } from '@hcengineering/attachment-resources'
import chunter from '../../plugin'
import ChatMessagePresenter from '../chat-message/ChatMessagePresenter.svelte'
@ -38,6 +39,7 @@
export let inline = false
export let withShowMore: boolean = true
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let attachmentImageSize: AttachmentImageSize = 'x-large'
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
@ -73,6 +75,7 @@
{hoverable}
{hoverStyles}
{withShowMore}
{attachmentImageSize}
showLinksPreview={false}
{onClick}
{onReply}