new project modal & seperate playback and music likes
This commit is contained in:
@@ -339,15 +339,37 @@ exports.createProjectLikeNotification = onDocumentWritten(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeLikes = Array.isArray(before?.likedBy) ? before.likedBy : [];
|
const beforeSongLikes = Array.isArray(before?.likes?.song)
|
||||||
const afterLikes = Array.isArray(after?.likedBy) ? after.likedBy : [];
|
? before.likes.song
|
||||||
|
: [];
|
||||||
|
const afterSongLikes = Array.isArray(after?.likes?.song)
|
||||||
|
? after.likes.song
|
||||||
|
: [];
|
||||||
|
const beforePlaybackLikes = Array.isArray(before?.likes?.playback)
|
||||||
|
? before.likes.playback
|
||||||
|
: [];
|
||||||
|
const afterPlaybackLikes = Array.isArray(after?.likes?.playback)
|
||||||
|
? after.likes.playback
|
||||||
|
: [];
|
||||||
|
|
||||||
if (afterLikes.length <= beforeLikes.length) {
|
const beforeSongSet = new Set(beforeSongLikes);
|
||||||
return null;
|
const beforePlaybackSet = new Set(beforePlaybackLikes);
|
||||||
}
|
|
||||||
|
|
||||||
const beforeSet = new Set(beforeLikes);
|
const newSongLikers = afterSongLikes.filter(
|
||||||
const newLikers = afterLikes.filter((uid) => !beforeSet.has(uid));
|
(uid) => uid && !beforeSongSet.has(uid)
|
||||||
|
);
|
||||||
|
const newPlaybackLikers = afterPlaybackLikes.filter(
|
||||||
|
(uid) => uid && !beforePlaybackSet.has(uid)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newLikers = [];
|
||||||
|
|
||||||
|
newSongLikers.forEach((uid) => {
|
||||||
|
newLikers.push({ likerId: uid, likeType: "song" });
|
||||||
|
});
|
||||||
|
newPlaybackLikers.forEach((uid) => {
|
||||||
|
newLikers.push({ likerId: uid, likeType: "playback" });
|
||||||
|
});
|
||||||
|
|
||||||
if (!newLikers.length) {
|
if (!newLikers.length) {
|
||||||
return null;
|
return null;
|
||||||
@@ -359,7 +381,7 @@ exports.createProjectLikeNotification = onDocumentWritten(
|
|||||||
: "ton projet";
|
: "ton projet";
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
newLikers.map(async (likerId) => {
|
newLikers.map(async ({ likerId, likeType }) => {
|
||||||
if (!likerId || likerId === ownerId) {
|
if (!likerId || likerId === ownerId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -371,7 +393,9 @@ exports.createProjectLikeNotification = onDocumentWritten(
|
|||||||
? liker.userName.trim()
|
? liker.userName.trim()
|
||||||
: "Un utilisateur";
|
: "Un utilisateur";
|
||||||
|
|
||||||
const message = `${likerName} a aimé ton projet "${projectTitle}"`;
|
const isPlaybackLike = likeType === "playback";
|
||||||
|
const assetLabel = isPlaybackLike ? "ton playback" : "ta musique";
|
||||||
|
const message = `${likerName} a aimé ${assetLabel} "${projectTitle}"`;
|
||||||
|
|
||||||
await sendNotification({
|
await sendNotification({
|
||||||
sender: likerId,
|
sender: likerId,
|
||||||
@@ -384,6 +408,7 @@ exports.createProjectLikeNotification = onDocumentWritten(
|
|||||||
projectId,
|
projectId,
|
||||||
likerId,
|
likerId,
|
||||||
likerName,
|
likerName,
|
||||||
|
likeType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
Image,
|
||||||
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
|
import AppActionSheet from "../AppActionSheet";
|
||||||
|
import GradientButton from "../GradientButton";
|
||||||
|
import { img } from "../../assets";
|
||||||
|
import { Routes } from "../../navigation";
|
||||||
|
import { navigate } from "../../navigation/NavigationService";
|
||||||
|
import { useUserData } from "../../providers/UserDataProvider";
|
||||||
|
import { Palette } from "../../styles";
|
||||||
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
|
import Style, { size } from "../../styles/Style";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
|
|
||||||
|
const SHEET_ID = "PlaybackPicker";
|
||||||
|
|
||||||
|
const PlaybackPickerModal = () => {
|
||||||
|
const { userProjects = [], createNewProject } = useUserData();
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [webVisible, setWebVisible] = useState(true);
|
||||||
|
|
||||||
|
const sanitizedProjects = useMemo(() => {
|
||||||
|
if (!Array.isArray(userProjects)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProjects.filter((project) => {
|
||||||
|
if (!project || project.playbackUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!project?.songUrl;
|
||||||
|
});
|
||||||
|
}, [userProjects]);
|
||||||
|
const hasAnyMusic = Array.isArray(userProjects) && userProjects.length > 0;
|
||||||
|
|
||||||
|
const hideSheet = useCallback(() => {
|
||||||
|
if (isWeb) {
|
||||||
|
setWebVisible(false);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const maybePromise = SheetManager.hide(SHEET_ID);
|
||||||
|
return Promise.resolve(maybePromise);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[PlaybackPicker] hide error", error?.message || error);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}, [setWebVisible]);
|
||||||
|
|
||||||
|
const handleSelectProject = useCallback(
|
||||||
|
async (project) => {
|
||||||
|
if (!project?.id) return;
|
||||||
|
|
||||||
|
const hidePromise = hideSheet();
|
||||||
|
await Promise.race([
|
||||||
|
Promise.resolve(hidePromise),
|
||||||
|
new Promise((resolve) => setTimeout(resolve, 200)),
|
||||||
|
]).catch(() => {});
|
||||||
|
|
||||||
|
navigate(Routes.Playback, { project });
|
||||||
|
},
|
||||||
|
[hideSheet]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateNew = useCallback(async () => {
|
||||||
|
if (isCreating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
|
try {
|
||||||
|
let projectId = null;
|
||||||
|
if (typeof createNewProject === "function") {
|
||||||
|
projectId = await createNewProject({ hasLyrics: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePromise = hideSheet();
|
||||||
|
await Promise.race([
|
||||||
|
Promise.resolve(hidePromise),
|
||||||
|
new Promise((resolve) => setTimeout(resolve, 200)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
navigate(Routes.WritingLyrics, { projectId });
|
||||||
|
} else {
|
||||||
|
navigate(Routes.WritingLyrics);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[PlaybackPicker] create project failed", error);
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
}, [createNewProject, hideSheet, isCreating]);
|
||||||
|
|
||||||
|
const hasProjects = sanitizedProjects.length > 0;
|
||||||
|
|
||||||
|
if (isWeb && !webVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppActionSheet id={SHEET_ID} webModal onClose={hideSheet}>
|
||||||
|
<View style={{ gap: 18 }}>
|
||||||
|
<Text style={styles.title}>Ajouter un playback</Text>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Choisis une musique déjà créée pour y ajouter un playback ou lance un
|
||||||
|
nouveau projet.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{hasProjects ? (
|
||||||
|
<ScrollView
|
||||||
|
style={{ maxHeight: 320 }}
|
||||||
|
contentContainerStyle={{ gap: 12, paddingVertical: 4 }}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{sanitizedProjects.map((project) => {
|
||||||
|
const coverUri = project?.coverUrl || null;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={project.id}
|
||||||
|
onPress={() => handleSelectProject(project)}
|
||||||
|
style={styles.projectCard}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={coverUri ? { uri: coverUri } : img.placeholder}
|
||||||
|
style={styles.cover}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
<View style={{ flex: 1, gap: 4 }}>
|
||||||
|
<Text numberOfLines={1} style={styles.projectTitle}>
|
||||||
|
{project?.title || "Sans titre"}
|
||||||
|
</Text>
|
||||||
|
<Text numberOfLines={1} style={styles.projectSubtitle}>
|
||||||
|
{project?.userName || "Moi"}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.projectHint}>
|
||||||
|
Playback à créer
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
) : (
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={styles.emptyTitle}>
|
||||||
|
{hasAnyMusic
|
||||||
|
? "Aucune musique n'est prête pour l'étape playback"
|
||||||
|
: "Tu n'as pas encore de musique prête pour le playback"}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.emptyDescription}>
|
||||||
|
{hasAnyMusic
|
||||||
|
? "Complète d'abord la création de ta chanson, puis reviens ici pour enregistrer le playback."
|
||||||
|
: "Crée un nouveau projet pour composer ta musique et lancer ton playback."}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<GradientButton
|
||||||
|
title="Créer un nouveau projet"
|
||||||
|
onPress={handleCreateNew}
|
||||||
|
containerStyle={{ alignSelf: "center", minWidth: 220 }}
|
||||||
|
disabled={isCreating}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</AppActionSheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlaybackPickerModal;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
opacity: 0.85,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
textAlign: "center",
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
projectCard: {
|
||||||
|
...Style.containerRow,
|
||||||
|
gap: 12,
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: Palette.glass,
|
||||||
|
},
|
||||||
|
cover: {
|
||||||
|
...size({ size: 60 }),
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: Palette.black,
|
||||||
|
},
|
||||||
|
projectTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
},
|
||||||
|
projectSubtitle: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.gray,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
},
|
||||||
|
projectHint: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
opacity: 0.7,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
paddingVertical: 40,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: Palette.glass,
|
||||||
|
},
|
||||||
|
emptyTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
emptyDescription: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
opacity: 0.8,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
textAlign: "center",
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useGlobal } from "reactn";
|
import { useGlobal } from "reactn";
|
||||||
import firebase from "../config/firebase";
|
import firebase from "../config/firebase";
|
||||||
|
import { getLikeFieldPath, LIKE_TARGET } from "../utils/likes";
|
||||||
import useDataFromRef from "./useDataFromRef";
|
import useDataFromRef from "./useDataFromRef";
|
||||||
|
|
||||||
export default function useUserLikedProjects() {
|
export default function useUserLikedProjects() {
|
||||||
@@ -11,7 +12,11 @@ export default function useUserLikedProjects() {
|
|||||||
? firebase
|
? firebase
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection("projects")
|
.collection("projects")
|
||||||
.where("likedBy", "array-contains", currentUID)
|
.where(
|
||||||
|
getLikeFieldPath(LIKE_TARGET.SONG),
|
||||||
|
"array-contains",
|
||||||
|
currentUID
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
simpleRef: false,
|
simpleRef: false,
|
||||||
listener: true,
|
listener: true,
|
||||||
@@ -21,4 +26,3 @@ export default function useUserLikedProjects() {
|
|||||||
|
|
||||||
return { projects: Array.isArray(data) ? data : [], loading };
|
return { projects: Array.isArray(data) ? data : [], loading };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import firebase, {
|
|||||||
usersRef,
|
usersRef,
|
||||||
} from "../config/firebase";
|
} from "../config/firebase";
|
||||||
import { getUserPreferredArtistName } from "../utils/artistName";
|
import { getUserPreferredArtistName } from "../utils/artistName";
|
||||||
|
import { getLikeFieldPath, LIKE_TARGET } from "../utils/likes";
|
||||||
|
|
||||||
|
const SONG_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.SONG);
|
||||||
|
const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK);
|
||||||
|
|
||||||
export const UserDataContext = createContext();
|
export const UserDataContext = createContext();
|
||||||
|
|
||||||
@@ -63,7 +67,21 @@ export default ({ children }) => {
|
|||||||
} = useDataFromRef({
|
} = useDataFromRef({
|
||||||
ref: currentUID
|
ref: currentUID
|
||||||
? projectsRef
|
? projectsRef
|
||||||
.where("likedBy", "array-contains", currentUID)
|
.where(SONG_LIKES_FIELD, "array-contains", currentUID)
|
||||||
|
.orderBy("updatedAt", "desc")
|
||||||
|
: null,
|
||||||
|
simpleRef: false,
|
||||||
|
listener: true,
|
||||||
|
condition: !!currentUID,
|
||||||
|
refreshArray: [currentUID],
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: userLikedPlaybacks = [],
|
||||||
|
loading: userLikedPlaybacksLoading = true,
|
||||||
|
} = useDataFromRef({
|
||||||
|
ref: currentUID
|
||||||
|
? projectsRef
|
||||||
|
.where(PLAYBACK_LIKES_FIELD, "array-contains", currentUID)
|
||||||
.orderBy("updatedAt", "desc")
|
.orderBy("updatedAt", "desc")
|
||||||
: null,
|
: null,
|
||||||
simpleRef: false,
|
simpleRef: false,
|
||||||
@@ -71,9 +89,6 @@ export default ({ children }) => {
|
|||||||
condition: !!currentUID,
|
condition: !!currentUID,
|
||||||
refreshArray: [currentUID],
|
refreshArray: [currentUID],
|
||||||
});
|
});
|
||||||
const userLikedPlaybacks = Array.isArray(userLikedProjects)
|
|
||||||
? userLikedProjects.filter((project) => project.playbackUrl)
|
|
||||||
: [];
|
|
||||||
// Subscribe to user's playlists
|
// Subscribe to user's playlists
|
||||||
const { data: userPlaylists = [] } = useDataFromRef({
|
const { data: userPlaylists = [] } = useDataFromRef({
|
||||||
ref: currentUID ? playlistsRef.where("createdBy", "==", currentUID) : null,
|
ref: currentUID ? playlistsRef.where("createdBy", "==", currentUID) : null,
|
||||||
@@ -406,7 +421,7 @@ export default ({ children }) => {
|
|||||||
userLikedProjects,
|
userLikedProjects,
|
||||||
userLikedProjectsLoading,
|
userLikedProjectsLoading,
|
||||||
userLikedPlaybacks,
|
userLikedPlaybacks,
|
||||||
userLikedPlaybacksLoading: userLikedProjectsLoading,
|
userLikedPlaybacksLoading,
|
||||||
userPlaylists,
|
userPlaylists,
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Routes } from "../../navigation";
|
|||||||
import { navigate } from "../../navigation/NavigationService";
|
import { navigate } from "../../navigation/NavigationService";
|
||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { getArtistDisplayName } from "../../utils/artistName";
|
import { getArtistDisplayName } from "../../utils/artistName";
|
||||||
|
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
|
||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import MusicCard from "../Library/components/MusicCard";
|
import MusicCard from "../Library/components/MusicCard";
|
||||||
@@ -86,7 +87,7 @@ const HomeSave = () => {
|
|||||||
subtitle={item?.userName || ownerDisplayName}
|
subtitle={item?.userName || ownerDisplayName}
|
||||||
imageUri={item?.coverUrl || null}
|
imageUri={item?.coverUrl || null}
|
||||||
projectId={item?.id}
|
projectId={item?.id}
|
||||||
likedBy={item?.likedBy || []}
|
likedBy={getProjectLikes(item, LIKE_TARGET.SONG)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
selectProject(item.id);
|
selectProject(item.id);
|
||||||
navigate(Routes.Home);
|
navigate(Routes.Home);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
import GradientButton from "../../../components/GradientButton";
|
import GradientButton from "../../../components/GradientButton";
|
||||||
import useLayoutType from "../../../hooks/useLayoutType";
|
import useLayoutType from "../../../hooks/useLayoutType";
|
||||||
import { Routes } from "../../../navigation";
|
import { Routes } from "../../../navigation";
|
||||||
@@ -29,7 +30,10 @@ const BackTracks = () => {
|
|||||||
0,
|
0,
|
||||||
Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns)
|
Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns)
|
||||||
);
|
);
|
||||||
console.log("BackTracks items:", items);
|
|
||||||
|
const openPlaybackPicker = useCallback(() => {
|
||||||
|
SheetManager.show("PlaybackPicker");
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
@@ -41,7 +45,7 @@ const BackTracks = () => {
|
|||||||
liked: false,
|
liked: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onPressPlus={() => navigate(Routes.WritingLyrics)}
|
onPressPlus={openPlaybackPicker}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
@@ -85,7 +89,7 @@ const BackTracks = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
containerStyle={{ padding: 2 }}
|
containerStyle={{ padding: 2 }}
|
||||||
onPress={() => navigate(Routes.WritingLyrics)}
|
onPress={openPlaybackPicker}
|
||||||
title=" Créer un playback"
|
title=" Créer un playback"
|
||||||
></GradientButton>
|
></GradientButton>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { responsiveWidth } from "react-native-responsive-dimensions";
|
|||||||
import { useGlobal } from "reactn";
|
import { useGlobal } from "reactn";
|
||||||
import { icons, img } from "../../../assets";
|
import { icons, img } from "../../../assets";
|
||||||
import PressableScale from "../../../components/PressableScale";
|
import PressableScale from "../../../components/PressableScale";
|
||||||
import { arrayRemove, arrayUnion, projectsRef } from "../../../config/firebase";
|
import { LIKE_TARGET, toggleProjectLike } from "../../../utils/likes";
|
||||||
import { Palette, Style } from "../../../styles";
|
import { Palette, Style } from "../../../styles";
|
||||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||||
import { size } from "../../../styles/Style";
|
import { size } from "../../../styles/Style";
|
||||||
@@ -26,6 +26,7 @@ const MusicCard = ({
|
|||||||
imageUri = null,
|
imageUri = null,
|
||||||
projectId = null,
|
projectId = null,
|
||||||
likedBy = [],
|
likedBy = [],
|
||||||
|
likeTarget = LIKE_TARGET.SONG,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentUID] = useGlobal("currentUID");
|
const [currentUID] = useGlobal("currentUID");
|
||||||
const [selected, setSelected] = useState(false);
|
const [selected, setSelected] = useState(false);
|
||||||
@@ -58,8 +59,11 @@ const MusicCard = ({
|
|||||||
const next = !selected;
|
const next = !selected;
|
||||||
setSelected(next);
|
setSelected(next);
|
||||||
try {
|
try {
|
||||||
await projectsRef.doc(projectId).update({
|
await toggleProjectLike({
|
||||||
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
|
projectId,
|
||||||
|
target: likeTarget,
|
||||||
|
currentUID,
|
||||||
|
next,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// rollback on failure
|
// rollback on failure
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ import { icons, img } from "../../assets";
|
|||||||
import { openComments } from "../../components/bottomsheets/CommentsBottomSheet";
|
import { openComments } from "../../components/bottomsheets/CommentsBottomSheet";
|
||||||
import KaraokeLyrics from "../../components/KaraokeLyrics";
|
import KaraokeLyrics from "../../components/KaraokeLyrics";
|
||||||
import ProfilePicture from "../../components/ProfilePicture";
|
import ProfilePicture from "../../components/ProfilePicture";
|
||||||
import {
|
import { projectsRef, usersRef } from "../../config/firebase";
|
||||||
arrayRemove,
|
|
||||||
arrayUnion,
|
|
||||||
projectsRef,
|
|
||||||
usersRef,
|
|
||||||
} from "../../config/firebase";
|
|
||||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { navigate } from "../../navigation/NavigationService";
|
import { navigate } from "../../navigation/NavigationService";
|
||||||
@@ -28,11 +23,16 @@ import { useUser } from "../../providers/UserDataProvider";
|
|||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import { size } from "../../styles/Style";
|
import { size } from "../../styles/Style";
|
||||||
|
import {
|
||||||
|
getProjectLikes,
|
||||||
|
LIKE_TARGET,
|
||||||
|
toggleProjectLike,
|
||||||
|
} from "../../utils/likes";
|
||||||
|
|
||||||
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||||
const { currentUID, followUser, unfollowUser } = useUser() || {};
|
const { currentUID, followUser, unfollowUser } = useUser() || {};
|
||||||
const videoUrl = item?.playbackUrl || null;
|
const videoUrl = item?.playbackUrl || null;
|
||||||
const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : [];
|
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
|
||||||
const [isLiked, setIsLiked] = useState(
|
const [isLiked, setIsLiked] = useState(
|
||||||
currentUID ? initialLikedBy.includes(currentUID) : false
|
currentUID ? initialLikedBy.includes(currentUID) : false
|
||||||
);
|
);
|
||||||
@@ -100,10 +100,10 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
}, [owner?.followedBy, currentUID]);
|
}, [owner?.followedBy, currentUID]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lb = Array.isArray(item?.likedBy) ? item.likedBy : [];
|
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
|
||||||
setLikesCount(lb.length);
|
setLikesCount(lb.length);
|
||||||
setIsLiked(currentUID ? lb.includes(currentUID) : false);
|
setIsLiked(currentUID ? lb.includes(currentUID) : false);
|
||||||
}, [item?.likedBy, currentUID]);
|
}, [item?.likes?.playback, currentUID]);
|
||||||
|
|
||||||
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
|
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
|
||||||
|
|
||||||
@@ -292,16 +292,12 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
setIsLiked(nextLiked);
|
setIsLiked(nextLiked);
|
||||||
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
||||||
|
|
||||||
const ref = projectsRef.doc(item.id);
|
await toggleProjectLike({
|
||||||
await ref.set(
|
projectId: item.id,
|
||||||
{
|
currentUID,
|
||||||
likedBy: nextLiked
|
target: LIKE_TARGET.PLAYBACK,
|
||||||
? arrayUnion(currentUID)
|
next: nextLiked,
|
||||||
: arrayRemove(currentUID),
|
});
|
||||||
// Optionally: updatedAt could be set if needed
|
|
||||||
},
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// rollback on failure
|
// rollback on failure
|
||||||
setIsLiked((v) => !v);
|
setIsLiked((v) => !v);
|
||||||
|
|||||||
@@ -17,12 +17,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { icons, img } from "../../../assets";
|
import { icons, img } from "../../../assets";
|
||||||
import KaraokeLyrics from "../../../components/KaraokeLyrics";
|
import KaraokeLyrics from "../../../components/KaraokeLyrics";
|
||||||
import {
|
import { usersRef } from "../../../config/firebase";
|
||||||
arrayRemove,
|
|
||||||
arrayUnion,
|
|
||||||
projectsRef,
|
|
||||||
usersRef,
|
|
||||||
} from "../../../config/firebase";
|
|
||||||
import { Routes } from "../../../navigation";
|
import { Routes } from "../../../navigation";
|
||||||
import { navigate } from "../../../navigation/NavigationService";
|
import { navigate } from "../../../navigation/NavigationService";
|
||||||
import { useUser } from "../../../providers/UserDataProvider";
|
import { useUser } from "../../../providers/UserDataProvider";
|
||||||
@@ -30,6 +25,11 @@ import { Palette } from "../../../styles";
|
|||||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||||
import { size } from "../../../styles/Style";
|
import { size } from "../../../styles/Style";
|
||||||
import CommentsPanel from "./CommentsPanel.web";
|
import CommentsPanel from "./CommentsPanel.web";
|
||||||
|
import {
|
||||||
|
getProjectLikes,
|
||||||
|
LIKE_TARGET,
|
||||||
|
toggleProjectLike,
|
||||||
|
} from "../../../utils/likes";
|
||||||
|
|
||||||
// Debug logging toggle for web playback
|
// Debug logging toggle for web playback
|
||||||
const DEBUG_PLAYBACK_WEB = true;
|
const DEBUG_PLAYBACK_WEB = true;
|
||||||
@@ -61,7 +61,7 @@ const PlaybackItem = ({
|
|||||||
const wasActiveRef = useRef(false);
|
const wasActiveRef = useRef(false);
|
||||||
const startedRef = useRef(false);
|
const startedRef = useRef(false);
|
||||||
|
|
||||||
const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : [];
|
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
|
||||||
const [isLiked, setIsLiked] = useState(
|
const [isLiked, setIsLiked] = useState(
|
||||||
currentUID ? initialLikedBy.includes(currentUID) : false
|
currentUID ? initialLikedBy.includes(currentUID) : false
|
||||||
);
|
);
|
||||||
@@ -130,10 +130,10 @@ const PlaybackItem = ({
|
|||||||
}, [owner?.followedBy, currentUID]);
|
}, [owner?.followedBy, currentUID]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lb = Array.isArray(item?.likedBy) ? item.likedBy : [];
|
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
|
||||||
setLikesCount(lb.length);
|
setLikesCount(lb.length);
|
||||||
setIsLiked(currentUID ? lb.includes(currentUID) : false);
|
setIsLiked(currentUID ? lb.includes(currentUID) : false);
|
||||||
}, [item?.likedBy, currentUID]);
|
}, [item?.likes?.playback, currentUID]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
startedRef.current = false;
|
startedRef.current = false;
|
||||||
@@ -428,15 +428,12 @@ const PlaybackItem = ({
|
|||||||
setIsLiked(nextLiked);
|
setIsLiked(nextLiked);
|
||||||
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
||||||
|
|
||||||
const ref = projectsRef.doc(item.id);
|
await toggleProjectLike({
|
||||||
await ref.set(
|
projectId: item.id,
|
||||||
{
|
currentUID,
|
||||||
likedBy: nextLiked
|
target: LIKE_TARGET.PLAYBACK,
|
||||||
? arrayUnion(currentUID)
|
next: nextLiked,
|
||||||
: arrayRemove(currentUID),
|
});
|
||||||
},
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
setIsLiked((v) => !v);
|
setIsLiked((v) => !v);
|
||||||
setLikesCount((c) =>
|
setLikesCount((c) =>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { Routes } from "../../navigation";
|
|||||||
import { push } from "../../navigation/NavigationService";
|
import { push } from "../../navigation/NavigationService";
|
||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { getArtistDisplayName } from "../../utils/artistName";
|
import { getArtistDisplayName } from "../../utils/artistName";
|
||||||
|
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
|
||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import Style, { gutters, size } from "../../styles/Style";
|
import Style, { gutters, size } from "../../styles/Style";
|
||||||
@@ -191,7 +192,11 @@ const Profile = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Composant factorisé pour afficher la liste de musiques
|
// Composant factorisé pour afficher la liste de musiques
|
||||||
const MusicListSection = ({ data, emptyText }) => (
|
const MusicListSection = ({
|
||||||
|
data,
|
||||||
|
emptyText,
|
||||||
|
likeTarget = LIKE_TARGET.SONG,
|
||||||
|
}) => (
|
||||||
<ScrollView style={{ flex: 1, marginBottom: 70 }}>
|
<ScrollView style={{ flex: 1, marginBottom: 70 }}>
|
||||||
{Array.isArray(data) && data.length > 0 ? (
|
{Array.isArray(data) && data.length > 0 ? (
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -207,12 +212,21 @@ const Profile = () => {
|
|||||||
subtitle={isSelf ? selfDisplayName : targetDisplayName}
|
subtitle={isSelf ? selfDisplayName : targetDisplayName}
|
||||||
imageUri={item?.coverUrl || null}
|
imageUri={item?.coverUrl || null}
|
||||||
projectId={item?.id}
|
projectId={item?.id}
|
||||||
likedBy={item?.likedBy || []}
|
likedBy={getProjectLikes(item, likeTarget)}
|
||||||
|
likeTarget={likeTarget}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigateToMusicDetails({
|
navigateToMusicDetails({
|
||||||
projectId: item.id,
|
projectId: item.id,
|
||||||
songUrl: item?.songUrl,
|
songUrl: item?.songUrl,
|
||||||
project: item,
|
project: item,
|
||||||
|
queueProjects: data,
|
||||||
|
queueSource: {
|
||||||
|
id: isSelf ? "profile-self" : "profile-other",
|
||||||
|
type: "collection",
|
||||||
|
name: isSelf
|
||||||
|
? "Mes musiques"
|
||||||
|
: targetDisplayName || "Profil",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onPressMore={(posTop) => {
|
onPressMore={(posTop) => {
|
||||||
@@ -528,6 +542,7 @@ const Profile = () => {
|
|||||||
<MusicListSection
|
<MusicListSection
|
||||||
data={displayedPlaybacks}
|
data={displayedPlaybacks}
|
||||||
emptyText="Aucun playback pour le moment"
|
emptyText="Aucun playback pour le moment"
|
||||||
|
likeTarget={LIKE_TARGET.PLAYBACK}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selected === "Chansons" && (
|
{selected === "Chansons" && (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import DeleteAccountModal from "../components/modal/DeleteAccountModal";
|
|||||||
import DeletePlaybackModal from "../components/modal/DeletePlaybackModal";
|
import DeletePlaybackModal from "../components/modal/DeletePlaybackModal";
|
||||||
import DeleteAudioModal from "../components/modal/DeleteAudioModal";
|
import DeleteAudioModal from "../components/modal/DeleteAudioModal";
|
||||||
import PlaylistModal from "../components/modal/PlaylistModal";
|
import PlaylistModal from "../components/modal/PlaylistModal";
|
||||||
|
import PlaybackPickerModal from "../components/modal/PlaybackPickerModal";
|
||||||
import ShareModal from "../components/modal/ShareModal";
|
import ShareModal from "../components/modal/ShareModal";
|
||||||
|
|
||||||
registerSheet("Delete", DeleteModal);
|
registerSheet("Delete", DeleteModal);
|
||||||
@@ -13,6 +14,7 @@ registerSheet("DeleteAccount", DeleteAccountModal);
|
|||||||
registerSheet("DeletePlayback", DeletePlaybackModal);
|
registerSheet("DeletePlayback", DeletePlaybackModal);
|
||||||
registerSheet("DeleteAudio", DeleteAudioModal);
|
registerSheet("DeleteAudio", DeleteAudioModal);
|
||||||
registerSheet("Playlist", PlaylistModal);
|
registerSheet("Playlist", PlaylistModal);
|
||||||
|
registerSheet("PlaybackPicker", PlaybackPickerModal);
|
||||||
registerSheet("Share", ShareModal);
|
registerSheet("Share", ShareModal);
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { arrayRemove, arrayUnion, projectsRef } from "../config/firebase";
|
||||||
|
|
||||||
|
export const LIKE_TARGET = {
|
||||||
|
SONG: "song",
|
||||||
|
PLAYBACK: "playback",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLikeFieldPath = (target = LIKE_TARGET.SONG) => {
|
||||||
|
return target === LIKE_TARGET.PLAYBACK ? "likes.playback" : "likes.song";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProjectLikes = (project, target = LIKE_TARGET.SONG) => {
|
||||||
|
const path = target === LIKE_TARGET.PLAYBACK ? "playback" : "song";
|
||||||
|
const likes = project?.likes;
|
||||||
|
const list = likes ? likes[path] : null;
|
||||||
|
return Array.isArray(list) ? list : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isProjectLikedByUser = (
|
||||||
|
project,
|
||||||
|
target,
|
||||||
|
currentUID
|
||||||
|
) => {
|
||||||
|
if (!currentUID) return false;
|
||||||
|
return getProjectLikes(project, target).includes(currentUID);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleProjectLike = async ({
|
||||||
|
projectId,
|
||||||
|
target = LIKE_TARGET.SONG,
|
||||||
|
currentUID,
|
||||||
|
next,
|
||||||
|
}) => {
|
||||||
|
if (!projectId || !currentUID) return;
|
||||||
|
const fieldPath = getLikeFieldPath(target);
|
||||||
|
await projectsRef.doc(projectId).set(
|
||||||
|
{
|
||||||
|
[fieldPath]: next
|
||||||
|
? arrayUnion(currentUID)
|
||||||
|
: arrayRemove(currentUID),
|
||||||
|
},
|
||||||
|
{ merge: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user