SkateSpots/src/hooks/useUploadFile.ts
SkateSpots d9ad9e9b29 feat(offline): cache map tiles on-device + photo mirror fallback
Map tiles: new lib/tileCache.ts (Dexie SkateSpotsTilesDB, bounded to ~3000
tiles) and lib/cachedTileLayer.ts, a Leaflet TileLayer that reads through it
(cached -> fetch+cache -> plain URL fallback). Wired into both maps (SpotMap,
RouteDialog). Chosen over a service worker so it works in the Capacitor
WebView too, where a SW never sees tile requests.

Photos: useUploadFile drops from three Blossom hosts to two and records
['fallback', '<mirror>/<sha256>']. BlossomUploader signs once and Promise.any
PUTs to every server in parallel, so the old three-host list was triple-
uploading every photo (the 'tries them in order' comment was wrong). Read
side: lib/imageFallback.ts derives same-hash mirrors and ImageWithFallback
retries them before showing the placeholder - one choke point for ~20 photo
call sites, so a dead host no longer blanks a spot.
2026-09-28 13:01:27 +07:00

52 lines
1.6 KiB
TypeScript

import { useMutation } from "@tanstack/react-query";
import { BlossomUploader } from '@nostrify/nostrify/uploaders';
import { useCurrentUser } from "./useCurrentUser";
/**
* Two Blossom hosts. `BlossomUploader` signs ONE auth event and PUTs to every
* server in parallel, resolving with the first success — so the same bytes
* normally land on both from a single signature (no double Amber prompt). We then
* record a mirror URL on the returned tags so photo redundancy is explicit in the
* event; the read side also derives it from the sha256 (lib/imageFallback).
*/
const UPLOAD_SERVERS = ['https://blossom.primal.net/', 'https://blossom.band/'];
function hostOf(url: string): string {
try {
return new URL(url).hostname;
} catch {
return '';
}
}
export function useUploadFile() {
const { user } = useCurrentUser();
return useMutation({
mutationFn: async (file: File) => {
if (!user) {
throw new Error('Must be logged in to upload files');
}
const uploader = new BlossomUploader({
servers: UPLOAD_SERVERS,
signer: user.signer,
});
const tags = await uploader.upload(file);
// Remember the mirror copy of the same blob. Blossom is content-addressed,
// so `<other-host>/<sha256>` is the same file.
const primaryUrl = tags[0]?.[1];
const sha = tags.find(([name]) => name === 'x')?.[1];
if (primaryUrl && sha) {
const primaryHost = hostOf(primaryUrl);
const mirror = UPLOAD_SERVERS.find((s) => hostOf(s) !== primaryHost);
if (mirror) tags.push(['fallback', `${mirror.replace(/\/+$/, '')}/${sha}`]);
}
return tags;
},
});
}