389 lines
12 KiB
JavaScript
389 lines
12 KiB
JavaScript
// 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' // l’input 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;
|