Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ jest.mock('./FileLoader/FileLoader', () => ({
}));

jest.mock('./ImageFileView/ImageFileView', () => ({
ImageFileView: () => <div data-uie-name="image-view">Image View</div>,
ImageFileView: ({src}: {src?: string}) => (
<div data-uie-name="image-view" data-src={src}>
Image View
</div>
),
}));

jest.mock('./NoPreviewAvailable/NoPreviewAvailable', () => ({
Expand Down Expand Up @@ -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(
<FileFullscreenModal
{...defaultProps}
filePreviewUrl="https://example.com/preview.jpg"
fileExtension="jpg"
fileUrl="https://example.com/original.jpg"
/>,
);

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(
<FileFullscreenModal
{...defaultProps}
filePreviewUrl="https://example.com/preview.jpg"
fileExtension="heic"
fileUrl="https://example.com/original.heic"
/>,
);

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(
<FileFullscreenModal
{...defaultProps}
filePreviewUrl="https://example.com/preview.jpg"
fileExtension="jpg"
fileUrl={undefined}
/>,
);

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(<FileFullscreenModal {...defaultProps} isEditMode />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -148,7 +151,13 @@ const ModalContent = ({
}

if (type === 'image') {
return <ImageFileView src={filePreviewUrl} senderName={senderName} timestamp={timestamp} />;
const imageSrc = getBestPreviewSource({
fileExtension,
fileUrl: Maybe.of(fileUrl),
filePreviewUrl: Maybe.of(filePreviewUrl),
}).unwrapOr(undefined);

return <ImageFileView src={imageSrc} senderName={senderName} timestamp={timestamp} />;
}

return <NoPreviewAvailable fileUrl={fileUrl} fileName={fileName} fileExtension={fileExtension} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (
<>
Expand Down Expand Up @@ -100,7 +108,7 @@ export const ImageAssetLarge = ({
</div>
<div css={imageWrapperStyles}>
<img
src={filePreviewUrl}
src={displaySrc}
alt=""
css={imageStyle}
style={
Expand Down
45 changes: 44 additions & 1 deletion apps/webapp/src/script/util/ImageUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
*
*/

import {imageHasExifData, isPreviewableImage, stripImageExifData} from './ImageUtil';
import {Maybe} from 'true-myth';

import {getBestPreviewSource, imageHasExifData, isPreviewableImage, stripImageExifData} from './ImageUtil';

const jpegWithExif = new Blob([new Uint8Array([0xff, 0xd8, 0xff, 0xe1, 0x00, 0x10, 0x45, 0x78, 0x69, 0x66])], {
type: 'image/jpeg',
Expand Down Expand Up @@ -140,4 +142,45 @@ describe('ImageUtil', () => {
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');
});
});
});
18 changes: 18 additions & 0 deletions apps/webapp/src/script/util/ImageUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*
*/

import {Maybe} from 'true-myth';

export const stripImageExifData = async (image: Blob): Promise<Blob> => {
const url = URL.createObjectURL(image);
try {
Expand Down Expand Up @@ -179,3 +181,19 @@ export const isPreviewableImage = ({

return !!normalizedExtension && PREVIEWABLE_IMAGE_EXTENSIONS.has(normalizedExtension);
};

type GetBestPreviewSourceOptions = {
readonly fileExtension: string;
readonly fileUrl: Maybe<string>;
readonly filePreviewUrl: Maybe<string>;
};

export function getBestPreviewSource(options: GetBestPreviewSourceOptions): Maybe<string> {
const {fileExtension, fileUrl, filePreviewUrl} = options;

if (!isPreviewableImage({extension: fileExtension})) {
return filePreviewUrl;
}

return fileUrl.or(filePreviewUrl);
}