diff --git a/functions/index.js b/functions/index.js index b281aa8..134d496 100644 --- a/functions/index.js +++ b/functions/index.js @@ -18,3 +18,4 @@ exports.cover = require("./src/cover"); exports.projects = require("./src/project"); exports.thumbnail = require("./src/thumbnail"); exports.upload = require("./src/upload"); +exports.algolia = require("./src/algolia"); diff --git a/functions/src/algolia.js b/functions/src/algolia.js new file mode 100644 index 0000000..201294e --- /dev/null +++ b/functions/src/algolia.js @@ -0,0 +1,26 @@ +const { onRequest } = require("firebase-functions/v2/https"); + +exports.algoliaTransformProjectData = onRequest( + { region: "europe-west1" }, + (req, res) => { + const payload = req.body.data; + const objectID = payload.objectID; + try { + const flat = { ...payload }; + delete flat["musicTimestamps"]; + + console.log(`Change in ${payload.objectID}`); + console.log(flat); + // Ton object final doit contenir "objectID" + const result = { + objectID, + ...flat, + }; + + res.send({ result }); + } catch (e) { + console.log(`Error ${objectID}:`, e.message); + res.status(500).end(); + } + }, +); diff --git a/functions/src/project.js b/functions/src/project.js index f30ec58..3f0be78 100644 --- a/functions/src/project.js +++ b/functions/src/project.js @@ -33,6 +33,20 @@ exports.onProjectUpdate = onDocumentWritten( batch.delete(doc.ref); }); await batch.commit(); + } else { + if (previousData) { + // edit + if (!previousData?.songUrl && currentData?.songUrl) { + await refList.projects.doc(projectId).update({ + hasSong: true, + }); + } + if (!previousData?.playbackUrl && currentData?.playbackUrl) { + await refList.projects.doc(projectId).update({ + hasPlayback: true, + }); + } + } } } catch (e) { console.log(e); diff --git a/src/data/keys.js b/src/data/keys.js index 5a3d833..0e36e89 100644 --- a/src/data/keys.js +++ b/src/data/keys.js @@ -19,3 +19,17 @@ export const GOOGLE_IOS_CLIENT_ID = // Android client ID (client_type 1 from google-services.json) export const GOOGLE_ANDROID_CLIENT_ID = "305598753437-qck73beo37mi04qlautgafbpdl3ccaoh.apps.googleusercontent.com"; + +const AlgoliaAppId = "38AHXYR8S1"; +const AlgoliaSearchKey = "2ac15d7b8b5e6490b53821067e4a0f03"; + +export const AlgoliaUserConfig = { + index: "users", + projectID: AlgoliaAppId, + publicKey: AlgoliaSearchKey, +}; +export const AlgoliaProjectConfig = { + index: "projects", + projectID: AlgoliaAppId, + publicKey: AlgoliaSearchKey, +}; diff --git a/src/hooks/useSearch.js b/src/hooks/useSearch.js index 6767356..45124a4 100644 --- a/src/hooks/useSearch.js +++ b/src/hooks/useSearch.js @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useState } from "react"; -import { useGlobal } from "reactn"; -import { usersRef, projectsRef } from "../config/firebase"; -import useDataFromRef from "./useDataFromRef"; +import { useState } from "react"; +import useAlgoliaSearch from "react-native-minuit/src/hooks/useAlgoliaSearch"; +import { AlgoliaUserConfig, AlgoliaProjectConfig } from "../data/keys"; +import { useUserData } from "../providers/UserDataProvider"; const batchSizes = { users: 5, @@ -11,221 +11,42 @@ const batchSizes = { const useSearch = () => { const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); - const queryText = useMemo(() => search.trim(), [search]); - const queryLower = useMemo( - () => (search || "").toLowerCase().trim(), - [search], - ); - const [currentUID] = useGlobal("currentUID"); - const [autoLoads, setAutoLoads] = useState(0); + const { currentUID } = useUserData(); - const usersQueryRef = useMemo(() => { - try { - const lower = queryLower; - if (lower) { - return usersRef - .orderBy("userNameLower") - .startAt(lower) - .endAt(lower + "\uf8ff"); - } - return usersRef.orderBy("userNameLower"); - } catch (_error) { - return null; - } - }, [queryLower]); - - const projectsQueryRefLower = useMemo(() => { - try { - const lower = queryText ? queryText.toLowerCase() : ""; - if (lower) { - return projectsRef - .orderBy("titleLower") - .startAt(lower) - .endAt(lower + "\uf8ff"); - } - return null; - } catch (_error) { - return null; - } - }, [queryText]); - - const projectsQueryRefCase = useMemo(() => { - try { - const raw = queryText || ""; - if (raw) { - return projectsRef - .orderBy("title") - .startAt(raw) - .endAt(raw + "\uf8ff"); - } - return null; - } catch (_error) { - return null; - } - }, [queryText]); - - const { - data: userResults = [], - loading: usersLoading, - loadMore: loadMoreUsers, - } = useDataFromRef({ - ref: usersQueryRef, - simpleRef: false, - listener: false, - condition: !!usersQueryRef, - refreshArray: [queryLower], - usePagination: true, - batchSize: batchSizes.users, + const { hits: users, loading: userLoading } = useAlgoliaSearch({ + query: search, + algoliaObject: AlgoliaUserConfig, + batch: batchSizes.users, + searchParams: { + filters: `NOT objectID:${currentUID}`, + }, }); - - const filteredUsers = useMemo(() => { - const arr = Array.isArray(userResults) ? userResults : []; - const matchingUsers = queryLower - ? arr.filter((user) => { - const nameLower = ( - user?.userNameLower || - user?.userName || - "" - ).toLowerCase(); - return nameLower.includes(queryLower); - }) - : arr; - const seen = new Set(); - const unique = []; - for (const user of matchingUsers) { - const id = user?.id; - if (!id || seen.has(id)) continue; - if (id === currentUID || user?.userID === currentUID) continue; - seen.add(id); - unique.push(user); - } - return unique; - }, [userResults, queryLower, currentUID]); - - const { - data: projectResultsLower = [], - loading: projectsLoadingLower, - loadMore: loadMoreProjectsLower, - } = useDataFromRef({ - ref: projectsQueryRefLower, - simpleRef: false, - listener: false, - condition: !!projectsQueryRefLower, - refreshArray: [queryText], - usePagination: true, - batchSize: batchSizes.projects, + const { hits: playbacks, loading: playbackLoading } = useAlgoliaSearch({ + query: search, + algoliaObject: AlgoliaProjectConfig, + batch: batchSizes.projects, + searchParams: { + filters: `hasPlayback:true AND hasSong:true`, + }, }); - - const { - data: projectResultsCase = [], - loading: projectsLoadingCase, - loadMore: loadMoreProjectsCase, - } = useDataFromRef({ - ref: projectsQueryRefCase, - simpleRef: false, - listener: false, - condition: !!projectsQueryRefCase, - refreshArray: [queryText], - usePagination: true, - batchSize: batchSizes.projects, + const { hits: musics, loading: musicLoading } = useAlgoliaSearch({ + query: search, + algoliaObject: AlgoliaProjectConfig, + batch: batchSizes.projects, + searchParams: { + filters: `hasPlayback:false AND hasSong:true`, + }, }); - const { - data: projectResultsDefault = [], - loading: projectsLoadingDefault, - loadMore: loadMoreProjectsDefault, - } = useDataFromRef({ - ref: !queryText ? projectsRef.orderBy("updatedAt", "desc") : null, - simpleRef: false, - listener: false, - condition: !queryText, - refreshArray: [queryText], - usePagination: true, - batchSize: batchSizes.projects, - }); - - const projectsLoading = queryText - ? projectsLoadingLower || projectsLoadingCase - : projectsLoadingDefault; - - const loadMoreProjects = () => { - if (queryText) { - loadMoreProjectsLower?.(); - loadMoreProjectsCase?.(); - } else { - loadMoreProjectsDefault?.(); - } - }; - - const filteredProjects = useMemo(() => { - const lowerMatches = Array.isArray(projectResultsLower) - ? projectResultsLower - : []; - const caseMatches = Array.isArray(projectResultsCase) - ? projectResultsCase - : []; - const defaultProjects = Array.isArray(projectResultsDefault) - ? projectResultsDefault - : []; - - const arr = queryText ? [...lowerMatches, ...caseMatches] : defaultProjects; - const seen = new Set(); - const unique = []; - for (const project of arr) { - const id = project?.id; - if (!id || seen.has(id)) continue; - seen.add(id); - unique.push(project); - } - return unique; - }, [ - queryText, - projectResultsLower, - projectResultsCase, - projectResultsDefault, - ]); - - useEffect(() => { - setAutoLoads(0); - }, [queryText]); - - useEffect(() => { - if ( - filteredUsers.length < batchSizes.users && - usersQueryRef && - !usersLoading - ) { - if (autoLoads < 3) { - setAutoLoads((previous) => previous + 1); - loadMoreUsers?.(); - } - } - }, [ - filteredUsers.length, - usersLoading, - usersQueryRef, - autoLoads, - loadMoreUsers, - ]); - - const toggleSelected = (item) => { - setSelected((prev) => (prev === item ? null : item)); - }; - return { search, setSearch, - queryText, - queryLower, selected, setSelected, - toggleSelected, - filteredUsers, - usersLoading, - loadMoreUsers, - filteredProjects, - projectsLoading, - loadMoreProjects, + users, + musics, + playbacks, + loading: userLoading || musicLoading || playbackLoading, }; }; diff --git a/src/screens/Library/Library.web.js b/src/screens/Library/Library.web.js index 6ba43b3..00995c4 100644 --- a/src/screens/Library/Library.web.js +++ b/src/screens/Library/Library.web.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Image, Platform, ScrollView, Text, View } from "react-native"; import { responsiveHeight } from "react-native-responsive-dimensions"; import { background, icons } from "../../assets"; @@ -50,15 +50,38 @@ const Library = () => { setSearch, selected, setSelected, - toggleSelected, - filteredProjects, - projectsLoading, - loadMoreProjects, - filteredUsers, - usersLoading, - loadMoreUsers, + users = [], + musics = [], + playbacks = [], + loading, } = useSearch(); + const toggleSelected = useCallback( + (value) => { + setSelected((previous) => (previous === value ? null : value)); + }, + [setSelected], + ); + + const musicResults = useMemo( + () => (Array.isArray(musics) ? musics : []), + [musics], + ); + + const playbackResults = useMemo( + () => (Array.isArray(playbacks) ? playbacks : []), + [playbacks], + ); + + const userResults = useMemo( + () => (Array.isArray(users) ? users : []), + [users], + ); + + const musicsLoading = loading; + const playbacksLoading = loading; + const usersLoading = loading; + const [dropdownVisible, setDropdownVisible] = useState(false); const searchWrapperRef = useRef(null); @@ -152,12 +175,12 @@ const Library = () => { /> diff --git a/src/screens/Library/Research.js b/src/screens/Library/Research.js index 1c276ad..a2d9871 100644 --- a/src/screens/Library/Research.js +++ b/src/screens/Library/Research.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import { View } from "react-native"; import { background } from "../../assets"; import useSearch from "../../hooks/useSearch"; @@ -12,15 +12,39 @@ const Research = () => { search, setSearch, selected, - toggleSelected, - filteredProjects, - projectsLoading, - loadMoreProjects, - filteredUsers, - usersLoading, - loadMoreUsers, + setSelected, + users = [], + musics = [], + playbacks = [], + loading, } = useSearch(); + const toggleSelected = useCallback( + (value) => { + setSelected((previous) => (previous === value ? null : value)); + }, + [setSelected], + ); + + const musicResults = useMemo( + () => (Array.isArray(musics) ? musics : []), + [musics], + ); + + const playbackResults = useMemo( + () => (Array.isArray(playbacks) ? playbacks : []), + [playbacks], + ); + + const userResults = useMemo( + () => (Array.isArray(users) ? users : []), + [users], + ); + + const musicsLoading = loading; + const playbacksLoading = loading; + const usersLoading = loading; + return ( { /> diff --git a/src/screens/Library/components/SearchResultsList.js b/src/screens/Library/components/SearchResultsList.js index 80980c4..8d4ce0b 100644 --- a/src/screens/Library/components/SearchResultsList.js +++ b/src/screens/Library/components/SearchResultsList.js @@ -1,6 +1,6 @@ -import { Image as ExpoImage } from "expo-image"; import React, { useState } from "react"; import { Pressable, ScrollView, Text, View } from "react-native"; +import { Image as ExpoImage } from "expo-image"; import { img } from "../../../assets"; import MoreMenu from "../../../components/MoreMenu"; import { Routes } from "../../../navigation"; @@ -28,12 +28,12 @@ const EmptyText = ({ text }) => ( const SearchResultsList = ({ selected, - filteredProjects, - projectsLoading, - loadMoreProjects, - filteredUsers, - usersLoading, - loadMoreUsers, + musics = [], + musicsLoading = false, + playbacks = [], + playbacksLoading = false, + users = [], + usersLoading = false, onResultSelected, contentContainerStyle, style, @@ -52,14 +52,23 @@ const SearchResultsList = ({ onResultSelected?.(); }; + const shouldShowMusics = + (!selected && (musics.length > 0 || musicsLoading)) || selected === "Musiques"; + + const shouldShowPlaybacks = + (!selected && (playbacks.length > 0 || playbacksLoading)) || + selected === "Playbacks"; + + const shouldShowUsers = + (!selected && (users.length > 0 || usersLoading)) || selected === "Profils"; + return ( - {((!selected && (filteredProjects.length > 0 || projectsLoading)) || - selected === "Musiques") && ( + {shouldShowMusics && ( - {filteredProjects.length > 0 && ( + {musics.length > 0 && ( - {Array.isArray(filteredProjects) && - filteredProjects.slice(0, 6).map((project) => ( + {Array.isArray(musics) && + musics.slice(0, 6).map((project) => ( !previous || - positionTop?.top !== (menuPosition?.top ?? null) + positionTop?.top !== (menuPosition?.top ?? null), ); }} /> ))} - {/* {(filteredProjects.length >= 6 || projectsLoading) && ( - - - {projectsLoading ? "Chargement…" : "Charger plus"} - - - )} */} + {"Chargement…"} + + )} )} - {filteredProjects.length === 0 && !projectsLoading && ( + {musics.length === 0 && !musicsLoading && ( )} )} - {((!selected && (filteredUsers.length > 0 || usersLoading)) || - selected === "Profils") && ( + {shouldShowPlaybacks && ( + + + {playbacks.length > 0 && ( + + {Array.isArray(playbacks) && + playbacks.slice(0, 6).map((project) => ( + handleProjectPress(project.id)} + onPressMore={(positionTop) => { + setSelectedProjectId(project.id); + setMenuPosition(positionTop); + setShowMenu( + (previous) => + !previous || + positionTop?.top !== (menuPosition?.top ?? null), + ); + }} + /> + ))} + {playbacksLoading && ( + + {"Chargement…"} + + )} + + )} + {playbacks.length === 0 && !playbacksLoading && ( + + )} + + + )} + {shouldShowUsers && ( - {filteredUsers.length > 0 && ( + {users.length > 0 && ( - {Array.isArray(filteredUsers) && - filteredUsers.slice(0, 5).map((user) => ( + {Array.isArray(users) && + users.slice(0, 5).map((user) => ( ))} - {/* {(filteredUsers.length >= 5 || usersLoading) && ( - - - {usersLoading ? "Chargement…" : "Charger plus"} - - - )} */} + {"Chargement…"} + + )} )} - {filteredUsers.length === 0 && !usersLoading && ( + {users.length === 0 && !usersLoading && ( )} )} - {selected === "Playbacks" && ( - - - - - - )} {selected === "Clips" && ( { await setIsLoading(true); await updateProjectData({ title: result?.title || "", - titleLower: (result?.title || "").toLowerCase(), lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], description: result?.lyricsDescription, config: config || null, @@ -198,7 +197,6 @@ const CreatingLyrics = ({ active, config, selections }) => { await setIsLoading(true); await updateProjectData({ title: result?.title || "", - titleLower: (result?.title || "").toLowerCase(), lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js index 7b77738..b797f5f 100644 --- a/src/screens/Writing/Lyrics.js +++ b/src/screens/Writing/Lyrics.js @@ -112,7 +112,7 @@ const Lyrics = ({ navigation }) => { if (invalid) { Alert.alert( "Champs incomplets", - "Chaque couplet et refrain doit contenir du texte." + "Chaque couplet et refrain doit contenir du texte.", ); return; } @@ -133,7 +133,7 @@ const Lyrics = ({ navigation }) => { normalizedOldLyrics.every( (s, i) => s.type === normalizedNewLyrics[i]?.type && - s.lyrics === normalizedNewLyrics[i]?.lyrics + s.lyrics === normalizedNewLyrics[i]?.lyrics, ); // 1) Appel de la Cloud Function de modération avant tout enregistrement @@ -157,7 +157,7 @@ const Lyrics = ({ navigation }) => { : null; Alert.alert( "Contenu interdit", - [data?.message, quotes].filter(Boolean).join("\n\n") + [data?.message, quotes].filter(Boolean).join("\n\n"), ); return; // stop here } @@ -165,7 +165,7 @@ const Lyrics = ({ navigation }) => { if (data?.errorCode === "ANALYSE_FAILED") { Alert.alert( "Analyse indisponible", - "Impossible de vérifier la toxicité pour le moment. Réessayez plus tard." + "Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.", ); return; } @@ -200,18 +200,17 @@ const Lyrics = ({ navigation }) => { } catch (moderationError) { console.log( "Moderation call failed", - moderationError?.message || moderationError + moderationError?.message || moderationError, ); Alert.alert( "Analyse indisponible", - "Impossible de vérifier la toxicité pour le moment. Réessayez plus tard." + "Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.", ); return; } const baseData = { title: titleTrimmed, - titleLower: titleTrimmed.toLowerCase(), lyrics: normalizedNewLyrics, config: sanitize(projectConfig), selections: sanitize(projectSelections),