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;