diff --git a/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.test.tsx b/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.test.tsx index 19c4073f0ff..6276ebb2cfc 100644 --- a/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.test.tsx +++ b/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.test.tsx @@ -48,7 +48,11 @@ jest.mock('./FileLoader/FileLoader', () => ({ })); jest.mock('./ImageFileView/ImageFileView', () => ({ - ImageFileView: () =>
Image View
, + ImageFileView: ({src}: {src?: string}) => ( +
+ Image View +
+ ), })); jest.mock('./NoPreviewAvailable/NoPreviewAvailable', () => ({ @@ -206,6 +210,47 @@ describe('FileFullscreenModal - File Version Restore', () => { }); }); + describe('Image source selection', () => { + it('passes the original fileUrl to the image viewer for browser-previewable formats', () => { + render( + , + ); + + expect(screen.getByTestId('image-view')).toHaveAttribute('data-src', 'https://example.com/original.jpg'); + }); + + it('passes the server-generated preview to the image viewer for HEIC files (not browser-decodable)', () => { + render( + , + ); + + expect(screen.getByTestId('image-view')).toHaveAttribute('data-src', 'https://example.com/preview.jpg'); + }); + + it('falls back to filePreviewUrl when fileUrl is absent for a previewable format', () => { + render( + , + ); + + expect(screen.getByTestId('image-view')).toHaveAttribute('data-src', 'https://example.com/preview.jpg'); + }); + }); + describe('Content Refresh After Version Restore', () => { it('should render fresh content when component remounts', () => { const {rerender} = render(); diff --git a/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.tsx b/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.tsx index 2ba05afb488..7f2585f71ec 100644 --- a/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.tsx +++ b/apps/webapp/src/script/components/FileFullscreenModal/FileFullscreenModal.tsx @@ -19,10 +19,13 @@ import {useEffect, useState} from 'react'; +import {Maybe} from 'true-myth'; + import {PDFViewer} from 'Components/FileFullscreenModal/PdfViewer/PdfViewer'; import {FullscreenModal} from 'Components/FullscreenModal/FullscreenModal'; import {isFileEditable} from 'Util/FileTypeUtil'; import {getFileTypeFromExtension} from 'Util/getFileTypeFromExtension/getFileTypeFromExtension'; +import {getBestPreviewSource} from 'Util/ImageUtil'; import {getFileExtensionFromUrl} from 'Util/util'; import {FileEditor} from './FileEditor/FileEditor'; @@ -148,7 +151,13 @@ const ModalContent = ({ } if (type === 'image') { - return ; + const imageSrc = getBestPreviewSource({ + fileExtension, + fileUrl: Maybe.of(fileUrl), + filePreviewUrl: Maybe.of(filePreviewUrl), + }).unwrapOr(undefined); + + return ; } return ; diff --git a/apps/webapp/src/script/components/MessagesList/Message/ContentMessage/asset/MultipartAssets/ImageAssetCard/ImageAssetLarge/ImageAssetLarge.tsx b/apps/webapp/src/script/components/MessagesList/Message/ContentMessage/asset/MultipartAssets/ImageAssetCard/ImageAssetLarge/ImageAssetLarge.tsx index 0daab2ffc08..40fa4a45615 100644 --- a/apps/webapp/src/script/components/MessagesList/Message/ContentMessage/asset/MultipartAssets/ImageAssetCard/ImageAssetLarge/ImageAssetLarge.tsx +++ b/apps/webapp/src/script/components/MessagesList/Message/ContentMessage/asset/MultipartAssets/ImageAssetCard/ImageAssetLarge/ImageAssetLarge.tsx @@ -19,9 +19,12 @@ import {CSSProperties, useState} from 'react'; +import {Maybe} from 'true-myth'; + import {ICellAsset} from '@wireapp/protocol-messaging'; import {UnavailableFileIcon} from '@wireapp/react-ui-kit'; +import {getBestPreviewSource} from 'Util/ImageUtil'; import {t} from 'Util/LocalizerUtil'; import { @@ -67,6 +70,11 @@ export const ImageAssetLarge = ({ const aspectRatio = metadata?.width && metadata?.height ? metadata?.width / metadata?.height : undefined; const opacity = isLoaded ? 1 : 0; const isUnavailable = isError || hasLoadError; + const displaySrc = getBestPreviewSource({ + fileExtension: extension, + fileUrl: Maybe.of(fileUrl), + filePreviewUrl: Maybe.of(filePreviewUrl), + }).unwrapOr(undefined); return ( <> @@ -100,7 +108,7 @@ export const ImageAssetLarge = ({
{ expect(isPreviewableImage({})).toBe(false); }); }); + + describe('getBestPreviewSource', () => { + it('returns the original fileUrl for browser-previewable formats (JPEG, PNG, WebP, GIF)', () => { + expect(getBestPreviewSource({fileExtension: 'jpg', fileUrl: Maybe.of('original.jpg'), filePreviewUrl: Maybe.of('preview.jpg')})).toStrictEqual(Maybe.just('original.jpg')); + expect(getBestPreviewSource({fileExtension: 'png', fileUrl: Maybe.of('original.png'), filePreviewUrl: Maybe.of('preview.jpg')})).toStrictEqual(Maybe.just('original.png')); + expect(getBestPreviewSource({fileExtension: 'webp', fileUrl: Maybe.of('original.webp'), filePreviewUrl: Maybe.of('preview.jpg')})).toStrictEqual(Maybe.just('original.webp')); + expect(getBestPreviewSource({fileExtension: 'gif', fileUrl: Maybe.of('original.gif'), filePreviewUrl: Maybe.of('preview.jpg')})).toStrictEqual(Maybe.just('original.gif')); + }); + + it('returns the server-generated preview for HEIC files (not natively decodable by browsers)', () => { + expect(getBestPreviewSource({fileExtension: 'heic', fileUrl: Maybe.of('original.heic'), filePreviewUrl: Maybe.of('preview.jpg')})).toStrictEqual(Maybe.just('preview.jpg')); + }); + + it('falls back to filePreviewUrl when fileUrl is absent for a previewable format', () => { + expect(getBestPreviewSource({fileExtension: 'jpg', fileUrl: Maybe.nothing(), filePreviewUrl: Maybe.of('preview.jpg')})).toStrictEqual(Maybe.just('preview.jpg')); + }); + + it('returns Nothing when both fileUrl and filePreviewUrl are absent', () => { + expect(getBestPreviewSource({fileExtension: 'jpg', fileUrl: Maybe.nothing(), filePreviewUrl: Maybe.nothing()})).toStrictEqual(Maybe.nothing()); + }); + + it('can be safely unwrapped to undefined for UI src props when no source exists', () => { + const source = getBestPreviewSource({ + fileExtension: 'jpg', + fileUrl: Maybe.nothing(), + filePreviewUrl: Maybe.nothing(), + }).unwrapOr(undefined); + + expect(source).toBeUndefined(); + }); + + it('can be safely unwrapped to a string for UI src props when a source exists', () => { + const source = getBestPreviewSource({ + fileExtension: 'jpg', + fileUrl: Maybe.of('original.jpg'), + filePreviewUrl: Maybe.nothing(), + }).unwrapOr(undefined); + + expect(source).toBe('original.jpg'); + }); + }); }); diff --git a/apps/webapp/src/script/util/ImageUtil.ts b/apps/webapp/src/script/util/ImageUtil.ts index e7d3cfbf271..2a960b71a44 100644 --- a/apps/webapp/src/script/util/ImageUtil.ts +++ b/apps/webapp/src/script/util/ImageUtil.ts @@ -17,6 +17,8 @@ * */ +import {Maybe} from 'true-myth'; + export const stripImageExifData = async (image: Blob): Promise => { const url = URL.createObjectURL(image); try { @@ -179,3 +181,19 @@ export const isPreviewableImage = ({ return !!normalizedExtension && PREVIEWABLE_IMAGE_EXTENSIONS.has(normalizedExtension); }; + +type GetBestPreviewSourceOptions = { + readonly fileExtension: string; + readonly fileUrl: Maybe; + readonly filePreviewUrl: Maybe; +}; + +export function getBestPreviewSource(options: GetBestPreviewSourceOptions): Maybe { + const {fileExtension, fileUrl, filePreviewUrl} = options; + + if (!isPreviewableImage({extension: fileExtension})) { + return filePreviewUrl; + } + + return fileUrl.or(filePreviewUrl); +}