diff --git a/src/pages/feed/FeedTabBlogs.tsx b/src/pages/feed/FeedTabBlogs.tsx index f151f19..5088f2c 100644 --- a/src/pages/feed/FeedTabBlogs.tsx +++ b/src/pages/feed/FeedTabBlogs.tsx @@ -1,8 +1,21 @@ -import { useAppSelector, useLocalStorage } from 'hooks' -import { FilterOptions } from 'types' -import { DEFAULT_FILTER_OPTIONS } from 'utils' +import { + NDKFilter, + NDKKind, + NDKSubscriptionCacheUsage +} from '@nostr-dev-kit/ndk' +import { useAppSelector, useLocalStorage, useNDKContext } from 'hooks' +import { useEffect, useMemo, useState } from 'react' +import { BlogCardDetails, FilterOptions, NSFWFilter, SortBy } from 'types' +import { DEFAULT_FILTER_OPTIONS, extractBlogCardDetails } from 'utils' +import { FeedPageLoaderResult } from './loader' +import { useLoaderData } from 'react-router-dom' +import { LoadingSpinner } from 'components/LoadingSpinner' +import { BlogCard } from 'components/BlogCard' export const FeedTabBlogs = () => { + const SHOWING_STEP = 10 + const { muteLists, nsfwList, followList } = + useLoaderData() as FeedPageLoaderResult const userState = useAppSelector((state) => state.user) const userPubkey = userState.user?.pubkey as string | undefined @@ -10,7 +23,171 @@ export const FeedTabBlogs = () => { const [filterOptions] = useLocalStorage(filterKey, { ...DEFAULT_FILTER_OPTIONS }) + const { ndk } = useNDKContext() + const [blogs, setBlogs] = useState[]>([]) + const [isFetching, setIsFetching] = useState(false) + const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true) + const [showing, setShowing] = useState(SHOWING_STEP) + + const handleLoadMore = () => { + const LOAD_MORE_STEP = SHOWING_STEP * 2 + setShowing((prev) => prev + SHOWING_STEP) + const lastBlog = filteredBlogs[filteredBlogs.length - 1] + const filter: NDKFilter = { + authors: [...followList], + kinds: [NDKKind.Article], + limit: LOAD_MORE_STEP + } + + if (filterOptions.nsfw === NSFWFilter.Only_NSFW) { + filter['#L'] = ['content-warning'] + } + + if (filterOptions.source === window.location.host) { + filter['#r'] = [window.location.host] + } + + if (filterOptions.sort === SortBy.Latest) { + filter.until = lastBlog.published_at + } else if (filterOptions.sort === SortBy.Oldest) { + filter.since = lastBlog.published_at + } + + setIsFetching(true) + ndk + .fetchEvents(filter, { + closeOnEose: true, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + }) + .then((ndkEventSet) => { + setBlogs((prevBlogs) => { + const newBlogs = Array.from(ndkEventSet) + const combinedBlogs = [ + ...prevBlogs, + ...newBlogs.map(extractBlogCardDetails) + ] + const uniqueBlogs = Array.from( + new Set(combinedBlogs.map((b) => b.id)) + ) + .map((id) => combinedBlogs.find((b) => b.id === id)) + .filter((b): b is BlogCardDetails => b !== undefined) + + if (newBlogs.length < LOAD_MORE_STEP) { + setIsLoadMoreVisible(false) + } + + return uniqueBlogs + }) + }) + .finally(() => { + setIsFetching(false) + }) + } + + useEffect(() => { + setIsFetching(true) + setIsLoadMoreVisible(true) + const filter: NDKFilter = { + authors: [...followList], + kinds: [NDKKind.Article], + limit: 50 + } + + if (filterOptions.nsfw === NSFWFilter.Only_NSFW) { + filter['#L'] = ['content-warning'] + } + + if (filterOptions.source === window.location.host) { + filter['#r'] = [window.location.host] + } + + ndk + .fetchEvents(filter, { + closeOnEose: true, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + }) + .then((ndkEventSet) => { + const ndkEvents = Array.from(ndkEventSet) + setBlogs(ndkEvents.map(extractBlogCardDetails)) + }) + .finally(() => { + setIsFetching(false) + }) + }, [filterOptions.nsfw, filterOptions.source, followList, ndk]) + + const filteredBlogs = useMemo(() => { + let _blogs = blogs || [] + + // Add nsfw tag to blogs included in nsfwList + if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) { + _blogs = _blogs.map((b) => { + return !b.nsfw && b.aTag && nsfwList.includes(b.aTag) + ? { ...b, nsfw: true } + : b + }) + } + // Filter nsfw (Hide_NSFW option) + _blogs = _blogs.filter( + (b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW) + ) + + _blogs = _blogs.filter( + (b) => + !muteLists.admin.authors.includes(b.author!) && + !muteLists.admin.replaceableEvents.includes(b.aTag!) + ) + + if (filterOptions.sort === SortBy.Latest) { + _blogs.sort((a, b) => + a.published_at && b.published_at ? b.published_at - a.published_at : 0 + ) + } else if (filterOptions.sort === SortBy.Oldest) { + _blogs.sort((a, b) => + a.published_at && b.published_at ? a.published_at - b.published_at : 0 + ) + } + + showing > 0 && _blogs.splice(showing) + return _blogs + }, [ + blogs, + filterOptions.nsfw, + filterOptions.sort, + muteLists.admin.authors, + muteLists.admin.replaceableEvents, + nsfwList, + showing + ]) if (!userPubkey) return null - return <> + return ( + <> + {isFetching && ( + + )} +
+
+ {filteredBlogs.map((blog) => ( + + ))} + {filteredBlogs.length === 0 && !isFetching && ( +
+

You aren't following people (or there are no posts to show)

+
+ )} +
+
+ {!isFetching && isLoadMoreVisible && ( +
+ +
+ )} + + ) } diff --git a/src/pages/feed/FeedTabMods.tsx b/src/pages/feed/FeedTabMods.tsx index 221c962..65af174 100644 --- a/src/pages/feed/FeedTabMods.tsx +++ b/src/pages/feed/FeedTabMods.tsx @@ -23,6 +23,7 @@ import { LoadingSpinner } from 'components/LoadingSpinner' import { ModCard } from 'components/ModCard' export const FeedTabMods = () => { + const SHOWING_STEP = 10 const { muteLists, nsfwList, repostList, followList } = useLoaderData() as FeedPageLoaderResult const userState = useAppSelector((state) => state.user) @@ -36,15 +37,16 @@ export const FeedTabMods = () => { const [mods, setMods] = useState([]) const [isFetching, setIsFetching] = useState(false) const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true) - const [showing, setShowing] = useState(10) + const [showing, setShowing] = useState(SHOWING_STEP) const handleLoadMore = () => { - setShowing((prev) => prev + 10) - const lastMod = mods[mods.length - 1] + const LOAD_MORE_STEP = SHOWING_STEP * 2 + setShowing((prev) => prev + SHOWING_STEP) + const lastMod = filteredModList[filteredModList.length - 1] const filter: NDKFilter = { authors: [...followList], kinds: [NDKKind.Classified], - limit: 20 + limit: LOAD_MORE_STEP } if (filterOptions.source === window.location.host) { @@ -78,7 +80,7 @@ export const FeedTabMods = () => { .map((id) => combinedMods.find((mod) => mod.id === id)) .filter((mod): mod is ModDetails => mod !== undefined) - if (newMods.length < 20) { + if (newMods.length < LOAD_MORE_STEP) { setIsLoadMoreVisible(false) } @@ -92,6 +94,7 @@ export const FeedTabMods = () => { useEffect(() => { setIsFetching(true) + setIsLoadMoreVisible(true) const filter: NDKFilter = { authors: [...followList], kinds: [NDKKind.Classified],