* 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website:
  Photo Search

# Conflicts:
#	src/pages/PhotoSearchPage.tsx
This commit is contained in:
Amey Bhagwat 2026-08-17 19:34:55 +05:30
commit 0b44e15b50
2 changed files with 53 additions and 50 deletions

View File

@ -243,7 +243,8 @@ async function resolveShareFile(options: {
/** /**
* Open the OS share sheet (iOS / Android) with the photo + caption. * 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` * 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 * 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; imgEl?: HTMLImageElement | null;
title?: string; title?: string;
text?: string; text?: string;
/** When true, never fall back to sharing a URL — only the image file. */
filesOnly?: boolean;
}): Promise<SharePhotoWallResult> { }): Promise<SharePhotoWallResult> {
const title = options.title || "EventBuzz"; const title = options.title || "EventBuzz";
const text = options.text || `Check out my photo from ${title}!`; const text = options.text || `Check out my photo from ${title}!`;
const imageUrl = options.imageUrl || undefined; const imageUrl = options.imageUrl || undefined;
const filesOnly = options.filesOnly === true;
if (typeof navigator.share !== "function") { if (typeof navigator.share !== "function") {
throw new Error("Sharing is not supported on this device"); 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 readyFile = options.file && options.file.size > 0 ? options.file : null;
const tryShareFiles = async (file: File): Promise<SharePhotoWallResult | "skip"> => { const tryShareFiles = async (file: File): Promise<SharePhotoWallResult | "skip"> => {
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 { try {
const canFiles = const canFiles =
typeof navigator.canShare !== "function" || navigator.canShare(payloadWithFiles); typeof navigator.canShare !== "function" || navigator.canShare(payloadWithFiles);
@ -287,21 +296,7 @@ export async function shareViaNativeSheet(options: {
if (result !== "skip") return result; if (result !== "skip") return result;
} }
// 2) Text/url-only share still before any fetch (gesture-safe fallback). // 2) Resolve the image file (fetch / canvas) so we share bytes, not a URL.
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).
if (!readyFile) { if (!readyFile) {
const file = await resolveShareFile({ const file = await resolveShareFile({
file: null, 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."); throw new Error("Could not share this photo. Try Download, then share from your photos.");
} }

View File

@ -28,6 +28,7 @@ import { getAttendeeParticipant, getAttendeeToken } from "../lib/attendeeAuth";
import { import {
canUseNativeShareSheet, canUseNativeShareSheet,
downloadImageFromUrl, downloadImageFromUrl,
ensureShareableImageFile,
fetchImageFile, fetchImageFile,
sharePhotoWallImage, sharePhotoWallImage,
shareViaNativeSheet, shareViaNativeSheet,
@ -72,8 +73,9 @@ function MatchCard({
enabledSharePlatforms: Array<{ key: keyof PhotoWallSharePlatforms; label: string }>; enabledSharePlatforms: Array<{ key: keyof PhotoWallSharePlatforms; label: string }>;
}) { }) {
const imgRef = useRef<HTMLImageElement>(null); const imgRef = useRef<HTMLImageElement>(null);
const shareMenuRef = useRef<HTMLDivElement>(null);
const [shareFile, setShareFile] = useState<File | null>(null); const [shareFile, setShareFile] = useState<File | null>(null);
const [actionMsg, setActionMsg] = useState<string | null>(null); const [shareMenuOpen, setShareMenuOpen] = useState(false);
const caption = `Check out my photo from ${eventName}!`; const caption = `Check out my photo from ${eventName}!`;
/** Grid preview uses CDN thumb (`url`) only — never the API file proxy. */ /** Grid preview uses CDN thumb (`url`) only — never the API file proxy. */
const previewSrc = match.url || undefined; const previewSrc = match.url || undefined;
@ -83,6 +85,7 @@ function MatchCard({
if (!match.fileUrl) return; if (!match.fileUrl) return;
let cancelled = false; let cancelled = false;
void fetchImageFile(match.fileUrl, match.originalFileName || "event-photo.jpg") void fetchImageFile(match.fileUrl, match.originalFileName || "event-photo.jpg")
.then((f) => ensureShareableImageFile(f, match.originalFileName || "event-photo.jpg"))
.then((f) => { .then((f) => {
if (!cancelled) setShareFile(f); if (!cancelled) setShareFile(f);
}) })
@ -94,8 +97,18 @@ function MatchCard({
}; };
}, [match.fileUrl, match.originalFileName]); }, [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 () => { const onDownload = async () => {
setActionMsg(null); setShareMenuOpen(false);
try { try {
if (shareFile) { if (shareFile) {
const objectUrl = URL.createObjectURL(shareFile); const objectUrl = URL.createObjectURL(shareFile);
@ -106,63 +119,43 @@ function MatchCard({
a.click(); a.click();
a.remove(); a.remove();
URL.revokeObjectURL(objectUrl); URL.revokeObjectURL(objectUrl);
setActionMsg("Download started");
return; return;
} }
if (!downloadSrc) return; if (!downloadSrc) return;
await downloadImageFromUrl(downloadSrc, match.originalFileName || "event-photo.jpg"); await downloadImageFromUrl(downloadSrc, match.originalFileName || "event-photo.jpg");
setActionMsg("Download started"); } catch {
} catch (e) { /* keep the card quiet */
setActionMsg(e instanceof Error ? e.message : "Download failed");
} }
}; };
const onNativeShare = async () => { const onNativeShare = async () => {
setActionMsg(null);
try { try {
const result = await shareViaNativeSheet({ await shareViaNativeSheet({
file: shareFile, file: shareFile,
imageUrl: downloadSrc, imageUrl: downloadSrc,
imgEl: imgRef.current, imgEl: imgRef.current,
title: eventName, title: eventName,
text: caption, text: caption,
filesOnly: true,
}); });
if (result === "shared") setActionMsg("Shared"); } catch {
else setActionMsg(null); /* keep the card quiet */
} 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,
);
} }
}; };
const onShare = async (platform: keyof PhotoWallSharePlatforms) => { const onShare = async (platform: keyof PhotoWallSharePlatforms) => {
if (!downloadSrc && !shareFile) return; if (!downloadSrc && !shareFile) return;
setActionMsg(null); setShareMenuOpen(false);
try { try {
const result = await sharePhotoWallImage({ await sharePhotoWallImage({
platform, platform,
imageUrl: downloadSrc || "", imageUrl: downloadSrc || "",
file: shareFile, file: shareFile,
title: eventName, title: eventName,
text: caption, text: caption,
}); });
if (result === "shared") { } catch {
setActionMsg( /* keep the card quiet */
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");
} }
}; };
@ -218,7 +211,6 @@ function MatchCard({
</div> </div>
) )
) : null} ) : null}
{actionMsg ? <p className="text-center text-[11px] text-[#666]">{actionMsg}</p> : null}
</div> </div>
</div> </div>
); );