Files
musicland/src/components/bottomsheets/CommentsBottomSheet.js
T
2025-10-23 16:07:52 +02:00

387 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,
BottomSheetTextInput,
} from '@gorhom/bottom-sheet';
import { BlurView } from 'expo-blur';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Image,
Platform,
Pressable,
Text,
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 { getArtistDisplayName } from '../../utils/artistName';
import { Palette } from '../../styles';
import { FONT_FAMILY } from '../../styles/Fonts';
import ProfilePicture from '../ProfilePicture';
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 currentUserDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, ''),
[currentUserData]
);
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 INPUT_HEIGHT = 54;
const INPUT_MARGIN_VERTICAL = 12;
const bottomPadding = useMemo(
() => INPUT_HEIGHT + INPUT_MARGIN_VERTICAL + (insets?.bottom || 0) + 12,
[insets?.bottom]
);
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: currentUserDisplayName,
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: currentUserDisplayName,
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>
<View style={{ flex: 1 }}>
<BottomSheetScrollView
ref={scrollRef}
keyboardShouldPersistTaps='handled'
keyboardDismissMode='interactive'
onContentSizeChange={() => {
if (typing) {
try {
scrollRef.current?.scrollToEnd?.({ animated: true });
} catch {}
}
}}
onScroll={handleScroll}
scrollEventThrottle={100}
contentContainerStyle={{
paddingHorizontal: 14,
paddingBottom: bottomPadding,
}}
showsVerticalScrollIndicator>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c) => (
<View
key={c.id}
style={{ flexDirection: 'row', gap: 12, marginBottom: 14 }}>
<ProfilePicture uri={c?.profilePicture || null} size={42} />
<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>
)}
</BottomSheetScrollView>
<View
style={{
position: 'absolute',
left: 14,
right: 14,
bottom: (insets?.bottom || 0) + INPUT_MARGIN_VERTICAL,
}}>
<BlurView
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingHorizontal: 16,
backgroundColor: Palette.glass,
overflow: 'hidden',
height: INPUT_HEIGHT,
borderRadius: 16,
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<BottomSheetTextInput
placeholder='Ajouter un commentaire'
placeholderTextColor='#FFFFFFAA'
value={text}
onChangeText={(t) => {
setText(t);
}}
onFocus={() => {
setTyping(true);
}}
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>
</View>
</KeyboardAvoidingView>
</BlurView>
</BottomSheetModal>
);
};
export default CommentsBottomSheet;