/** * [Gitea #155] Web Share Target plumbing shared by the service worker and the * app. Android's share sheet POSTs a multipart form to /share-target (see * public/manifest.json); the worker stashes the payload in the Cache API and * redirects to the in-app /share page, which reads it back, lets the user * pick a room and pre-attaches the files to that room's composer. */ export const SHARE_TARGET_PATH = '/share-target'; export const SHARE_PAGE_PATH = '/share'; export const SHARE_CACHE = 'lotus-share-target'; export const SHARE_PAYLOAD_KEY = '/share-target/payload'; export const shareFileKey = (index: number) => `/share-target/file/${index}`; export type SharePayload = { title?: string; text?: string; url?: string; files: { name: string; type: string; size: number }[]; receivedAt: number; }; /** Worker side: stash the POSTed form and answer with a redirect to /share. */ export async function stashSharedForm(request: Request, base: string): Promise { const form = await request.formData(); const files = form.getAll('files').filter((f): f is File => f instanceof File); const str = (k: string) => { const v = form.get(k); return typeof v === 'string' && v.trim() ? v : undefined; }; const payload: SharePayload = { title: str('title'), text: str('text'), url: str('url'), files: files.map((f) => ({ name: f.name, type: f.type, size: f.size })), receivedAt: Date.now(), }; const cache = await caches.open(SHARE_CACHE); await Promise.all([...(await cache.keys()).map((k) => cache.delete(k))]); await cache.put( SHARE_PAYLOAD_KEY, new Response(JSON.stringify(payload), { headers: { 'Content-Type': 'application/json' } }), ); await Promise.all( files.map((f, i) => cache.put( shareFileKey(i), new Response(f, { headers: { 'Content-Type': f.type || 'application/octet-stream' }, }), ), ), ); return Response.redirect(`${base}${SHARE_PAGE_PATH}`, 303); } /** The share sheet's title/text/url as one draft; title and url are dropped when the text already carries them. */ export const shareDraftText = (p: SharePayload): string => { const text = p.text?.trim() ?? ''; const keep = (s: string | undefined) => !!s && s.trim() !== '' && !text.includes(s.trim()); return [keep(p.title) ? p.title : '', text, keep(p.url) ? p.url : ''].filter(Boolean).join('\n'); }; /** App side: read what the worker stashed (undefined when there is nothing). */ export async function readSharedPayload(): Promise< { payload: SharePayload; files: File[] } | undefined > { if (typeof caches === 'undefined') return undefined; const cache = await caches.open(SHARE_CACHE); const res = await cache.match(SHARE_PAYLOAD_KEY); if (!res) return undefined; const payload = (await res.json()) as SharePayload; const files = await Promise.all( payload.files.map(async (meta, i) => { const r = await cache.match(shareFileKey(i)); const blob = r ? await r.blob() : new Blob(); return new File([blob], meta.name || `shared-${i + 1}`, { type: meta.type || blob.type, }); }), ); return { payload, files }; } export async function clearSharedPayload(): Promise { if (typeof caches === 'undefined') return; await caches.delete(SHARE_CACHE); }