follow users

This commit is contained in:
2025-08-29 14:11:55 +02:00
parent 0699ac9ea8
commit 5473beb988
6 changed files with 229 additions and 55 deletions
+1
View File
@@ -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");
+48 -2
View File
@@ -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}
+90 -35
View File
@@ -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 = () => {
<ResearchHeader
onPress={(item) => onPressMenu(item)}
selected={selected}
searchValue={search}
onChangeSearch={setSearch}
/>
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingTop: 2 }}>
@@ -113,33 +135,66 @@ const Research = () => {
{(!selected || selected === "Profils") && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<View style={{ gap: 8 }}>
{Array.from({ length: 2 }).map((_, index) => (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
key={index}
onPress={() => navigate(Routes.SingerProfile)}
{queryText.length === 0 && (
<Text
style={{
fontSize: 12,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Commence à taper pour rechercher des profils
</Text>
)}
{queryText.length > 0 &&
Array.isArray(userResults) &&
userResults.map((u) => (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
key={u.id}
onPress={() =>
navigate(Routes.SingerProfile, { userId: u.id })
}
>
<Image
source={
u?.photoURL ? { uri: u.photoURL } : img.profile
}
style={{ ...size({ size: 60 }), borderRadius: 100 }}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{u?.userName || "Utilisateur"}
</Text>
</Pressable>
))}
{queryText.length > 0 && userResults.length > 0 && (
<Pressable
onPress={loadMoreUsers}
style={{ alignSelf: "center", marginTop: 6 }}
>
<Image
source={img.profile}
style={{ ...size({ size: 60 }), borderRadius: 100 }}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: usersLoading ? 0.6 : 1,
}}
>
ReggaetonLil
{usersLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
))}
)}
</View>
</CreateLyricsHeader>
</View>
@@ -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 (
<View style={{ gap: 12 }}>
<SearchBar />
<SearchBar
textInputProps={{
value: searchValue,
onChangeText: onChangeSearch,
}}
/>
<View
style={{
...Style.containerRow,
+63 -16
View File
@@ -1,5 +1,5 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import {
FlatList,
Image,
@@ -14,14 +14,46 @@ import { background, icons, img } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import GradientButton from "../../components/GradientButton";
import Page from "../../layouts/Page";
import { useUser } from "../../providers/UserDataProvider";
import { getFollowersCount, getFollowingCount } from "../../services/follows";
import { useGlobal } from "reactn";
import { followsRef } from "../../config/firebase";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import MusicCard from "../Library/components/MusicCard";
const SingerProfile = () => {
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 (
<Page
backgroundImg={background.profileBG}
@@ -74,7 +118,7 @@ const SingerProfile = () => {
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
elia.mrn
{userData?.userName}
</Text>
</View>
<View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}>
@@ -83,22 +127,25 @@ const SingerProfile = () => {
<Text style={styles.label}>playbacks</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>880</Text>
<Text style={styles.value}>{followers}</Text>
<Text style={styles.label}>abonnés</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>6</Text>
<Text style={styles.value}>{following}</Text>
<Text style={styles.label}>abonnements</Text>
</View>
</View>
<GradientButton
title="Suivre"
containerStyle={{
width: "80%",
alignSelf: "center",
marginTop: 18,
}}
/>
{currentUID !== userId && (
<GradientButton
onPress={handleFollowUser}
title={isFollowing ? "Ne plus suivre" : "Suivre"}
containerStyle={{
width: "80%",
alignSelf: "center",
marginTop: 18,
}}
/>
)}
</BlurView>
</View>
<View
+20
View File
@@ -0,0 +1,20 @@
import { followsRef } from "../config/firebase";
export const getFollowersCount = async (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;
};