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
3 changes: 1 addition & 2 deletions src/app/components/image-viewer/ImageViewer.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const ImageViewerContent = style([
export const ImageViewerContentMobile = style({
backgroundColor: '#000',
color: '#fff',
paddingBottom: safeAreaBottom,
});

export const ImageViewerInput = style([
Expand All @@ -135,13 +136,11 @@ export const ImageViewerImg = style([
maxHeight: 'none',
backgroundColor: color.Surface.Container,
transition: 'transform 100ms linear',
willChange: 'transform',
},
]);

export const ImageViewerImgPixelated = style({
imageRendering: 'pixelated',
willChange: 'auto',
});

const mobileGalleryControl = {
Expand Down
13 changes: 13 additions & 0 deletions src/app/components/image-viewer/ImageViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ describe('ImageViewer', () => {
expect(FileSaver.saveAs).toHaveBeenCalledWith(expect.any(Blob), 'kitten.png');
});

it("downloads the Matrix source behind Android's sable-media URL", async () => {
const source = 'https://matrix.example.org/_matrix/client/v1/media/download/example.org/kitten';
const src = `https://sable-media.localhost/${encodeURIComponent(source)}?__sable_media_cache=3`;
downloadMedia.mockResolvedValue(new Blob(['image']));

renderViewer({ src });
fireEvent.click(screen.getByText('Download'));

await waitFor(() => {
expect(downloadMedia).toHaveBeenCalledWith(source);
});
});

it('activates the download control on the first touch sequence', async () => {
screenMocks.isMobile = true;
downloadMedia.mockClear();
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/image-viewer/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { IImageInfo } from '$types/matrix/common';
import { CheckerboardIcon, CopyIcon, ImagesIcon } from '@phosphor-icons/react';
import { copyImageToClipboard } from '$utils/dom';
import { getDownloadFilename, saveFileToDevice, saveMediaToGallery } from '$utils/download';
import { getTauriMediaSourceUrl } from '$utils/mediaUrl';
import { ResponsiveMenu } from '$components/ResponsiveMenu';
import { isAndroidTauri, iosApp } from '$utils/platform';
import { setImmersiveMode } from '$generated/tauri/commands';
Expand Down Expand Up @@ -160,7 +161,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
}
let fileContent: Blob;
try {
fileContent = await downloadMedia(src);
fileContent = await downloadMedia(getTauriMediaSourceUrl(src) ?? src);
} catch (error) {
const message = error instanceof Error ? error.message : 'unknown error';
showToast(`Failed to download file: ${message}`);
Expand Down
2 changes: 1 addition & 1 deletion src/app/hooks/useImageGestures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ export const useImageGestures = (
const imageWidth = img instanceof HTMLCanvasElement ? img.width : img.naturalWidth;
const heightRatio = height / imageHeight;
const widthRatio = width / imageWidth;
const fitZoom = Math.min(heightRatio, widthRatio);
const fitZoom = Math.min(heightRatio, widthRatio, 1);

img.style.transition = 'none';
setFitRatio(fitZoom);
Expand Down
15 changes: 15 additions & 0 deletions src/app/utils/mediaUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ vi.mock('./mediaTransport', () => ({

import {
addTauriMediaRetryRevision,
getTauriMediaSourceUrl,
getTauriMediaRetryTarget,
rewriteAuthenticatedMediaUrl,
} from './mediaUrl';
Expand Down Expand Up @@ -219,3 +220,17 @@ describe('getTauriMediaRetryTarget', () => {
}
);
});

describe('getTauriMediaSourceUrl', () => {
it('unwraps the Android protocol URL to its Matrix media source', () => {
const source = 'https://matrix.example.com/_matrix/client/v1/media/download/example.com/abc123';
const url = `https://sable-media.localhost/${encodeURIComponent(source)}?__sable_media_cache=3`;

expect(getTauriMediaSourceUrl(url)).toBe(source);
});

it('passes ordinary URLs through unchanged', () => {
const url = 'https://example.org/image.png';
expect(getTauriMediaSourceUrl(url)).toBe(url);
});
});
4 changes: 2 additions & 2 deletions src/app/utils/mediaUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const TAURI_MEDIA_PROTOCOL = 'sable-media://';
const TAURI_MEDIA_LOCALHOST = 'localhost';
const TAURI_MEDIA_LOCALHOST_HOST = 'sable-media.localhost';

const getTauriMediaInnerTarget = (mediaUrl: string): string | undefined => {
export const getTauriMediaSourceUrl = (mediaUrl: string): string | undefined => {
if (mediaUrl.startsWith(TAURI_MEDIA_PROTOCOL)) {
const wrappedUrl = mediaUrl.slice(TAURI_MEDIA_PROTOCOL.length);

Expand Down Expand Up @@ -88,7 +88,7 @@ export const getTauriMediaRetryTarget = (
revision: number
): string | undefined => {
if (revision <= 0 || !isTauri()) return undefined;
const innerTarget = getTauriMediaInnerTarget(mediaUrl);
const innerTarget = getTauriMediaSourceUrl(mediaUrl);
if (!innerTarget) return undefined;
let parsedUrl: URL;
try {
Expand Down
Loading