add - commentsCount

This commit is contained in:
Victor
2025-09-16 15:17:35 +02:00
parent ea9bfb0f05
commit e79a4c7140
2 changed files with 126 additions and 125 deletions
@@ -3,16 +3,16 @@ import {
BottomSheetBackdrop, BottomSheetBackdrop,
BottomSheetModal, BottomSheetModal,
BottomSheetScrollView, BottomSheetScrollView,
} from "@gorhom/bottom-sheet"; } from '@gorhom/bottom-sheet';
import { BlurView } from "expo-blur"; import { BlurView } from 'expo-blur';
import { Image as ExpoImage } from "expo-image"; import { Image as ExpoImage } from 'expo-image';
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { import {
Image, Image,
Platform, Platform,
@@ -20,29 +20,29 @@ import {
Text, Text,
TextInput, TextInput,
View, View,
} from "react-native"; } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { icons } from "../../assets"; import { icons } from '../../assets';
import { projectsRef, serverTimestamp } from "../../config/firebase"; import { increment, projectsRef, serverTimestamp } from '../../config/firebase';
import useDataFromRef from "../../hooks/useDataFromRef"; import useDataFromRef from '../../hooks/useDataFromRef';
import { useUser } from "../../providers/UserDataProvider"; import { useUser } from '../../providers/UserDataProvider';
import { Palette } from "../../styles"; import { Palette } from '../../styles';
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from '../../styles/Fonts';
import { size } from "../../styles/Style"; import { size } from '../../styles/Style';
let externalOpen; let externalOpen;
let externalClose; let externalClose;
export const openComments = (projectId) => { export const openComments = (projectId) => {
if (typeof externalOpen === "function") externalOpen(projectId); if (typeof externalOpen === 'function') externalOpen(projectId);
}; };
export const closeComments = () => { export const closeComments = () => {
if (typeof externalClose === "function") externalClose(); if (typeof externalClose === 'function') externalClose();
}; };
const formatRelativeTime = (date) => { const formatRelativeTime = (date) => {
try { try {
const d = date instanceof Date ? date : date?.toDate?.() || null; const d = date instanceof Date ? date : date?.toDate?.() || null;
if (!d) return ""; if (!d) return '';
const diff = Math.max(0, Date.now() - d.getTime()); const diff = Math.max(0, Date.now() - d.getTime());
const sec = Math.floor(diff / 1000); const sec = Math.floor(diff / 1000);
if (sec < 60) return `${sec}s`; if (sec < 60) return `${sec}s`;
@@ -53,7 +53,7 @@ const formatRelativeTime = (date) => {
const dA = Math.floor(h / 24); const dA = Math.floor(h / 24);
return `${dA}j`; return `${dA}j`;
} catch (e) { } catch (e) {
return ""; return '';
} }
}; };
@@ -63,7 +63,7 @@ const CommentsBottomSheet = () => {
const { currentUID, currentUserData } = useUser(); const { currentUID, currentUserData } = useUser();
const [projectId, setProjectId] = useState(null); const [projectId, setProjectId] = useState(null);
const [text, setText] = useState(""); const [text, setText] = useState('');
const [typing, setTyping] = useState(false); const [typing, setTyping] = useState(false);
useEffect(() => { useEffect(() => {
@@ -82,15 +82,15 @@ const CommentsBottomSheet = () => {
}; };
}, []); }, []);
const snapPoints = useMemo(() => ["50%", "90%"], []); const snapPoints = useMemo(() => ['50%', '90%'], []);
const commentsRef = useMemo(() => { const commentsRef = useMemo(() => {
try { try {
return projectId return projectId
? projectsRef ? projectsRef
.doc(projectId) .doc(projectId)
.collection("comments") .collection('comments')
.orderBy("createdAt", "desc") .orderBy('createdAt', 'desc')
: null; : null;
} catch { } catch {
return null; return null;
@@ -113,26 +113,31 @@ const CommentsBottomSheet = () => {
}); });
const onSend = async () => { const onSend = async () => {
const value = (text || "").trim(); const value = (text || '').trim();
if (!value || !projectId || !currentUID) return; if (!value || !projectId || !currentUID) return;
try { try {
setText(""); setText('');
const docRef = await projectsRef const docRef = await projectsRef
.doc(projectId) .doc(projectId)
.collection("comments") .collection('comments')
.add({ .add({
userId: currentUID, userId: currentUID,
userName: currentUserData?.userName || "", userName: currentUserData?.userName || '',
profilePicture: currentUserData?.profilePictureURL || "", profilePicture: currentUserData?.profilePictureURL || '',
text: value, text: value,
createdAt: serverTimestamp(), createdAt: serverTimestamp(),
}); });
try {
await projectsRef
.doc(projectId)
.set({ commentsCount: increment(1) }, { merge: true });
} catch {}
// Optimistic: ajouter immédiatement le commentaire en tête de liste // Optimistic: ajouter immédiatement le commentaire en tête de liste
const optimistic = { const optimistic = {
id: docRef?.id || Math.random().toString(36).slice(2), id: docRef?.id || Math.random().toString(36).slice(2),
userId: currentUID, userId: currentUID,
userName: currentUserData?.userName || "", userName: currentUserData?.userName || '',
profilePicture: currentUserData?.profilePictureURL || "", profilePicture: currentUserData?.profilePictureURL || '',
text: value, text: value,
createdAt: new Date(), createdAt: new Date(),
}; };
@@ -187,15 +192,14 @@ const CommentsBottomSheet = () => {
bottomInset={insets.bottom} bottomInset={insets.bottom}
backdropComponent={renderBackdrop} backdropComponent={renderBackdrop}
handleIndicatorStyle={{ backgroundColor: Palette.white }} handleIndicatorStyle={{ backgroundColor: Palette.white }}
backgroundStyle={{ backgroundColor: "transparent" }} backgroundStyle={{ backgroundColor: 'transparent' }}
onDismiss={() => { onDismiss={() => {
setProjectId(null); setProjectId(null);
setTyping(false); setTyping(false);
setText(""); setText('');
}} }}>
>
<BlurView <BlurView
intensity={Platform.OS !== "ios" ? 10 : 20} intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{ style={{
flex: 1, flex: 1,
backgroundColor: Palette.glass, backgroundColor: Palette.glass,
@@ -209,9 +213,8 @@ const CommentsBottomSheet = () => {
<View style={{ paddingTop: 10, paddingBottom: 8 }}> <View style={{ paddingTop: 10, paddingBottom: 8 }}>
<Pressable <Pressable
onPress={() => modalRef.current?.dismiss?.()} onPress={() => modalRef.current?.dismiss?.()}
style={{ position: "absolute", right: 10, top: 8, padding: 6 }} style={{ position: 'absolute', right: 10, top: 8, padding: 6 }}
hitSlop={8} hitSlop={8}>
>
<Image <Image
source={icons.close} source={icons.close}
style={{ width: 22, height: 22, tintColor: Palette.white }} style={{ width: 22, height: 22, tintColor: Palette.white }}
@@ -223,9 +226,8 @@ const CommentsBottomSheet = () => {
fontSize: 22, fontSize: 22,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
alignSelf: "center", alignSelf: 'center',
}} }}>
>
Commentaires Commentaires
</Text> </Text>
</View> </View>
@@ -238,14 +240,12 @@ const CommentsBottomSheet = () => {
paddingHorizontal: 14, paddingHorizontal: 14,
paddingBottom: (insets?.bottom || 0) + 12, paddingBottom: (insets?.bottom || 0) + 12,
}} }}
showsVerticalScrollIndicator showsVerticalScrollIndicator>
>
{Array.isArray(comments) && comments.length > 0 ? ( {Array.isArray(comments) && comments.length > 0 ? (
comments.map((c) => ( comments.map((c) => (
<View <View
key={c.id} key={c.id}
style={{ flexDirection: "row", gap: 12, marginBottom: 14 }} style={{ flexDirection: 'row', gap: 12, marginBottom: 14 }}>
>
{c?.profilePicture ? ( {c?.profilePicture ? (
<ExpoImage <ExpoImage
source={{ uri: c.profilePicture }} source={{ uri: c.profilePicture }}
@@ -260,26 +260,24 @@ const CommentsBottomSheet = () => {
style={{ style={{
...size({ size: 42 }), ...size({ size: 42 }),
borderRadius: 100, borderRadius: 100,
backgroundColor: "#FFFFFF22", backgroundColor: '#FFFFFF22',
}} }}
/> />
)} )}
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<View <View
style={{ style={{
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
gap: 6, gap: 6,
}} }}>
>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
}} }}>
> {c?.userId === currentUID ? 'vous' : c?.userName || ''}
{c?.userId === currentUID ? "vous" : c?.userName || ""}
</Text> </Text>
<Text <Text
style={{ style={{
@@ -287,8 +285,7 @@ const CommentsBottomSheet = () => {
color: Palette.white, color: Palette.white,
opacity: 0.75, opacity: 0.75,
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}>
>
{formatRelativeTime(c?.createdAt)} {formatRelativeTime(c?.createdAt)}
</Text> </Text>
</View> </View>
@@ -297,8 +294,7 @@ const CommentsBottomSheet = () => {
fontSize: 14, fontSize: 14,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}>
>
{c?.text} {c?.text}
</Text> </Text>
</View> </View>
@@ -311,10 +307,9 @@ const CommentsBottomSheet = () => {
color: Palette.white, color: Palette.white,
opacity: 0.8, opacity: 0.8,
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center", textAlign: 'center',
marginTop: 6, marginTop: 6,
}} }}>
>
Aucun commentaire pour le moment Aucun commentaire pour le moment
</Text> </Text>
)} )}
@@ -322,14 +317,14 @@ const CommentsBottomSheet = () => {
{/* INPUT en bas du contenu */} {/* INPUT en bas du contenu */}
<View style={{ height: 12 }} /> <View style={{ height: 12 }} />
<BlurView <BlurView
intensity={Platform.OS !== "ios" ? 10 : 20} intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{ style={{
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
gap: 10, gap: 10,
paddingHorizontal: 16, paddingHorizontal: 16,
backgroundColor: Palette.glass, backgroundColor: Palette.glass,
overflow: "hidden", overflow: 'hidden',
height: 54, height: 54,
borderRadius: 16, borderRadius: 16,
}} }}
+69 -63
View File
@@ -1,33 +1,33 @@
import { useIsFocused, useRoute } from "@react-navigation/native"; import { useIsFocused, useRoute } from '@react-navigation/native';
import { useAudioPlayer } from "expo-audio"; import { useAudioPlayer } from 'expo-audio';
import { BlurView } from "expo-blur"; import { BlurView } from 'expo-blur';
import { VideoView, useVideoPlayer } from "expo-video"; import { VideoView, useVideoPlayer } from 'expo-video';
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { Image, Platform, Pressable, Share, Text, View } from "react-native"; import { Image, Platform, Pressable, Share, Text, View } from 'react-native';
import Carousel from "react-native-reanimated-carousel"; import Carousel from 'react-native-reanimated-carousel';
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from 'react-native-responsive-dimensions';
import { icons, img } from "../assets"; import { icons, img } from '../assets';
import { openComments } from "../components/bottomsheets/CommentsBottomSheet"; import { openComments } from '../components/bottomsheets/CommentsBottomSheet';
import KaraokeLyrics from "../components/KaraokeLyrics"; import KaraokeLyrics from '../components/KaraokeLyrics';
import { import {
arrayRemove, arrayRemove,
arrayUnion, arrayUnion,
projectsRef, projectsRef,
usersRef, usersRef,
} from "../config/firebase"; } from '../config/firebase';
import useDataFromRef from "../hooks/useDataFromRef"; import useDataFromRef from '../hooks/useDataFromRef';
import { Routes } from "../navigation"; import { Routes } from '../navigation';
import { navigate } from "../navigation/NavigationService"; import { navigate } from '../navigation/NavigationService';
import { useUser } from "../providers/UserDataProvider"; import { useUser } from '../providers/UserDataProvider';
import { Palette } from "../styles"; import { Palette } from '../styles';
import { FONT_FAMILY } from "../styles/Fonts"; import { FONT_FAMILY } from '../styles/Fonts';
import { size } from "../styles/Style"; import { size } from '../styles/Style';
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const { currentUID, followUser, unfollowUser } = useUser() || {}; const { currentUID, followUser, unfollowUser } = useUser() || {};
@@ -45,6 +45,11 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
); );
const [likesCount, setLikesCount] = useState(initialLikedBy.length); const [likesCount, setLikesCount] = useState(initialLikedBy.length);
const commentsCount = useMemo(
() => Number(item?.commentsCount || 0),
[item?.commentsCount]
);
const [owner, setOwner] = useState( const [owner, setOwner] = useState(
item?.userId && userCache?.current?.get(item.userId) item?.userId && userCache?.current?.get(item.userId)
? userCache.current.get(item.userId) ? userCache.current.get(item.userId)
@@ -171,7 +176,7 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []; const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
return arr.map((w) => ({ return arr.map((w) => ({
word: String(w?.word ?? ""), word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0), startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0), endS: Number(w?.endS ?? 0),
})); }));
@@ -197,47 +202,43 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
<View <View
style={{ style={{
height: responsiveHeight(100), height: responsiveHeight(100),
position: "relative", position: 'relative',
backgroundColor: "black", backgroundColor: 'black',
}} }}>
>
{!!videoUrl ? ( {!!videoUrl ? (
<VideoView <VideoView
player={videoPlayer} player={videoPlayer}
nativeControls={false} nativeControls={false}
contentFit="cover" contentFit="cover"
style={{ width: "100%", height: "100%" }} style={{ width: '100%', height: '100%' }}
/> />
) : ( ) : (
<Image <Image
source={img.placeholder3} source={img.placeholder3}
style={{ width: "100%", height: "100%" }} style={{ width: '100%', height: '100%' }}
/> />
)} )}
{/* Right side actions */} {/* Right side actions */}
<View <View
style={{ style={{
position: "absolute", position: 'absolute',
width: "100%", width: '100%',
bottom: 130, bottom: 130,
gap: 18, gap: 18,
}} }}>
>
<View <View
style={{ style={{
alignSelf: "flex-end", alignSelf: 'flex-end',
alignItems: "center", alignItems: 'center',
gap: 20, gap: 20,
paddingHorizontal: 13, paddingHorizontal: 13,
}} }}>
> <View style={{ gap: 6, alignItems: 'center' }}>
<View style={{ gap: 6, alignItems: "center" }}>
<Pressable <Pressable
onPress={() => { onPress={() => {
navigate(Routes.SingerProfile, { userId: item?.userId }); navigate(Routes.SingerProfile, { userId: item?.userId });
}} }}>
>
{owner?.profilePictureURL ? ( {owner?.profilePictureURL ? (
<Image <Image
source={{ uri: owner.profilePictureURL }} source={{ uri: owner.profilePictureURL }}
@@ -278,19 +279,18 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
// rollback on failure // rollback on failure
setIsFollowing((v) => !v); setIsFollowing((v) => !v);
} }
}} }}>
>
<BlurView <BlurView
tint="dark" tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20} intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{ style={{
paddingVertical: 6, paddingVertical: 6,
paddingHorizontal: 12, paddingHorizontal: 12,
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderColor: Palette.white, borderColor: Palette.white,
overflow: "hidden", overflow: 'hidden',
backgroundColor: isFollowing ? "#FFFFFF1A" : undefined, backgroundColor: isFollowing ? '#FFFFFF1A' : undefined,
}} }}
// experimentalBlurMethod={ // experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -301,9 +301,8 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
fontSize: 13, fontSize: 13,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
}} }}>
> {isFollowing ? 'Ne plus suivre' : 'Suivre'}
{isFollowing ? "Ne plus suivre" : "Suivre"}
</Text> </Text>
</BlurView> </BlurView>
</Pressable> </Pressable>
@@ -333,8 +332,7 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1))); setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)));
} }
}} }}
style={{ alignItems: "center" }} style={{ alignItems: 'center' }}>
>
<Image <Image
source={isLiked ? icons.heart : icons.heartOutline} source={isLiked ? icons.heart : icons.heartOutline}
style={size({ size: 26 })} style={size({ size: 26 })}
@@ -347,24 +345,34 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
fontSize: 11, fontSize: 11,
marginTop: 4, marginTop: 4,
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center", textAlign: 'center',
}} }}>
>
{likesCount} {likesCount}
</Text> </Text>
)} )}
</Pressable> </Pressable>
<Pressable <Pressable
onPress={() => item?.id && openComments(item.id)} onPress={() => item?.id && openComments(item.id)}
style={{ alignItems: "center" }} style={{ alignItems: 'center' }}>
>
<Image source={icons.chatBubble} style={size({ size: 26 })} /> <Image source={icons.chatBubble} style={size({ size: 26 })} />
{!!commentsCount && (
<Text
style={{
color: Palette.white,
fontSize: 11,
marginTop: 4,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: 'center',
}}>
{commentsCount}
</Text>
)}
</Pressable> </Pressable>
<Pressable <Pressable
onPress={async () => { onPress={async () => {
try { try {
const url = item?.playbackUrl || ""; const url = item?.playbackUrl || '';
const title = item?.title || "Partager"; const title = item?.title || 'Partager';
const base = item?.title const base = item?.title
? `Découvre « ${item.title} » sur MusicLand` ? `Découvre « ${item.title} » sur MusicLand`
: `Découvre ce playback sur MusicLand`; : `Découvre ce playback sur MusicLand`;
@@ -377,21 +385,20 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
}) })
); );
} catch (e) {} } catch (e) {}
}} }}>
>
<Image source={icons.share} style={size({ size: 26 })} /> <Image source={icons.share} style={size({ size: 26 })} />
</Pressable> </Pressable>
</View> </View>
<View style={{ paddingHorizontal: 28 }}> <View style={{ paddingHorizontal: 28 }}>
<BlurView <BlurView
tint="dark" tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20} intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{ style={{
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 8, paddingVertical: 8,
borderRadius: 20, borderRadius: 20,
backgroundColor: Palette.glass, backgroundColor: Palette.glass,
overflow: "hidden", overflow: 'hidden',
}} }}
// experimentalBlurMethod={ // experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -408,9 +415,8 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
fontSize: 12, fontSize: 12,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}>
> {item?.title || 'Description chanson'}
{item?.title || "Description chanson"}
</Text> </Text>
)} )}
</BlurView> </BlurView>
@@ -430,7 +436,7 @@ const Playbacks = () => {
const { getUserByUid } = useUser() || {}; const { getUserByUid } = useUser() || {};
const isFocused = useIsFocused(); const isFocused = useIsFocused();
const { data: playbacks = [], loadMore } = useDataFromRef({ const { data: playbacks = [], loadMore } = useDataFromRef({
ref: projectsRef.where("playbackUrl", "!=", null), ref: projectsRef.where('playbackUrl', '!=', null),
simpleRef: false, simpleRef: false,
listener: false, listener: false,
usePagination: true, usePagination: true,
@@ -469,7 +475,7 @@ const Playbacks = () => {
}, [focusProjectId, playbacks, loadMore]); }, [focusProjectId, playbacks, loadMore]);
return ( return (
<View style={{ flex: 1, backgroundColor: "black" }}> <View style={{ flex: 1, backgroundColor: 'black' }}>
<Carousel <Carousel
ref={carouselRef} ref={carouselRef}
data={playbacks} data={playbacks}