comments
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
// 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,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { icons } from "../../assets";
|
||||
import { 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;
|
||||
export const openComments = (projectId) => {
|
||||
if (typeof externalOpen === "function") externalOpen(projectId);
|
||||
};
|
||||
|
||||
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 [projectId, setProjectId] = useState(null);
|
||||
const [text, setText] = useState("");
|
||||
const [typing, setTyping] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
externalOpen = (pid) => {
|
||||
setProjectId(pid || null);
|
||||
requestAnimationFrame(() => modalRef.current?.present());
|
||||
};
|
||||
return () => {
|
||||
externalOpen = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const snapPoints = useMemo(() => ["50%", "90%"], []);
|
||||
|
||||
const commentsRef = useMemo(() => {
|
||||
try {
|
||||
return projectId
|
||||
? projectsRef
|
||||
.doc(projectId)
|
||||
.collection("comments")
|
||||
.orderBy("createdAt", "desc")
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const { data: comments = [] } = useDataFromRef({
|
||||
ref: commentsRef,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!commentsRef,
|
||||
refreshArray: [projectId],
|
||||
});
|
||||
|
||||
const onSend = async () => {
|
||||
const value = (text || "").trim();
|
||||
if (!value || !projectId || !currentUID) return;
|
||||
try {
|
||||
setText("");
|
||||
await projectsRef
|
||||
.doc(projectId)
|
||||
.collection("comments")
|
||||
.add({
|
||||
userId: currentUID,
|
||||
userName: currentUserData?.userName || "",
|
||||
profilePicture: currentUserData?.profilePictureURL || "",
|
||||
text: value,
|
||||
createdAt: serverTimestamp(),
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
appearsOnIndex={0}
|
||||
disappearsOnIndex={-1}
|
||||
pressBehavior={typing ? "none" : "close"}
|
||||
/>
|
||||
),
|
||||
[typing]
|
||||
);
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={modalRef}
|
||||
snapPoints={snapPoints}
|
||||
keyboardBehavior="interactive" // l’input est dans le contenu → mode interactif OK
|
||||
keyboardBlurBehavior="restore"
|
||||
enablePanDownToClose={!typing} // évite un dismiss pendant la frappe
|
||||
enableContentPanningGesture={!typing}
|
||||
stackBehavior="push"
|
||||
topInset={insets.top}
|
||||
bottomInset={insets.bottom}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{ backgroundColor: Palette.white }}
|
||||
backgroundStyle={{ backgroundColor: "transparent" }}
|
||||
>
|
||||
<BlurView
|
||||
intensity={20}
|
||||
style={{ flex: 1, backgroundColor: Palette.glass }}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<View style={{ paddingTop: 10, paddingBottom: 8 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
Commentaires
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<BottomSheetScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 14,
|
||||
paddingBottom: (insets?.bottom || 0) + 12,
|
||||
}}
|
||||
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={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={setText}
|
||||
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 style={{ height: (insets?.bottom || 0) + 8 }} />
|
||||
</BottomSheetScrollView>
|
||||
</BlurView>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentsBottomSheet;
|
||||
Reference in New Issue
Block a user