diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 38f37892..2ee750fb 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -9,6 +9,16 @@ "background_color": "#0b1220", "orientation": "any", "categories": ["productivity", "social", "utilities"], + "share_target": { + "action": "/share-target", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "link" + } + }, "icons": [ { "src": "/icon-192.png", diff --git a/src/App.tsx b/src/App.tsx index 77872092..fe26e955 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import Debug from './components/Debug' import Bookmarks from './components/Bookmarks' import RouteDebug from './components/RouteDebug' import Toast from './components/Toast' +import ShareTargetHandler from './components/ShareTargetHandler' import { useToast } from './hooks/useToast' import { useOnlineStatus } from './hooks/useOnlineStatus' import { RELAYS } from './config/relays' @@ -159,6 +160,10 @@ function AppRoutes({ return ( + } + /> { + const handleSharedContent = async () => { + // Parse query parameters + const params = new URLSearchParams(location.search) + const link = params.get('link') + const title = params.get('title') + const text = params.get('text') + + // Validate we have a URL + if (!link) { + showToast('No URL to save') + navigate('/') + return + } + + // If no active account, wait for login + if (!activeAccount) { + setWaitingForLogin(true) + showToast('Please log in to save this bookmark') + return + } + + // We have account and URL, proceed with saving + if (!processing) { + setProcessing(true) + try { + await createWebBookmark( + link, + title || undefined, + text || undefined, + undefined, + activeAccount, + relayPool, + getActiveRelayUrls(relayPool) + ) + showToast('Bookmark saved!') + navigate('/me/links') + } catch (err) { + console.error('Failed to save shared bookmark:', err) + showToast('Failed to save bookmark') + navigate('/') + } finally { + setProcessing(false) + } + } + } + + handleSharedContent() + }, [activeAccount, location.search, navigate, relayPool, showToast, processing]) + + // Show waiting for login state + if (waitingForLogin && !activeAccount) { + return ( +
+
+ +

Waiting for login...

+
+
+ ) + } + + // Show processing state + return ( +
+
+ +

Saving bookmark...

+
+
+ ) +} + diff --git a/src/sw.ts b/src/sw.ts index 81905903..7bb694e7 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -98,10 +98,36 @@ sw.addEventListener('message', (event: ExtendableMessageEvent) => { } }) -// Log fetch errors for debugging (doesn't affect functionality) +// Handle Web Share Target POST requests sw.addEventListener('fetch', (event: FetchEvent) => { const url = new URL(event.request.url) + // Handle POST to /share-target (Web Share Target API) + if (event.request.method === 'POST' && url.pathname === '/share-target') { + event.respondWith((async () => { + const formData = await event.request.formData() + const title = (formData.get('title') || '').toString() + const text = (formData.get('text') || '').toString() + let link = (formData.get('link') || '').toString() + + // Android often omits url param, extract from text + if (!link && text) { + const urlMatch = text.match(/https?:\/\/[^\s]+/) + if (urlMatch) { + link = urlMatch[0] + } + } + + const queryParams = new URLSearchParams() + if (link) queryParams.set('link', link) + if (title) queryParams.set('title', title) + if (text) queryParams.set('text', text) + + return Response.redirect(`/share-target?${queryParams.toString()}`, 303) + })()) + return + } + // Don't interfere with WebSocket connections (relay traffic) if (url.protocol === 'ws:' || url.protocol === 'wss:') { return