Skip to content

Commit 70b2403

Browse files
authored
fix: download Android media (#1722)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description <!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. --> Fixes # #### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. -->
2 parents a660bc2 + 4aa5430 commit 70b2403

6 files changed

Lines changed: 34 additions & 6 deletions

File tree

src/app/components/image-viewer/ImageViewer.css.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const ImageViewerContent = style([
109109
export const ImageViewerContentMobile = style({
110110
backgroundColor: '#000',
111111
color: '#fff',
112+
paddingBottom: safeAreaBottom,
112113
});
113114

114115
export const ImageViewerInput = style([
@@ -135,13 +136,11 @@ export const ImageViewerImg = style([
135136
maxHeight: 'none',
136137
backgroundColor: color.Surface.Container,
137138
transition: 'transform 100ms linear',
138-
willChange: 'transform',
139139
},
140140
]);
141141

142142
export const ImageViewerImgPixelated = style({
143143
imageRendering: 'pixelated',
144-
willChange: 'auto',
145144
});
146145

147146
const mobileGalleryControl = {

src/app/components/image-viewer/ImageViewer.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ describe('ImageViewer', () => {
9595
expect(FileSaver.saveAs).toHaveBeenCalledWith(expect.any(Blob), 'kitten.png');
9696
});
9797

98+
it("downloads the Matrix source behind Android's sable-media URL", async () => {
99+
const source = 'https://matrix.example.org/_matrix/client/v1/media/download/example.org/kitten';
100+
const src = `https://sable-media.localhost/${encodeURIComponent(source)}?__sable_media_cache=3`;
101+
downloadMedia.mockResolvedValue(new Blob(['image']));
102+
103+
renderViewer({ src });
104+
fireEvent.click(screen.getByText('Download'));
105+
106+
await waitFor(() => {
107+
expect(downloadMedia).toHaveBeenCalledWith(source);
108+
});
109+
});
110+
98111
it('activates the download control on the first touch sequence', async () => {
99112
screenMocks.isMobile = true;
100113
downloadMedia.mockClear();

src/app/components/image-viewer/ImageViewer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type { IImageInfo } from '$types/matrix/common';
4343
import { CheckerboardIcon, CopyIcon, ImagesIcon } from '@phosphor-icons/react';
4444
import { copyImageToClipboard } from '$utils/dom';
4545
import { getDownloadFilename, saveFileToDevice, saveMediaToGallery } from '$utils/download';
46+
import { getTauriMediaSourceUrl } from '$utils/mediaUrl';
4647
import { ResponsiveMenu } from '$components/ResponsiveMenu';
4748
import { isAndroidTauri, iosApp } from '$utils/platform';
4849
import { setImmersiveMode } from '$generated/tauri/commands';
@@ -160,7 +161,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
160161
}
161162
let fileContent: Blob;
162163
try {
163-
fileContent = await downloadMedia(src);
164+
fileContent = await downloadMedia(getTauriMediaSourceUrl(src) ?? src);
164165
} catch (error) {
165166
const message = error instanceof Error ? error.message : 'unknown error';
166167
showToast(`Failed to download file: ${message}`);

src/app/hooks/useImageGestures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ export const useImageGestures = (
374374
const imageWidth = img instanceof HTMLCanvasElement ? img.width : img.naturalWidth;
375375
const heightRatio = height / imageHeight;
376376
const widthRatio = width / imageWidth;
377-
const fitZoom = Math.min(heightRatio, widthRatio);
377+
const fitZoom = Math.min(heightRatio, widthRatio, 1);
378378

379379
img.style.transition = 'none';
380380
setFitRatio(fitZoom);

src/app/utils/mediaUrl.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ vi.mock('./mediaTransport', () => ({
2020

2121
import {
2222
addTauriMediaRetryRevision,
23+
getTauriMediaSourceUrl,
2324
getTauriMediaRetryTarget,
2425
rewriteAuthenticatedMediaUrl,
2526
} from './mediaUrl';
@@ -219,3 +220,17 @@ describe('getTauriMediaRetryTarget', () => {
219220
}
220221
);
221222
});
223+
224+
describe('getTauriMediaSourceUrl', () => {
225+
it('unwraps the Android protocol URL to its Matrix media source', () => {
226+
const source = 'https://matrix.example.com/_matrix/client/v1/media/download/example.com/abc123';
227+
const url = `https://sable-media.localhost/${encodeURIComponent(source)}?__sable_media_cache=3`;
228+
229+
expect(getTauriMediaSourceUrl(url)).toBe(source);
230+
});
231+
232+
it('passes ordinary URLs through unchanged', () => {
233+
const url = 'https://example.org/image.png';
234+
expect(getTauriMediaSourceUrl(url)).toBe(url);
235+
});
236+
});

src/app/utils/mediaUrl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const TAURI_MEDIA_PROTOCOL = 'sable-media://';
4646
const TAURI_MEDIA_LOCALHOST = 'localhost';
4747
const TAURI_MEDIA_LOCALHOST_HOST = 'sable-media.localhost';
4848

49-
const getTauriMediaInnerTarget = (mediaUrl: string): string | undefined => {
49+
export const getTauriMediaSourceUrl = (mediaUrl: string): string | undefined => {
5050
if (mediaUrl.startsWith(TAURI_MEDIA_PROTOCOL)) {
5151
const wrappedUrl = mediaUrl.slice(TAURI_MEDIA_PROTOCOL.length);
5252

@@ -88,7 +88,7 @@ export const getTauriMediaRetryTarget = (
8888
revision: number
8989
): string | undefined => {
9090
if (revision <= 0 || !isTauri()) return undefined;
91-
const innerTarget = getTauriMediaInnerTarget(mediaUrl);
91+
const innerTarget = getTauriMediaSourceUrl(mediaUrl);
9292
if (!innerTarget) return undefined;
9393
let parsedUrl: URL;
9494
try {

0 commit comments

Comments
 (0)