Files
musicland/src/components/bottomsheets/CommentsBottomSheet.js
T
2025-09-17 10:13:12 +02:00

389 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CommentsBottomSheet.jsx
import {
BottomSheetBackdrop,
BottomSheetModal,
BottomSheetScrollView,
} from '@gorhom/bottom-sheet';
import { BlurView } from 'expo-blur';
import { Image as ExpoImage } from 'expo-image';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Image,
Platform,
Pressable,
Text,
TextInput,
View,
KeyboardAvoidingView,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { icons } from '../../assets';
import { increment, projectsRef, serverTimestamp } from '../../config/firebase';
import useDataFromRef from '../../hooks/useDataFromRef';
import { useUser } from '../../providers/UserDataProvider';
import { Palette } from '../../styles';
import { FONT_FAMILY } from '../../styles/Fonts';
import { size } from '../../styles/Style';
let externalOpen;
let externalClose;
export const openComments = (projectId) => {
if (typeof externalOpen === 'function') externalOpen(projectId);
};
export const closeComments = () => {
if (typeof externalClose === 'function') externalClose();
};
const formatRelativeTime = (date) => {
try {
const d = date instanceof Date ? date : date?.toDate?.() || null;
if (!d) return '';
const diff = Math.max(0, Date.now() - d.getTime());
const sec = Math.floor(diff / 1000);
if (sec < 60) return `${sec}s`;
const min = Math.floor(sec / 60);
if (min < 60) return `${min}min`;
const h = Math.floor(min / 60);
if (h < 24) return `${h}h`;
const dA = Math.floor(h / 24);
return `${dA}j`;
} catch (e) {
return '';
}
};
const CommentsBottomSheet = () => {
const modalRef = useRef(null);
const insets = useSafeAreaInsets();
const { currentUID, currentUserData } = useUser();
const scrollRef = useRef(null);
const [projectId, setProjectId] = useState(null);
const [text, setText] = useState('');
const [typing, setTyping] = useState(false);
useEffect(() => {
externalOpen = (pid) => {
setProjectId(pid || null);
requestAnimationFrame(() => modalRef.current?.present());
};
externalClose = () => {
try {
modalRef.current?.dismiss?.();
} catch {}
};
return () => {
externalOpen = undefined;
externalClose = undefined;
};
}, []);
const snapPoints = useMemo(() => ['90%'], []);
const commentsRef = useMemo(() => {
try {
return projectId
? projectsRef
.doc(projectId)
.collection('comments')
.orderBy('createdAt', 'desc')
: null;
} catch {
return null;
}
}, [projectId]);
const {
data: comments = [],
setData: setComments,
loadMore,
loading: loadingComments,
} = useDataFromRef({
ref: commentsRef,
simpleRef: false,
listener: false,
condition: !!commentsRef,
refreshArray: [projectId],
usePagination: true,
batchSize: 20,
});
const onSend = async () => {
const value = (text || '').trim();
if (!value || !projectId || !currentUID) return;
try {
setText('');
const docRef = await projectsRef
.doc(projectId)
.collection('comments')
.add({
userId: currentUID,
userName: currentUserData?.userName || '',
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
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
const optimistic = {
id: docRef?.id || Math.random().toString(36).slice(2),
userId: currentUID,
userName: currentUserData?.userName || '',
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: new Date(),
};
setComments((prev = []) => [
optimistic,
...prev.filter((x) => x?.id !== optimistic.id),
]);
} catch {
// ignore
}
};
const renderBackdrop = useCallback(
(props) => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
pressBehavior='close'
/>
),
[]
);
const handleScroll = useCallback(
({ nativeEvent }) => {
try {
const { layoutMeasurement, contentOffset, contentSize } =
nativeEvent || {};
const paddingToBottom = 80;
if (
layoutMeasurement?.height + contentOffset?.y >=
(contentSize?.height || 0) - paddingToBottom
) {
loadMore?.();
}
} catch {}
},
[loadMore]
);
return (
<BottomSheetModal
ref={modalRef}
snapPoints={snapPoints}
keyboardBehavior='interactive' // linput est dans le contenu → mode interactif OK
keyboardBlurBehavior='restore'
enablePanDownToClose
enableContentPanningGesture
stackBehavior='push'
topInset={insets.top}
bottomInset={insets.bottom}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ backgroundColor: Palette.white }}
backgroundStyle={{ backgroundColor: 'transparent' }}
onDismiss={() => {
setProjectId(null);
setTyping(false);
setText('');
}}>
<BlurView
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
flex: 1,
backgroundColor: Palette.glass,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={(insets?.bottom || 0) + 36}>
<View style={{ paddingTop: 10, paddingBottom: 8 }}>
<Pressable
onPress={() => modalRef.current?.dismiss?.()}
style={{ position: 'absolute', right: 10, top: 8, padding: 6 }}
hitSlop={8}>
<Image
source={icons.close}
style={{ width: 22, height: 22, tintColor: Palette.white }}
resizeMode='contain'
/>
</Pressable>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
alignSelf: 'center',
}}>
Commentaires
</Text>
</View>
<BottomSheetScrollView
ref={scrollRef}
keyboardShouldPersistTaps='handled'
onContentSizeChange={() => {
if (typing) {
try {
scrollRef.current?.scrollToEnd?.({ animated: true });
} catch {}
}
}}
onScroll={handleScroll}
scrollEventThrottle={100}
contentContainerStyle={{
paddingHorizontal: 14,
paddingBottom: (insets?.bottom || 0) + 40,
}}
showsVerticalScrollIndicator>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c) => (
<View
key={c.id}
style={{ flexDirection: 'row', gap: 12, marginBottom: 14 }}>
{c?.profilePicture ? (
<ExpoImage
source={{ uri: c.profilePicture }}
cachePolicy='memory-disk'
priority='high'
contentFit='cover'
transition={100}
style={{ ...size({ size: 42 }), borderRadius: 100 }}
/>
) : (
<View
style={{
...size({ size: 42 }),
borderRadius: 100,
backgroundColor: '#FFFFFF22',
}}
/>
)}
<View style={{ flex: 1 }}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 6,
}}>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}>
{c?.userId === currentUID ? 'vous' : c?.userName || ''}
</Text>
<Text
style={{
fontSize: 12,
color: Palette.white,
opacity: 0.75,
fontFamily: FONT_FAMILY.InterRegular,
}}>
{formatRelativeTime(c?.createdAt)}
</Text>
</View>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}>
{c?.text}
</Text>
</View>
</View>
))
) : (
<Text
style={{
fontSize: 14,
color: Palette.white,
opacity: 0.8,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: 'center',
marginTop: 6,
}}>
Aucun commentaire pour le moment
</Text>
)}
{/* INPUT en bas du contenu */}
<View style={{ height: 12 }} />
<BlurView
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingHorizontal: 16,
backgroundColor: Palette.glass,
overflow: 'hidden',
height: 54,
borderRadius: 16,
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<TextInput
placeholder='Ajouter un commentaire'
placeholderTextColor='#FFFFFFAA'
value={text}
onChangeText={(t) => {
setText(t);
}}
onFocus={() => {
setTyping(true);
setTimeout(() => {
try {
scrollRef.current?.scrollToEnd?.({ animated: true });
} catch {}
}, 50);
}}
onBlur={() => setTyping(false)}
style={{
flex: 1,
color: Palette.white,
fontSize: 14,
fontFamily: FONT_FAMILY.InterRegular,
}}
/>
<Pressable onPress={onSend}>
<Image
source={icons.send}
style={{ width: 24, height: 24, tintColor: Palette.white }}
resizeMode='contain'
/>
</Pressable>
</BlurView>
<View style={{ height: (insets?.bottom || 0) + 24 }} />
</BottomSheetScrollView>
</KeyboardAvoidingView>
</BlurView>
</BottomSheetModal>
);
};
export default CommentsBottomSheet;