import { useIsFocused, useRoute } from "@react-navigation/native"; 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 { openComments } from "../../components/bottomsheets/CommentsBottomSheet"; import KaraokeLyrics from "../../components/KaraokeLyrics"; import ProfilePicture from "../../components/ProfilePicture"; import { 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"; import { getProjectLikes, LIKE_TARGET, toggleProjectLike, } from "../../utils/likes"; import { createPlaybackSharePayload, openShareSheet, } from "../../utils/shareSheet"; import { SheetManager } from "react-native-actions-sheet"; import { Feather } from "@expo/vector-icons"; const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { const { currentUID, followUser, unfollowUser } = useUser() || {}; const videoUrl = item?.playbackUrl || null; const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK); const [isLiked, setIsLiked] = useState( currentUID ? initialLikedBy.includes(currentUID) : false ); const [likesCount, setLikesCount] = useState(initialLikedBy.length); const commentsCount = useMemo( () => Number(item?.commentsCount || 0), [item?.commentsCount] ); 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); const [isFollowActionPending, setIsFollowActionPending] = useState(false); useEffect(() => { const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []; setIsFollowing(currentUID ? list.includes(currentUID) : false); }, [owner?.followedBy, currentUID]); useEffect(() => { const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK); setLikesCount(lb.length); setIsLiked(currentUID ? lb.includes(currentUID) : false); }, [item?.likes?.playback, currentUID]); const projectTitle = typeof item?.title === "string" ? item.title.trim() : ""; const creatorName = useMemo(() => { if (owner?.displayName) return owner.displayName; if (owner?.artistName) return owner.artistName; if (owner?.userName) return owner.userName; if (typeof item?.userName === "string") return item.userName; return ""; }, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]); const videoPlayer = useVideoPlayer(videoUrl || null, (p) => { p.loop = false; p.muted = false; p.timeUpdateEventInterval = 0.2; }); useEffect(() => { const toggle = async () => { try { if (isActive) { 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) {} } else { if (videoPlayer?.playing) videoPlayer.pause(); } } catch (e) {} }; toggle(); }, [isActive, videoPlayer]); useEffect(() => { return () => { try { if (videoPlayer?.playing) videoPlayer.pause(); } catch (e) {} }; }, [videoPlayer]); const handleReport = useCallback(() => { if (!item?.id) return; SheetManager.show("Report", { payload: { targetType: "playback", projectId: item?.id || null, playbackId: item?.id || null, title: item?.title || "", ownerId: item?.userId || null, }, }); }, [item?.id, item?.title, item?.userId]); // Build alignedWords from item musicTimestamps const alignedWords = useMemo(() => { const idx = Number(item?.songIndex) || 0; const ts = item?.musicTimestamps?.[idx]; const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []; return arr.map((w) => ({ word: String(w?.word ?? ""), startS: Number(w?.startS ?? 0), endS: Number(w?.endS ?? 0), })); }, [item?.musicTimestamps, item?.songIndex]); // Track current time for lyrics sync const [currentTimeS, setCurrentTimeS] = useState(0); useEffect(() => { if (!isActive) return; const id = global.setInterval(() => { try { setCurrentTimeS(Number(videoPlayer?.currentTime || 0)); } catch (e) {} }, 250); return () => global.clearInterval(id); }, [isActive, videoPlayer]); const sharePayload = useMemo(() => { if (!item?.id) return null; return createPlaybackSharePayload({ projectId: item.id, title: projectTitle || undefined, artist: creatorName || undefined, playbackUrl: videoUrl || undefined, }); }, [creatorName, item?.id, projectTitle, videoUrl]); const handleShare = useCallback(() => { if (sharePayload) { openShareSheet(sharePayload); } }, [sharePayload]); // partage: via feuille unifiée (prend en charge QR) return ( {!!videoUrl ? ( ) : ( )} {/* Right side actions */} { navigate(Routes.SingerProfile, { userId: item?.userId }); }} > {owner?.id && currentUID && owner.id !== currentUID && ( { if (isFollowActionPending) return; try { setIsFollowActionPending(true); const next = !isFollowing; console.log("follwin"); 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); // Timeout de 2 secondes avant de pouvoir recliquer global.setTimeout(() => { setIsFollowActionPending(false); }, 1000); } catch (e) { // rollback on failure setIsFollowing((v) => !v); setIsFollowActionPending(false); } }} > {isFollowing ? "Suivi(e)" : "Suivre"} )} { try { if (!currentUID || !item?.id) return; const nextLiked = !isLiked; setIsLiked(nextLiked); setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1))); await toggleProjectLike({ projectId: item.id, currentUID, target: LIKE_TARGET.PLAYBACK, next: nextLiked, }); } catch (e) { // rollback on failure setIsLiked((v) => !v); setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1))); } }} style={{ alignItems: "center" }} > {!!likesCount && ( {likesCount} )} item?.id && openComments(item.id)} style={{ alignItems: "center" }} > {!!commentsCount && ( {commentsCount} )} Signaler {alignedWords?.length > 0 ? ( ) : ( {item?.title || "Description chanson"} )} ); }; const Playbacks = () => { const [activeIndex, setActiveIndex] = useState(0); const carouselRef = useRef(null); const route = useRoute(); const focusProjectId = route?.params?.projectId || route?.params?.focusId || null; const userCache = useRef(new Map()); const { getUserByUid } = useUser() || {}; const isFocused = useIsFocused(); const { data: playbacks = [], loadMore } = useDataFromRef({ ref: projectsRef.where("playbackUrl", "!=", null), simpleRef: false, listener: false, usePagination: true, batchSize: 6, }); const onSnap = useCallback( (index) => { setActiveIndex(index); // Pré-charge la suite 6 par 6 if (index >= (playbacks?.length || 0) - 6) { loadMore?.(); } }, [playbacks?.length, loadMore] ); // When a specific playback id is provided via navigation params, // try to focus it by scrolling the carousel to its index. const triedLoadMoreRef = useRef(0); useEffect(() => { if (!focusProjectId) return; const idx = playbacks.findIndex((p) => p?.id === focusProjectId); if (idx >= 0) { setActiveIndex(idx); // give time for first render before scrolling global.setTimeout(() => { try { carouselRef.current?.scrollTo?.({ index: idx, animated: false }); } catch (e) {} }, 50); } else if (loadMore && triedLoadMoreRef.current < 6) { triedLoadMoreRef.current += 1; loadMore(); } }, [focusProjectId, playbacks, loadMore]); return ( ( )} /> ); }; export default Playbacks;