Files
cinny-mobile/overlay/src/app/utils/saveMedia.ts
litruv 8cf31b929d
All checks were successful
Build / increment-version (push) Successful in 5s
Build / build-android (push) Successful in 5m5s
Fix Android media downloads and dismiss notifs when read elsewhere.
FileSaver fails in Capacitor WebView, so save via MediaStore; clear tray notifications when sync reports rooms as read on another device, and improve collapsed notification avatars with conversation shortcuts.
2026-07-24 15:26:35 +10:00

104 lines
3.2 KiB
TypeScript

import FileSaver from 'file-saver';
import { Capacitor, registerPlugin } from '@capacitor/core';
import { downloadMedia } from './matrix';
import { getCurrentAccessToken } from './auth';
interface AndroidShareHandlerSavePlugin {
saveFile(options: {
filename: string;
mimeType?: string;
base64: string;
}): Promise<{ saved: boolean; shared?: boolean; uri?: string }>;
shareFile(options: {
filename: string;
mimeType?: string;
base64: string;
}): Promise<{ shared: boolean }>;
}
const AndroidShareHandler = registerPlugin<AndroidShareHandlerSavePlugin>('AndroidShareHandler');
const isAndroidNative = (): boolean =>
Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android';
/** Convert a Blob to raw base64 (no data: prefix). */
const blobToBase64 = (blob: Blob): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result !== 'string') {
reject(new Error('Failed to read blob as base64'));
return;
}
resolve(result.includes(',') ? result.split(',')[1] : result);
};
reader.onerror = () => reject(reader.error ?? new Error('FileReader failed'));
reader.readAsDataURL(blob);
});
const guessMimeFromName = (filename: string): string | undefined => {
const ext = filename.split('.').pop()?.toLowerCase();
if (!ext) return undefined;
const map: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
bmp: 'image/bmp',
svg: 'image/svg+xml',
mp4: 'video/mp4',
webm: 'video/webm',
mkv: 'video/x-matroska',
mov: 'video/quicktime',
mp3: 'audio/mpeg',
ogg: 'audio/ogg',
wav: 'audio/wav',
pdf: 'application/pdf',
txt: 'text/plain',
};
return map[ext];
};
/**
* Persist a Blob to disk. On Capacitor Android, uses MediaStore via the native
* ShareHandler plugin (FileSaver / `<a download>` does not work in WebView).
* Elsewhere falls back to FileSaver.
*/
export const saveMediaBlob = async (blob: Blob, filename: string): Promise<void> => {
if (isAndroidNative()) {
const base64 = await blobToBase64(blob);
const mimeType = blob.type || guessMimeFromName(filename) || 'application/octet-stream';
await AndroidShareHandler.saveFile({ filename, mimeType, base64 });
return;
}
FileSaver.saveAs(blob, filename);
};
/**
* Fetch media (auth download or blob:/http URL) and save it via [saveMediaBlob].
*/
export const downloadAndSaveMedia = async (
src: string,
filename: string,
accessToken?: string | null
): Promise<void> => {
let blob: Blob;
try {
if (src.startsWith('blob:') || src.startsWith('data:')) {
const res = await fetch(src);
if (!res.ok) throw new Error(`Failed to fetch ${src}`);
blob = await res.blob();
} else {
blob = await downloadMedia(src, accessToken ?? getCurrentAccessToken());
}
} catch (error) {
console.warn('[saveMedia] downloadMedia failed, trying fetch fallback:', error);
const res = await fetch(src);
if (!res.ok) throw error;
blob = await res.blob();
}
await saveMediaBlob(blob, filename);
};