diff --git a/src/pages/mods/index.tsx b/src/pages/mods/index.tsx index abd9870..86101ab 100644 --- a/src/pages/mods/index.tsx +++ b/src/pages/mods/index.tsx @@ -1,6 +1,5 @@ import { ModFilter } from 'components/Filters/ModsFilter' -import { Pagination } from 'components/Pagination' -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createSearchParams, useLoaderData, @@ -8,7 +7,7 @@ import { } from 'react-router-dom' import { LoadingSpinner } from '../../components/LoadingSpinner' import { ModCard } from '../../components/ModCard' -import { MOD_FILTER_LIMIT } from '../../constants' +import { MOD_FILTER_LIMIT, T_TAG_VALUE } from '../../constants' import { useAppSelector, useFilteredMods, @@ -20,77 +19,156 @@ import '../../styles/filters.css' import '../../styles/pagination.css' import '../../styles/search.css' import '../../styles/styles.css' -import { FilterOptions, ModDetails } from '../../types' -import { DEFAULT_FILTER_OPTIONS, scrollIntoView } from 'utils' +import { FilterOptions, ModDetails, SortBy } from '../../types' +import { + DEFAULT_FILTER_OPTIONS, + extractModData, + isModDataComplete +} from 'utils' import { SearchInput } from 'components/SearchInput' import { ModsPageLoaderResult } from './loader' +import { FetchModsOptions } from 'contexts/NDKContext' +import { + NDKFilter, + NDKKind, + NDKSubscription, + NDKSubscriptionCacheUsage +} from '@nostr-dev-kit/ndk' export const ModsPage = () => { const scrollTargetRef = useRef(null) const { repostList, muteLists, nsfwList } = useLoaderData() as ModsPageLoaderResult - const { fetchMods } = useNDKContext() + const { ndk, fetchMods } = useNDKContext() const [isFetching, setIsFetching] = useState(false) const [mods, setMods] = useState([]) - + const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true) const [filterOptions] = useLocalStorage( 'filter', DEFAULT_FILTER_OPTIONS ) - const [page, setPage] = useState(1) - const userState = useAppSelector((state) => state.user) useEffect(() => { setIsFetching(true) fetchMods({ source: filterOptions.source }) .then((res) => { - setMods(res) + if (filterOptions.sort === SortBy.Latest) { + res.sort((a, b) => b.published_at - a.published_at) + } else if (filterOptions.sort === SortBy.Oldest) { + res.sort((a, b) => a.published_at - b.published_at) + } + setIsLoadMoreVisible(res.length >= MOD_FILTER_LIMIT) + setMods(res.slice(0, MOD_FILTER_LIMIT)) }) .finally(() => { setIsFetching(false) }) - }, [filterOptions.source, fetchMods]) + }, [filterOptions.source, fetchMods, filterOptions.sort]) - const handleNext = useCallback(() => { + const lastMod: ModDetails | undefined = useMemo(() => { + // For the latest sort find oldest mod + // for the oldest sort find newest mod + return mods.reduce((prev, current) => { + if (!prev) return current + if (filterOptions.sort === SortBy.Latest) { + return current.edited_at < prev.edited_at ? current : prev + } else if (filterOptions.sort === SortBy.Oldest) { + return current.edited_at > prev.edited_at ? current : prev + } + return prev + }, undefined as ModDetails | undefined) + }, [mods, filterOptions.sort]) + + // Add missing mods to the list + useEffect(() => { + let sub: NDKSubscription + if (lastMod) { + const filter: NDKFilter = { + kinds: [NDKKind.Classified], + '#t': [T_TAG_VALUE] + } + if (filterOptions.sort === SortBy.Latest) { + filter.since = lastMod.edited_at + 1 + } else if (filterOptions.sort === SortBy.Oldest) { + filter.until = lastMod.edited_at - 1 + } + if (filterOptions.source === window.location.host) { + filter['#r'] = [window.location.host] + } + sub = ndk.subscribe( + filter, + { + closeOnEose: false, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + }, + undefined, + { + onEvent: (ndkEvent) => { + setMods((prevMods) => { + // Skip if not valid + if (!isModDataComplete(ndkEvent)) { + return prevMods + } + + // Skip existing + if ( + prevMods.find( + (e) => + e.id === ndkEvent.id || + prevMods.findIndex((n) => n.id === ndkEvent.id) !== -1 + ) + ) { + return prevMods + } + const newMod = extractModData(ndkEvent) + return [...prevMods, newMod] + }) + } + } + ) + } + return () => { + if (sub) sub.stop() + } + }, [filterOptions.sort, filterOptions.source, lastMod, ndk]) + + const handleLoadMore = useCallback(() => { setIsFetching(true) - const until = - mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined + const fetchModsOptions: FetchModsOptions = { + source: filterOptions.source + } - fetchMods({ - source: filterOptions.source, - until - }) + if (lastMod) { + if (filterOptions.sort === SortBy.Latest) { + fetchModsOptions.until = lastMod.edited_at - 1 + } else if (filterOptions.sort === SortBy.Oldest) { + fetchModsOptions.since = lastMod.edited_at + 1 + } + } + + fetchMods(fetchModsOptions) .then((res) => { - setMods(res) - setPage((prev) => prev + 1) - scrollIntoView(scrollTargetRef.current) + setMods((prevMods) => { + const newMods = res + const combinedMods = [...prevMods, ...newMods] + const uniqueMods = Array.from( + new Set(combinedMods.map((mod) => mod.id)) + ) + .map((id) => combinedMods.find((mod) => mod.id === id)) + .filter((mod): mod is ModDetails => mod !== undefined) + + setIsLoadMoreVisible(newMods.length >= MOD_FILTER_LIMIT) + + return uniqueMods + }) }) .finally(() => { setIsFetching(false) }) - }, [filterOptions.source, mods, fetchMods]) - - const handlePrev = useCallback(() => { - setIsFetching(true) - - const since = mods.length > 0 ? mods[0].published_at + 1 : undefined - - fetchMods({ - source: filterOptions.source, - since - }) - .then((res) => { - setMods(res) - setPage((prev) => prev - 1) - scrollIntoView(scrollTargetRef.current) - }) - .finally(() => { - setIsFetching(false) - }) - }, [filterOptions.source, mods, fetchMods]) + }, [fetchMods, filterOptions, lastMod]) const filteredModList = useFilteredMods( mods, @@ -121,12 +199,17 @@ export const ModsPage = () => { - + {!isFetching && isLoadMoreVisible && filteredModList.length > 0 && ( +
+ +
+ )}