import { useEffect, useState } from "react"; import useAlgoliaSearch from "react-native-minuit/src/hooks/useAlgoliaSearch"; import { AlgoliaUserConfig, AlgoliaProjectConfig } from "../data/keys"; import { projectsRef } from "../config/firebase"; import { useUserData } from "../providers/UserDataProvider"; const batchSizes = { users: 5, projects: 6, }; const isValidUri = (value) => typeof value === "string" && value.trim().length > 0; const hasCoverAsset = (project) => { if (!project) return false; if (isValidUri(project?.coverUrl)) { return true; } const cover = project?.cover; if (!cover || typeof cover !== "object") { return false; } const coverCandidates = [ cover?.result, cover?.finalUrl, cover?.generatedBackground, ]; if (Array.isArray(cover?.options)) { coverCandidates.push( ...cover.options.flatMap((option) => [ option?.finalUrl, option?.generatedUrl, ]), ); } return coverCandidates.some(isValidUri); }; const hasThumbnailAsset = (project) => isValidUri(project?.thumbnailUrl) || isValidUri(project?.songThumbnailUrl); const mergeProjectAssets = ( project, firestoreData, { mergeCover = false, mergeThumbnails = false } = {}, ) => { if (!firestoreData || typeof firestoreData !== "object") { return project; } const mergedProject = { ...project }; if (mergeCover) { if (!isValidUri(mergedProject.coverUrl) && isValidUri(firestoreData.coverUrl)) { mergedProject.coverUrl = firestoreData.coverUrl; } const firestoreCover = firestoreData.cover && typeof firestoreData.cover === "object" ? firestoreData.cover : null; if (firestoreCover) { mergedProject.cover = { ...(typeof mergedProject.cover === "object" ? mergedProject.cover : {}), ...firestoreCover, }; } } if (mergeThumbnails) { if (!isValidUri(mergedProject.thumbnailUrl) && isValidUri(firestoreData.thumbnailUrl)) { mergedProject.thumbnailUrl = firestoreData.thumbnailUrl; } if ( !isValidUri(mergedProject.songThumbnailUrl) && isValidUri(firestoreData.songThumbnailUrl) ) { mergedProject.songThumbnailUrl = firestoreData.songThumbnailUrl; } } return mergedProject; }; const hydrateProjects = async ( projects = [], { ensureCover = false, ensureThumbnails = false } = {}, ) => { if (!Array.isArray(projects) || projects.length === 0) { return []; } return Promise.all( projects.map(async (project) => { const needsCover = ensureCover && !hasCoverAsset(project); const needsThumbnail = ensureThumbnails && !hasThumbnailAsset(project); if ((!needsCover && !needsThumbnail) || !project?.id) { return project; } try { const snapshot = await projectsRef.doc(project.id).get(); if (!snapshot.exists) { return project; } const firestoreData = snapshot.data() || {}; return mergeProjectAssets(project, firestoreData, { mergeCover: needsCover, mergeThumbnails: needsThumbnail, }); } catch (error) { console.log("useSearch.hydrateProjects error", error?.message || error); return project; } }), ); }; const useSearch = () => { const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); const [hydratedMusics, setHydratedMusics] = useState(null); const [hydratedPlaybacks, setHydratedPlaybacks] = useState(null); const { currentUID } = useUserData(); const { hits: users, loading: userLoading } = useAlgoliaSearch({ query: search, algoliaObject: AlgoliaUserConfig, batch: batchSizes.users, searchParams: { filters: `NOT objectID:${currentUID}`, }, }); const { hits: playbacks, loading: playbackLoading } = useAlgoliaSearch({ query: search, algoliaObject: AlgoliaProjectConfig, batch: batchSizes.projects, searchParams: { filters: `hasPlayback:true AND hasSong:true`, }, }); const { hits: musics, loading: musicLoading } = useAlgoliaSearch({ query: search, algoliaObject: AlgoliaProjectConfig, batch: batchSizes.projects, searchParams: { filters: `hasSong:true`, }, }); useEffect(() => { let isCancelled = false; const nextMusics = Array.isArray(musics) ? musics : []; const requiresHydration = nextMusics.length > 0 && nextMusics.some((project) => !hasCoverAsset(project)); if (!requiresHydration) { setHydratedMusics(null); return () => { isCancelled = true; }; } setHydratedMusics(null); (async () => { const hydrated = await hydrateProjects(nextMusics, { ensureCover: true }); if (!isCancelled) { setHydratedMusics(hydrated); } })(); return () => { isCancelled = true; }; }, [musics]); useEffect(() => { let isCancelled = false; const nextPlaybacks = Array.isArray(playbacks) ? playbacks : []; const requiresHydration = nextPlaybacks.length > 0 && nextPlaybacks.some((project) => !hasThumbnailAsset(project)); if (!requiresHydration) { setHydratedPlaybacks(null); return () => { isCancelled = true; }; } setHydratedPlaybacks(null); (async () => { const hydrated = await hydrateProjects(nextPlaybacks, { ensureThumbnails: true, }); if (!isCancelled) { setHydratedPlaybacks(hydrated); } })(); return () => { isCancelled = true; }; }, [playbacks]); return { search, setSearch, selected, setSelected, users, musics: hydratedMusics ?? musics, playbacks: hydratedPlaybacks ?? playbacks, loading: userLoading || musicLoading || playbackLoading, }; }; export default useSearch;