Merge branch 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website
* 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website: Photo Search # Conflicts: # src/pages/PhotoSearchPage.tsx
This commit is contained in:
commit
0b44e15b50
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue