diff --git a/src/lib/photoWallShare.ts b/src/lib/photoWallShare.ts index 244ffa3..bfcfbc2 100644 --- a/src/lib/photoWallShare.ts +++ b/src/lib/photoWallShare.ts @@ -243,7 +243,8 @@ async function resolveShareFile(options: { /** * Open the OS share sheet (iOS / Android) with the photo + caption. - * Falls back to text/url-only share if the image bytes cannot be loaded. + * By default falls back to text/url-only share if the image bytes cannot be loaded. + * Pass `filesOnly: true` to never share a URL. * * Critical: do not `await` network/canvas work before the first `navigator.share` * when a File is already in hand — iOS Safari drops the user gesture and fails @@ -255,10 +256,13 @@ export async function shareViaNativeSheet(options: { imgEl?: HTMLImageElement | null; title?: string; text?: string; + /** When true, never fall back to sharing a URL — only the image file. */ + filesOnly?: boolean; }): Promise { const title = options.title || "EventBuzz"; const text = options.text || `Check out my photo from ${title}!`; const imageUrl = options.imageUrl || undefined; + const filesOnly = options.filesOnly === true; if (typeof navigator.share !== "function") { throw new Error("Sharing is not supported on this device"); @@ -267,7 +271,12 @@ export async function shareViaNativeSheet(options: { const readyFile = options.file && options.file.size > 0 ? options.file : null; const tryShareFiles = async (file: File): Promise => { - const payloadWithFiles: ShareData = { title, text, files: [file] }; + const type = (file.type || "").toLowerCase(); + const shareable = + type === "image/jpeg" || type === "image/png" || type === "image/webp" + ? file + : await ensureShareableImageFile(file, file.name || "photo.jpg"); + const payloadWithFiles: ShareData = { title, text, files: [shareable] }; try { const canFiles = typeof navigator.canShare !== "function" || navigator.canShare(payloadWithFiles); @@ -287,21 +296,7 @@ export async function shareViaNativeSheet(options: { if (result !== "skip") return result; } - // 2) Text/url-only share still before any fetch (gesture-safe fallback). - const payloadTextOnly: ShareData = imageUrl - ? { title, text, url: imageUrl } - : { title, text }; - - try { - await navigator.share(payloadTextOnly); - return "shared"; - } catch (err) { - const name = err instanceof DOMException ? err.name : ""; - if (name === "AbortError") return "cancelled"; - // Continue — try loading bytes for a second attempt (gesture may already be gone). - } - - // 3) Last resort: fetch / canvas, then share (may only work on Android). + // 2) Resolve the image file (fetch / canvas) so we share bytes, not a URL. if (!readyFile) { const file = await resolveShareFile({ file: null, @@ -314,6 +309,22 @@ export async function shareViaNativeSheet(options: { } } + if (filesOnly) { + throw new Error("Could not share this photo. Try Download, then share from your photos."); + } + + const payloadTextOnly: ShareData = imageUrl + ? { title, text, url: imageUrl } + : { title, text }; + + try { + await navigator.share(payloadTextOnly); + return "shared"; + } catch (err) { + const name = err instanceof DOMException ? err.name : ""; + if (name === "AbortError") return "cancelled"; + } + throw new Error("Could not share this photo. Try Download, then share from your photos."); } diff --git a/src/pages/PhotoSearchPage.tsx b/src/pages/PhotoSearchPage.tsx index ebded22..8072a22 100644 --- a/src/pages/PhotoSearchPage.tsx +++ b/src/pages/PhotoSearchPage.tsx @@ -28,6 +28,7 @@ import { getAttendeeParticipant, getAttendeeToken } from "../lib/attendeeAuth"; import { canUseNativeShareSheet, downloadImageFromUrl, + ensureShareableImageFile, fetchImageFile, sharePhotoWallImage, shareViaNativeSheet, @@ -72,8 +73,9 @@ function MatchCard({ enabledSharePlatforms: Array<{ key: keyof PhotoWallSharePlatforms; label: string }>; }) { const imgRef = useRef(null); + const shareMenuRef = useRef(null); const [shareFile, setShareFile] = useState(null); - const [actionMsg, setActionMsg] = useState(null); + const [shareMenuOpen, setShareMenuOpen] = useState(false); const caption = `Check out my photo from ${eventName}!`; /** Grid preview uses CDN thumb (`url`) only — never the API file proxy. */ const previewSrc = match.url || undefined; @@ -83,6 +85,7 @@ function MatchCard({ if (!match.fileUrl) return; let cancelled = false; void fetchImageFile(match.fileUrl, match.originalFileName || "event-photo.jpg") + .then((f) => ensureShareableImageFile(f, match.originalFileName || "event-photo.jpg")) .then((f) => { if (!cancelled) setShareFile(f); }) @@ -94,8 +97,18 @@ function MatchCard({ }; }, [match.fileUrl, match.originalFileName]); + useEffect(() => { + if (!shareMenuOpen) return; + const onPointerDown = (event: PointerEvent) => { + const el = shareMenuRef.current; + if (el && !el.contains(event.target as Node)) setShareMenuOpen(false); + }; + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [shareMenuOpen]); + const onDownload = async () => { - setActionMsg(null); + setShareMenuOpen(false); try { if (shareFile) { const objectUrl = URL.createObjectURL(shareFile); @@ -106,63 +119,43 @@ function MatchCard({ a.click(); a.remove(); URL.revokeObjectURL(objectUrl); - setActionMsg("Download started"); return; } if (!downloadSrc) return; await downloadImageFromUrl(downloadSrc, match.originalFileName || "event-photo.jpg"); - setActionMsg("Download started"); - } catch (e) { - setActionMsg(e instanceof Error ? e.message : "Download failed"); + } catch { + /* keep the card quiet */ } }; const onNativeShare = async () => { - setActionMsg(null); try { - const result = await shareViaNativeSheet({ + await shareViaNativeSheet({ file: shareFile, imageUrl: downloadSrc, imgEl: imgRef.current, title: eventName, text: caption, + filesOnly: true, }); - if (result === "shared") setActionMsg("Shared"); - else setActionMsg(null); - } catch (e) { - const msg = e instanceof Error ? e.message : "Share failed"; - setActionMsg( - /load failed|failed to fetch/i.test(msg) - ? "Could not share this photo. Try Download, then share from your photos." - : msg, - ); + } catch { + /* keep the card quiet */ } }; const onShare = async (platform: keyof PhotoWallSharePlatforms) => { if (!downloadSrc && !shareFile) return; - setActionMsg(null); + setShareMenuOpen(false); try { - const result = await sharePhotoWallImage({ + await sharePhotoWallImage({ platform, imageUrl: downloadSrc || "", file: shareFile, title: eventName, text: caption, }); - if (result === "shared") { - setActionMsg( - platform === "whatsapp" || platform === "facebook" || platform === "x" || platform === "linkedin" - ? "Opening app with your photo link" - : "Shared", - ); - } else if (result === "cancelled") { - setActionMsg(null); - } else { - setActionMsg("Photo saved and caption copied — paste / attach it in the app"); - } - } catch (e) { - setActionMsg(e instanceof Error ? e.message : "Share failed"); + } catch { + /* keep the card quiet */ } }; @@ -218,7 +211,6 @@ function MatchCard({ ) ) : null} - {actionMsg ?

{actionMsg}

: null} );