import { useAudioPlayer } from "expo-audio"; import { BlurView } from "expo-blur"; import { VideoView, useVideoPlayer } from "expo-video"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Image, Platform, Pressable, Text, View } from "react-native"; import Carousel from "react-native-reanimated-carousel"; import { responsiveHeight } from "react-native-responsive-dimensions"; import { icons, img } from "../assets"; import { arrayRemove, arrayUnion, projectsRef, usersRef } from "../config/firebase"; import useDataFromRef from "../hooks/useDataFromRef"; import { Routes } from "../navigation"; import { navigate } from "../navigation/NavigationService"; import { useUser } from "../providers/UserDataProvider"; import { Palette } from "../styles"; import { FONT_FAMILY } from "../styles/Fonts"; import { size } from "../styles/Style"; const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { const { currentUID, followUser, unfollowUser } = useUser() || {}; const videoUrl = item?.playbackUrl || null; const audioSource = useMemo(() => { const fromSong = item?.songUrl ? { uri: item.songUrl } : null; return fromSong; }, [item]); const hasExternalAudio = !!audioSource; const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : []; const [isLiked, setIsLiked] = useState( currentUID ? initialLikedBy.includes(currentUID) : false ); const [likesCount, setLikesCount] = useState(initialLikedBy.length); // Fetch owner profile (cached) const [owner, setOwner] = useState( item?.userId && userCache?.current?.get(item.userId) ? userCache.current.get(item.userId) : null ); useEffect(() => { let cancelled = false; const run = async () => { try { const uid = item?.userId; if (!uid || !userCache) return; const cached = userCache.current.get(uid); if (cached) { if (!cancelled) setOwner(cached); return; } const user = await getUserByUid?.(uid); if (!cancelled && user) { userCache.current.set(uid, user); setOwner(user); } } catch (e) {} }; run(); return () => { cancelled = true; }; }, [item?.userId, userCache, getUserByUid]); // Live sync owner from Firestore to reflect follow changes elsewhere useEffect(() => { const uid = item?.userId; if (!uid) return; const unsub = usersRef.doc(uid).onSnapshot( (doc) => { if (doc?.exists) { const data = { id: doc.id, ...doc.data() }; setOwner(data); try { userCache?.current?.set(uid, data); } catch (e) {} } }, () => {}, ); return () => unsub?.(); }, [item?.userId, userCache]); // Follow state derived from owner.followedBy const [isFollowing, setIsFollowing] = useState(false); useEffect(() => { const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []; setIsFollowing(currentUID ? list.includes(currentUID) : false); }, [owner?.followedBy, currentUID]); useEffect(() => { const lb = Array.isArray(item?.likedBy) ? item.likedBy : []; setLikesCount(lb.length); setIsLiked(currentUID ? lb.includes(currentUID) : false); }, [item?.likedBy, currentUID]); console.log("has external audio : ", hasExternalAudio); const audioPlayer = useAudioPlayer(audioSource || undefined); const videoPlayer = useVideoPlayer(videoUrl || null, (p) => { p.loop = false; p.muted = true; p.timeUpdateEventInterval = 0.2; }); useEffect(() => { const toggle = async () => { try { if (isActive) { try { if (audioPlayer && hasExternalAudio) await audioPlayer.seekTo?.(0); } catch (e) {} try { if (videoPlayer) videoPlayer.currentTime = 0; } catch (e) {} // Lancer quasi simultanément (éviter await pour limiter le décalage) try { if (videoPlayer) videoPlayer.play(); } catch (e) {} try { if (audioPlayer && hasExternalAudio) audioPlayer.play?.(); } catch (e) {} } else { if (audioPlayer?.playing) await audioPlayer.pause?.(); if (videoPlayer?.playing) videoPlayer.pause(); } } catch (e) {} }; toggle(); }, [isActive, audioPlayer, videoPlayer, hasExternalAudio]); // Micro-correction initiale uniquement pour absorber un léger décalage réseau useEffect(() => { if (!isActive || !hasExternalAudio) return; const t = setTimeout(() => { try { const a = audioPlayer?.currentTime || 0; const v = videoPlayer?.currentTime || 0; if (Math.abs(a - v) > 0.2 && videoPlayer) { videoPlayer.currentTime = Math.max(0, a); } } catch (e) {} }, 300); return () => clearTimeout(t); }, [isActive, hasExternalAudio, audioPlayer, videoPlayer]); useEffect(() => { return () => { try { if (audioPlayer?.playing) audioPlayer.pause?.(); if (videoPlayer?.playing) videoPlayer.pause(); } catch (e) {} }; }, [audioPlayer, videoPlayer]); return ( {!!videoUrl ? ( ) : ( )} {/* Right side actions */} { navigate(Routes.SingerProfile, { userId: item?.userId }); }} > {owner?.profilePictureURL ? ( ) : ( )} {owner?.id && currentUID && owner.id !== currentUID && ( { try { const next = !isFollowing; setIsFollowing(next); // Optimistic update of local owner.followedBy setOwner((prev) => { const fb = Array.isArray(prev?.followedBy) ? prev.followedBy : []; const newFb = next ? Array.from(new Set([...fb, currentUID])) : fb.filter((x) => x !== currentUID); return prev ? { ...prev, followedBy: newFb } : prev; }); if (next) await followUser?.(owner.id); else await unfollowUser?.(owner.id); } catch (e) { // rollback on failure setIsFollowing((v) => !v); } }} > {isFollowing ? "Ne plus suivre" : "Suivre"} )} { try { if (!currentUID || !item?.id) return; const nextLiked = !isLiked; setIsLiked(nextLiked); setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1))); const ref = projectsRef.doc(item.id); await ref.set( { likedBy: nextLiked ? arrayUnion(currentUID) : arrayRemove(currentUID), // Optionally: updatedAt could be set if needed }, { merge: true } ); } catch (e) { // rollback on failure setIsLiked((v) => !v); setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1))); } }} style={{ alignItems: "center" }} > {!!likesCount && ( {likesCount} )} {item?.title || "Description chanson"} ); }; const Playbacks = () => { const [activeIndex, setActiveIndex] = useState(0); const userCache = useRef(new Map()); const { getUserByUid } = useUser() || {}; const { data: playbacks = [], loadMore } = useDataFromRef({ ref: projectsRef.where("playbackUrl", "!=", null), simpleRef: false, listener: false, usePagination: true, batchSize: 2, }); const onSnap = useCallback( (index) => { setActiveIndex(index); // Pré-charge la suite 2 par 2 if (index >= (playbacks?.length || 0) - 2) { loadMore?.(); } }, [playbacks?.length, loadMore] ); return ( ( )} /> ); }; export default Playbacks;