From 5473beb988ad6a1ba7b61235ea67f34b9ec5fb31 Mon Sep 17 00:00:00 2001 From: leon-morival Date: Fri, 29 Aug 2025 14:11:55 +0200 Subject: [PATCH] follow users --- src/config/firebase.js | 1 + src/providers/UserDataProvider.js | 50 ++++++- src/screens/Library/Research.js | 125 +++++++++++++----- .../Library/components/ResearchHeader.js | 9 +- src/screens/Profile/SingerProfile.js | 79 ++++++++--- src/services/follows.js | 20 +++ 6 files changed, 229 insertions(+), 55 deletions(-) create mode 100644 src/services/follows.js diff --git a/src/config/firebase.js b/src/config/firebase.js index 08e018d..6ec7adf 100644 --- a/src/config/firebase.js +++ b/src/config/firebase.js @@ -49,6 +49,7 @@ try { export const usersRef = firestore.collection("users"); export const projectsRef = firestore.collection("projects"); export const playlistsRef = firestore.collection("playlists"); +export const followsRef = firestore.collection("follows"); export const notificationsRef = firestore.collection("notifications"); export const documentsRef = firestore.collection("documents"); export const chatsRef = firestore.collection("chats"); diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index ad3ced0..b7e8a0b 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -6,6 +6,7 @@ import { createContext, useContext, useGlobal } from "reactn"; import { useState } from "react"; import { checkIfEmailIsValid } from "../actions/signupActions"; import firebase, { + followsRef, playlistsRef, projectsRef, usersRef, @@ -26,7 +27,6 @@ export default ({ children }) => { const [userProjects, setUserProjects] = useState([]); const [userLikedProjects, setUserLikedProjects] = useState([]); const [userPlaylists, setUserPlaylists] = useState([]); - useDataFromRef({ ref: currentUID ? usersRef.doc(currentUID) : null, simpleRef: true, @@ -86,6 +86,50 @@ export default ({ children }) => { ...newData, })); }; + const followUser = async (userId) => { + try { + if (currentUID && userId) { + await followsRef.doc(`${currentUID}_${userId}`).set( + { + followerId: currentUID, + followeeId: userId, + createdAt: firebase.firestore.FieldValue.serverTimestamp(), + }, + { merge: true } + ); + setTooltip({ type: "success", text: "Abonnement mis à jour" }); + } + } catch (e) { + console.log(e); + setTooltip({ type: "error", text: e?.message || "Action impossible" }); + } + }; + + const unfollowUser = async (userId) => { + try { + if (currentUID && userId) { + await followsRef.doc(`${currentUID}_${userId}`).delete(); + setTooltip({ type: "success", text: "Vous ne suivez plus cet utilisateur" }); + } + } catch (e) { + console.log(e); + setTooltip({ type: "error", text: e?.message || "Action impossible" }); + } + }; + const getUserByUid = async (uid) => { + try { + const userDoc = await usersRef.doc(uid).get(); + return userDoc.exists + ? { + id: userDoc.id, + ...userDoc.data(), + } + : null; + } catch (e) { + console.log(e); + return null; + } + }; const updateUserData = async ({ data, shouldSetTooltip = true }) => { try { @@ -193,13 +237,15 @@ export default ({ children }) => { userPlaylists, setCurrentUserData, - + getUserByUid, onSignOut, pendingUserData, updatePendingUserData, updateUserData, + followUser, + unfollowUser, }} > {children} diff --git a/src/screens/Library/Research.js b/src/screens/Library/Research.js index 2f4eca3..664ac05 100644 --- a/src/screens/Library/Research.js +++ b/src/screens/Library/Research.js @@ -1,29 +1,49 @@ -import { - View, - Text, - StyleSheet, - Pressable, - ScrollView, - Image, -} from "react-native"; -import React, { useState } from "react"; -import Page from "../../layouts/Page"; +import React, { useMemo, useState } from "react"; +import { Image, Pressable, ScrollView, Text, View } from "react-native"; import { background, img } from "../../assets"; -import { gutters, Palette, Style } from "../../styles"; -import SearchBar from "../../components/SearchBar"; -import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; -import { FONT_FAMILY } from "../../styles/Fonts"; -import BorderGradient from "../../components/BorderGradient/BorderGradient"; -import { BlurView } from "expo-blur"; -import ResearchHeader from "./components/ResearchHeader"; -import MusicCard from "./components/MusicCard"; import MoreMenu from "../../components/MoreMenu"; -import { size } from "../../styles/Style"; -import { navigate } from "../../navigation/NavigationService"; +import { usersRef } from "../../config/firebase"; +import useDataFromRef from "../../hooks/useDataFromRef"; +import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; +import { navigate } from "../../navigation/NavigationService"; +import { gutters, Palette, Style } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; +import { size } from "../../styles/Style"; +import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; +import MusicCard from "./components/MusicCard"; +import ResearchHeader from "./components/ResearchHeader"; const Research = () => { const [selected, setSelected] = useState(null); + const [search, setSearch] = useState(""); + const queryText = useMemo(() => search.trim(), [search]); + + const usersQueryRef = useMemo(() => { + if (!queryText) return null; + try { + return usersRef + .orderBy("userName") + .startAt(queryText) + .endAt(queryText + "\uf8ff"); + } catch (e) { + return null; + } + }, [queryText]); + + const { + data: userResults = [], + loading: usersLoading, + loadMore: loadMoreUsers, + } = useDataFromRef({ + ref: usersQueryRef, + simpleRef: false, + listener: false, + condition: !!usersQueryRef && queryText.length > 0, + refreshArray: [queryText], + usePagination: true, + batchSize: 5, + }); const [menuTop, setMenuTop] = useState(0); const [showMenu, setShowMenu] = useState(false); const [selectedProjectId, setSelectedProjectId] = useState(null); @@ -49,6 +69,8 @@ const Research = () => { onPressMenu(item)} selected={selected} + searchValue={search} + onChangeSearch={setSearch} /> @@ -113,33 +135,66 @@ const Research = () => { {(!selected || selected === "Profils") && ( - {Array.from({ length: 2 }).map((_, index) => ( - navigate(Routes.SingerProfile)} + {queryText.length === 0 && ( + + Commence à taper pour rechercher des profils… + + )} + + {queryText.length > 0 && + Array.isArray(userResults) && + userResults.map((u) => ( + + navigate(Routes.SingerProfile, { userId: u.id }) + } + > + + + {u?.userName || "Utilisateur"} + + + ))} + + {queryText.length > 0 && userResults.length > 0 && ( + - - ReggaetonLil + {usersLoading ? "Chargement…" : "Charger plus"} - ))} + )} diff --git a/src/screens/Library/components/ResearchHeader.js b/src/screens/Library/components/ResearchHeader.js index c049144..78e6191 100644 --- a/src/screens/Library/components/ResearchHeader.js +++ b/src/screens/Library/components/ResearchHeader.js @@ -6,10 +6,15 @@ import BorderGradient from "../../../components/BorderGradient/BorderGradient"; import { BlurView } from "expo-blur"; import { FONT_FAMILY } from "../../../styles/Fonts"; -const ResearchHeader = ({ onPress, selected }) => { +const ResearchHeader = ({ onPress, selected, searchValue = "", onChangeSearch = () => {} }) => { return ( - + { +const SingerProfile = ({ route }) => { + const { userId } = route.params; + const { getUserByUid, followUser, unfollowUser } = useUser(); + const [currentUID] = useGlobal("currentUID"); + console.log("user id ", userId); const [selected, setSelected] = useState(null); - + const [userData, setUserData] = useState(null); + const [following, setFollowing] = useState(0); + const [followers, setFollowers] = useState(0); + const fetchUserData = async () => { + const data = userId ? await getUserByUid(userId) : {}; + setUserData(data); + }; + useEffect(() => { + fetchUserData(); + }, [userId]); + useEffect(() => { + if (!userId) return; + getFollowersCount(userId).then(setFollowers); + getFollowingCount(userId).then(setFollowing); + (async () => { + try { + if (currentUID && userId) { + const doc = await followsRef.doc(`${currentUID}_${userId}`).get(); + setIsFollowing(!!doc?.exists); + } else { + setIsFollowing(false); + } + } catch (_) {} + })(); + }, [userId, currentUID]); + console.log("following", following); const onPressMenu = (item) => { if (selected === item) { setSelected(null); @@ -29,7 +61,19 @@ const SingerProfile = () => { setSelected(item); } }; - + const [isFollowing, setIsFollowing] = useState(false); + const handleFollowUser = async () => { + if (!currentUID || !userId || currentUID === userId) return; + if (isFollowing) { + await unfollowUser(userId); + setIsFollowing(false); + setFollowers((v) => Math.max(0, (v || 0) - 1)); + } else { + await followUser(userId); + setIsFollowing(true); + setFollowers((v) => (v || 0) + 1); + } + }; return ( { fontFamily: FONT_FAMILY.InterSemiBold, }} > - elia.mrn + {userData?.userName} @@ -83,22 +127,25 @@ const SingerProfile = () => { playbacks - 880 + {followers} abonnés - 6 + {following} abonnements - + {currentUID !== userId && ( + + )} { + if (!userId) return 0; + const snap = await followsRef.where("followeeId", "==", userId).get(); + return snap?.size || 0; +}; + +export const getFollowingCount = async (userId) => { + if (!userId) return 0; + const snap = await followsRef.where("followerId", "==", userId).get(); + return snap?.size || 0; +}; + +export const unfollowUser = async ({ followerId, followeeId }) => { + if (!followerId || !followeeId) throw new Error("Missing followerId/followeeId"); + const id = `${followerId}_${followeeId}`; + await followsRef.doc(id).delete(); + return id; +};