import { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, Image, Pressable, StyleSheet, Text, View } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { Audio } from "expo-av"; import { getMediaUrl } from "../../../lib/mediaApi"; import { colors, featureColors, radius, spacing, typography } from "../../../theme"; import type { DareProofType } from "../types"; const ACCENT = featureColors.dares.accent; export interface DareProofViewProps { proofMediaId: string; proofType: DareProofType; } /** * Renders the attached proof inline on a dare card: the photo itself, or a * play button for audio. Signed URLs are short-lived, so the URL is resolved * lazily on first render (photo) or first play (audio). */ export function DareProofView({ proofMediaId, proofType }: DareProofViewProps) { const [url, setUrl] = useState(null); const [failed, setFailed] = useState(false); const [sound, setSound] = useState(null); const [playing, setPlaying] = useState(false); const [loadingAudio, setLoadingAudio] = useState(false); useEffect(() => { if (proofType !== "photo") return; let cancelled = false; getMediaUrl(proofMediaId) .then((result) => { if (!cancelled) setUrl(result.url); }) .catch(() => { if (!cancelled) setFailed(true); }); return () => { cancelled = true; }; }, [proofMediaId, proofType]); useEffect(() => { return () => { if (sound) void sound.unloadAsync(); }; }, [sound]); const togglePlayback = useCallback(async () => { try { if (sound) { const status = await sound.getStatusAsync(); if (status.isLoaded && status.isPlaying) { await sound.pauseAsync(); setPlaying(false); } else { await sound.replayAsync(); setPlaying(true); } return; } setLoadingAudio(true); const { url: signedUrl } = await getMediaUrl(proofMediaId); const created = await Audio.Sound.createAsync( { uri: signedUrl }, { shouldPlay: true }, (status) => { if (status.isLoaded && status.didJustFinish) setPlaying(false); }, ); setSound(created.sound); setPlaying(true); } catch { setFailed(true); } finally { setLoadingAudio(false); } }, [sound, proofMediaId]); if (failed) { return Proof could not be loaded.; } if (proofType === "photo") { return url ? ( ) : ( ); } return ( void togglePlayback()} disabled={loadingAudio}> {loadingAudio ? ( ) : ( )} {playing ? "Playing proof…" : "Play audio proof"} ); } const styles = StyleSheet.create({ photo: { width: "100%", height: 180, marginTop: spacing.sm, borderRadius: radius.md, borderWidth: 1, borderColor: colors.border, }, loader: { marginTop: spacing.sm, alignSelf: "flex-start" }, audioRow: { flexDirection: "row", alignItems: "center", gap: spacing.sm, marginTop: spacing.sm, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: radius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.background, alignSelf: "flex-start", }, audioLabel: { ...typography.subhead, color: colors.text, fontWeight: "600" }, failed: { ...typography.footnote, color: colors.textTertiary, marginTop: spacing.sm }, });