feat(pwa): register as an Android share target (#155)

public/manifest.json declares share_target (POST multipart to
/share-target: title/text/url + image/video/audio/pdf/text files). The
service worker answers that POST itself: it stashes the form in a Cache
API bucket and 303s to the in-app /share page, which lists what arrived,
offers a room search, and on pick writes the files into that room's
upload-board atom (encrypting first for E2EE rooms via the composer's
shared filesToUploadItems) and the title/text/url into its draft, then
opens the room — the user still presses Send. The stash is cleared once
placed; reopening /share afterwards says so.

nginx/caddy examples and the prod image config gain a 303 for
/share-target so a POST that reaches the origin before the worker
controls the page lands on /share instead of a 405. iOS has no share
target support and ignores the manifest entry.

Verified headless against the built preview: SW-controlled page → POST
/share-target (two PNGs + title + text) → /share lists both files and the
text → pick the DM → composer shows both files on the upload board and
the text in the draft → /share reports nothing pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-20 13:36:05 -04:00
co-authored by Claude Opus 5
parent 6aa77552b8
commit 96a97a2f86
10 changed files with 437 additions and 29 deletions
+11
View File
@@ -1,6 +1,7 @@
/// <reference lib="WebWorker" />
import { precacheAndRoute, type PrecacheEntry } from 'workbox-precaching';
import { SHARE_TARGET_PATH, stashSharedForm } from './swShare';
import { sendNotificationReply } from './swReply';
export type {};
@@ -234,6 +235,16 @@ function fetchConfig(token: string): RequestInit {
self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request;
// [Gitea #155] Android share sheet → POST /share-target (manifest
// share_target). Stash the form and bounce to the in-app /share page.
if (method === 'POST' && new URL(url).pathname.endsWith(SHARE_TARGET_PATH)) {
const base = new URL(url).pathname.slice(0, -SHARE_TARGET_PATH.length);
event.respondWith(
stashSharedForm(event.request, base).catch(() => Response.redirect(`${base}/`, 303)),
);
return;
}
if (method !== 'GET' || !mediaPath(url)) return;
const { clientId } = event;