From 04d2807c230cf9906e8f1f6bc02d61d2771fbb8e Mon Sep 17 00:00:00 2001 From: en Date: Tue, 4 Feb 2025 14:00:21 +0100 Subject: [PATCH 1/6] feat(feed): base layout --- src/components/ProfileSection.tsx | 9 +++++++++ src/layout/feed.tsx | 31 +++++++++++++++++++++++++++++-- src/layout/socialNav.tsx | 20 ++++++++++++-------- src/pages/notifications.tsx | 2 +- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 16e8b0d..1ef0b6e 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -381,6 +381,15 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => { } }) + // Hide follow if own profile + if ( + userState.auth && + userState.user?.pubkey && + userState.user?.pubkey === pubkey + ) { + return null + } + const getUserPubKey = async (): Promise => { if (userState.auth && userState.user?.pubkey) { return userState.user.pubkey as string diff --git a/src/layout/feed.tsx b/src/layout/feed.tsx index 6f68a29..7cef8ec 100644 --- a/src/layout/feed.tsx +++ b/src/layout/feed.tsx @@ -1,10 +1,37 @@ +import { Profile } from 'components/ProfileSection' +import { useAppSelector } from 'hooks' import { Outlet } from 'react-router-dom' export const FeedLayout = () => { + const userState = useAppSelector((state) => state.user) + return (
-

WIP

- +
+
+
+
+
+
+ {userState.auth && userState.user?.pubkey && ( +
+
+
+ +
+
+
+ )} +
+
+ +
+
+
+
+
+
+
) } diff --git a/src/layout/socialNav.tsx b/src/layout/socialNav.tsx index b218b6e..7aa4b47 100644 --- a/src/layout/socialNav.tsx +++ b/src/layout/socialNav.tsx @@ -43,14 +43,18 @@ export const SocialNav = () => { viewBox='0 -32 576 576' svgPath='M511.8 287.6L512.5 447.7C512.5 450.5 512.3 453.1 512 455.8V472C512 494.1 494.1 512 472 512H456C454.9 512 453.8 511.1 452.7 511.9C451.3 511.1 449.9 512 448.5 512H392C369.9 512 352 494.1 352 472V384C352 366.3 337.7 352 320 352H256C238.3 352 224 366.3 224 384V472C224 494.1 206.1 512 184 512H128.1C126.6 512 125.1 511.9 123.6 511.8C122.4 511.9 121.2 512 120 512H104C81.91 512 64 494.1 64 472V360C64 359.1 64.03 358.1 64.09 357.2V287.6H32.05C14.02 287.6 0 273.5 0 255.5C0 246.5 3.004 238.5 10.01 231.5L266.4 8.016C273.4 1.002 281.4 0 288.4 0C295.4 0 303.4 2.004 309.5 7.014L416 100.7V64C416 46.33 430.3 32 448 32H480C497.7 32 512 46.33 512 64V185L564.8 231.5C572.8 238.5 576.9 246.5 575.8 255.5C575.8 273.5 560.8 287.6 543.8 287.6L511.8 287.6z' /> - - + {!!userState.auth && ( + <> + + + + )} { - return

Notifications

+ return

WIP: Notifications

} From 2053e22753cbb34d94507e9fc325acd6b44475e2 Mon Sep 17 00:00:00 2001 From: en Date: Tue, 4 Feb 2025 14:16:12 +0100 Subject: [PATCH 2/6] fix(feed): redirect --- src/layout/feed.tsx | 4 +++- src/pages/{feed.tsx => feed/index.tsx} | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename src/pages/{feed.tsx => feed/index.tsx} (100%) diff --git a/src/layout/feed.tsx b/src/layout/feed.tsx index 7cef8ec..f85f2ce 100644 --- a/src/layout/feed.tsx +++ b/src/layout/feed.tsx @@ -1,9 +1,11 @@ import { Profile } from 'components/ProfileSection' import { useAppSelector } from 'hooks' -import { Outlet } from 'react-router-dom' +import { Navigate, Outlet } from 'react-router-dom' +import { appRoutes } from 'routes' export const FeedLayout = () => { const userState = useAppSelector((state) => state.user) + if (!userState.user?.pubkey) return return (
diff --git a/src/pages/feed.tsx b/src/pages/feed/index.tsx similarity index 100% rename from src/pages/feed.tsx rename to src/pages/feed/index.tsx From 31d9a2b2585ceb770e27b2a46d73b2dd470c2266 Mon Sep 17 00:00:00 2001 From: en Date: Tue, 4 Feb 2025 17:23:11 +0100 Subject: [PATCH 3/6] feat(feed): mods feed --- src/components/Filters/FeedFilter.tsx | 83 ++++++++++ src/pages/feed/FeedTabBlogs.tsx | 16 ++ src/pages/feed/FeedTabMods.tsx | 226 ++++++++++++++++++++++++++ src/pages/feed/FeedTabPosts.tsx | 3 + src/pages/feed/index.tsx | 21 ++- src/pages/feed/loader.ts | 101 ++++++++++++ src/routes/index.tsx | 4 +- src/styles/feed.css | 24 +++ 8 files changed, 476 insertions(+), 2 deletions(-) create mode 100644 src/components/Filters/FeedFilter.tsx create mode 100644 src/pages/feed/FeedTabBlogs.tsx create mode 100644 src/pages/feed/FeedTabMods.tsx create mode 100644 src/pages/feed/FeedTabPosts.tsx create mode 100644 src/pages/feed/loader.ts diff --git a/src/components/Filters/FeedFilter.tsx b/src/components/Filters/FeedFilter.tsx new file mode 100644 index 0000000..bdd8cb3 --- /dev/null +++ b/src/components/Filters/FeedFilter.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import { PropsWithChildren } from 'react' +import { Filter } from '.' +import { FilterOptions, SortBy } from 'types' +import { Dropdown } from './Dropdown' +import { Option } from './Option' +import { DEFAULT_FILTER_OPTIONS } from 'utils' +import { useLocalStorage } from 'hooks' +import { NsfwFilterOptions } from './NsfwFilterOptions' + +interface FeedFilterProps { + tab: number +} +export const FeedFilter = React.memo( + ({ tab, children }: PropsWithChildren) => { + const filterKey = `filter-feed-${tab}` + const [filterOptions, setFilterOptions] = useLocalStorage( + filterKey, + DEFAULT_FILTER_OPTIONS + ) + + return ( + + {/* sort filter options */} + {/* show only if not posts tabs */} + {tab !== 2 && ( + + {Object.values(SortBy).map((item, index) => ( + + ))} + + )} + + {/* nsfw filter options */} + + + + + {/* source filter options */} + + + + + + {children} + + ) + } +) diff --git a/src/pages/feed/FeedTabBlogs.tsx b/src/pages/feed/FeedTabBlogs.tsx new file mode 100644 index 0000000..f151f19 --- /dev/null +++ b/src/pages/feed/FeedTabBlogs.tsx @@ -0,0 +1,16 @@ +import { useAppSelector, useLocalStorage } from 'hooks' +import { FilterOptions } from 'types' +import { DEFAULT_FILTER_OPTIONS } from 'utils' + +export const FeedTabBlogs = () => { + const userState = useAppSelector((state) => state.user) + const userPubkey = userState.user?.pubkey as string | undefined + + const filterKey = 'filter-feed-1' + const [filterOptions] = useLocalStorage(filterKey, { + ...DEFAULT_FILTER_OPTIONS + }) + + if (!userPubkey) return null + return <> +} diff --git a/src/pages/feed/FeedTabMods.tsx b/src/pages/feed/FeedTabMods.tsx new file mode 100644 index 0000000..221c962 --- /dev/null +++ b/src/pages/feed/FeedTabMods.tsx @@ -0,0 +1,226 @@ +import { useAppSelector, useLocalStorage, useNDKContext } from 'hooks' +import { useEffect, useMemo, useState } from 'react' +import { useLoaderData } from 'react-router-dom' +import { + FilterOptions, + ModDetails, + NSFWFilter, + RepostFilter, + SortBy +} from 'types' +import { + constructModListFromEvents, + DEFAULT_FILTER_OPTIONS, + orderEventsChronologically +} from 'utils' +import { FeedPageLoaderResult } from './loader' +import { + NDKFilter, + NDKKind, + NDKSubscriptionCacheUsage +} from '@nostr-dev-kit/ndk' +import { LoadingSpinner } from 'components/LoadingSpinner' +import { ModCard } from 'components/ModCard' + +export const FeedTabMods = () => { + const { muteLists, nsfwList, repostList, followList } = + useLoaderData() as FeedPageLoaderResult + const userState = useAppSelector((state) => state.user) + const userPubkey = userState.user?.pubkey as string | undefined + + const filterKey = 'filter-feed-0' + const [filterOptions] = useLocalStorage(filterKey, { + ...DEFAULT_FILTER_OPTIONS + }) + const { ndk } = useNDKContext() + const [mods, setMods] = useState([]) + const [isFetching, setIsFetching] = useState(false) + const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true) + const [showing, setShowing] = useState(10) + + const handleLoadMore = () => { + setShowing((prev) => prev + 10) + const lastMod = mods[mods.length - 1] + const filter: NDKFilter = { + authors: [...followList], + kinds: [NDKKind.Classified], + limit: 20 + } + + if (filterOptions.source === window.location.host) { + filter['#r'] = [window.location.host] + } + + if (filterOptions.sort === SortBy.Latest) { + filter.until = lastMod.published_at + } else if (filterOptions.sort === SortBy.Oldest) { + filter.since = lastMod.published_at + } + + setIsFetching(true) + ndk + .fetchEvents(filter, { + closeOnEose: true, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + }) + .then((ndkEventSet) => { + const ndkEvents = Array.from(ndkEventSet) + orderEventsChronologically( + ndkEvents, + filterOptions.sort === SortBy.Latest + ) + setMods((prevMods) => { + const newMods = constructModListFromEvents(ndkEvents) + 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) + + if (newMods.length < 20) { + setIsLoadMoreVisible(false) + } + + return uniqueMods + }) + }) + .finally(() => { + setIsFetching(false) + }) + } + + useEffect(() => { + setIsFetching(true) + const filter: NDKFilter = { + authors: [...followList], + kinds: [NDKKind.Classified], + limit: 50 + } + // If the source matches the current window location, add a filter condition + 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) + setMods(constructModListFromEvents(ndkEvents)) + }) + .finally(() => { + setIsFetching(false) + }) + }, [filterOptions.source, followList, ndk]) + + const filteredModList = useMemo(() => { + const nsfwFilter = (mods: ModDetails[]) => { + // Add nsfw tag to mods included in nsfwList + if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) { + mods = mods.map((mod) => { + return !mod.nsfw && nsfwList.includes(mod.aTag) + ? { ...mod, nsfw: true } + : mod + }) + } + + // Determine the filtering logic based on the NSFW filter option + switch (filterOptions.nsfw) { + case NSFWFilter.Hide_NSFW: + // If 'Hide_NSFW' is selected, filter out NSFW mods + return mods.filter((mod) => !mod.nsfw && !nsfwList.includes(mod.aTag)) + case NSFWFilter.Show_NSFW: + // If 'Show_NSFW' is selected, return all mods (no filtering) + return mods + case NSFWFilter.Only_NSFW: + // If 'Only_NSFW' is selected, filter to show only NSFW mods + return mods.filter((mod) => mod.nsfw || nsfwList.includes(mod.aTag)) + } + } + + const repostFilter = (mods: ModDetails[]) => { + if (filterOptions.repost !== RepostFilter.Hide_Repost) { + // Add repost tag to mods included in repostList + mods = mods.map((mod) => { + return !mod.repost && repostList.includes(mod.aTag) + ? { ...mod, repost: true } + : mod + }) + } + // Determine the filtering logic based on the Repost filter option + switch (filterOptions.repost) { + case RepostFilter.Hide_Repost: + return mods.filter( + (mod) => !mod.repost && !repostList.includes(mod.aTag) + ) + case RepostFilter.Show_Repost: + return mods + case RepostFilter.Only_Repost: + return mods.filter( + (mod) => mod.repost || repostList.includes(mod.aTag) + ) + } + } + + let filtered = nsfwFilter(mods) + filtered = repostFilter(filtered) + + // Filter out mods from muted authors and replaceable events + filtered = filtered.filter( + (mod) => + !muteLists.admin.authors.includes(mod.author) && + !muteLists.admin.replaceableEvents.includes(mod.aTag) + ) + + if (filterOptions.sort === SortBy.Latest) { + filtered.sort((a, b) => b.published_at - a.published_at) + } else if (filterOptions.sort === SortBy.Oldest) { + filtered.sort((a, b) => a.published_at - b.published_at) + } + showing > 0 && filtered.splice(showing) + return filtered + }, [ + filterOptions.nsfw, + filterOptions.repost, + filterOptions.sort, + mods, + muteLists.admin.authors, + muteLists.admin.replaceableEvents, + nsfwList, + repostList, + showing + ]) + + if (!userPubkey) return null + return ( + <> + {isFetching && } +
+
+ {filteredModList.map((mod) => ( + + ))} + {filteredModList.length === 0 && !isFetching && ( +
+

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

+
+ )} +
+
+ {!isFetching && isLoadMoreVisible && ( +
+ +
+ )} + + ) +} diff --git a/src/pages/feed/FeedTabPosts.tsx b/src/pages/feed/FeedTabPosts.tsx new file mode 100644 index 0000000..c2ecfdc --- /dev/null +++ b/src/pages/feed/FeedTabPosts.tsx @@ -0,0 +1,3 @@ +export const FeedTabPosts = () => { + return <>WIP: Posts +} diff --git a/src/pages/feed/index.tsx b/src/pages/feed/index.tsx index 88694d9..a2bef4c 100644 --- a/src/pages/feed/index.tsx +++ b/src/pages/feed/index.tsx @@ -1,3 +1,22 @@ +import { Tabs } from 'components/Tabs' +import { useState } from 'react' +import { FeedTabBlogs } from './FeedTabBlogs' +import { FeedTabMods } from './FeedTabMods' +import { FeedTabPosts } from './FeedTabPosts' +import { FeedFilter } from 'components/Filters/FeedFilter' + export const FeedPage = () => { - return

Feed

+ const [tab, setTab] = useState(0) + + return ( + <> + + + + + {tab === 0 && } + {tab === 1 && } + {tab === 2 && } + + ) } diff --git a/src/pages/feed/loader.ts b/src/pages/feed/loader.ts new file mode 100644 index 0000000..6f76ce8 --- /dev/null +++ b/src/pages/feed/loader.ts @@ -0,0 +1,101 @@ +import { NDKUser } from '@nostr-dev-kit/ndk' +import { NDKContextType } from 'contexts/NDKContext' +import { redirect } from 'react-router-dom' +import { appRoutes } from 'routes' +import { store } from 'store' +import { MuteLists } from 'types' +import { + CurationSetIdentifiers, + getFallbackPubkey, + getReportingSet, + log, + LogType +} from 'utils' + +export interface FeedPageLoaderResult { + muteLists: { + admin: MuteLists + user: MuteLists + } + nsfwList: string[] + repostList: string[] + followList: string[] +} + +export const feedPageLoader = (ndkContext: NDKContextType) => async () => { + // Empty result + const result: FeedPageLoaderResult = { + muteLists: { + admin: { + authors: [], + replaceableEvents: [] + }, + user: { + authors: [], + replaceableEvents: [] + } + }, + nsfwList: [], + repostList: [], + followList: [] + } + + // Get the current state + const userState = store.getState().user + const loggedInUserPubkey = + (userState?.user?.pubkey as string | undefined) || getFallbackPubkey() + if (!loggedInUserPubkey) return redirect(appRoutes.home) + const ndkUser = new NDKUser({ pubkey: loggedInUserPubkey }) + ndkUser.ndk = ndkContext.ndk + + const settled = await Promise.allSettled([ + ndkContext.getMuteLists(loggedInUserPubkey), + getReportingSet(CurationSetIdentifiers.NSFW, ndkContext), + getReportingSet(CurationSetIdentifiers.Repost, ndkContext), + ndkUser.followSet() + ]) + + // Check the mutelist event result + const muteListResult = settled[0] + if (muteListResult.status === 'fulfilled' && muteListResult.value) { + result.muteLists = muteListResult.value + } else if (muteListResult.status === 'rejected') { + log(true, LogType.Error, 'Failed to fetch mutelist.', muteListResult.reason) + } + + // Check the nsfwlist event result + const nsfwListResult = settled[1] + if (nsfwListResult.status === 'fulfilled' && nsfwListResult.value) { + result.nsfwList = nsfwListResult.value + } else if (nsfwListResult.status === 'rejected') { + log(true, LogType.Error, 'Failed to fetch nsfwlist.', nsfwListResult.reason) + } + + // Check the repostlist event result + const repostListResult = settled[2] + if (repostListResult.status === 'fulfilled' && repostListResult.value) { + result.repostList = repostListResult.value + } else if (repostListResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Failed to fetch repost list.', + repostListResult.reason + ) + } + + // Check the followSet result + const followSetResult = settled[3] + if (followSetResult.status === 'fulfilled') { + result.followList = Array.from(followSetResult.value) + } else if (followSetResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Failed to fetch follow set.', + followSetResult.reason + ) + } + + return result +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f26bfc9..066c487 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -19,6 +19,7 @@ import { NotFoundPage } from '../pages/404' import { submitModRouteAction } from 'pages/submitMod/action' import { FeedLayout } from '../layout/feed' import { FeedPage } from '../pages/feed' +import { feedPageLoader } from 'pages/feed/loader' import { NotificationsPage } from '../pages/notifications' import { WritePage } from '../pages/write' import { writeRouteAction } from '../pages/write/action' @@ -197,7 +198,8 @@ export const routerWithNdkContext = (context: NDKContextType) => children: [ { path: appRoutes.feed, - element: + element: , + loader: feedPageLoader(context) }, { path: appRoutes.notifications, diff --git a/src/styles/feed.css b/src/styles/feed.css index 4ec6549..681d937 100644 --- a/src/styles/feed.css +++ b/src/styles/feed.css @@ -119,3 +119,27 @@ padding-top: 15px; } +.IBMSMListFeedLoadMore { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.btnMain.IBMSMListFeedLoadMoreBtn { + width: 100%; + max-width: 300px; +} + +.IBMSMListFeedNoPosts { + width: 100%; + padding: 10px; + border-radius: 10px; + border: solid 1px rgba(255,255,255,0.1); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: rgba(255,255,255,0.25); + font-weight: 500; +} From 2991dd448c9376ef01d7882ca6854360997fc95c Mon Sep 17 00:00:00 2001 From: en Date: Tue, 4 Feb 2025 18:34:37 +0100 Subject: [PATCH 4/6] feat(feed): add blogs feed --- src/pages/feed/FeedTabBlogs.tsx | 185 +++++++++++++++++++++++++++++++- src/pages/feed/FeedTabMods.tsx | 13 ++- 2 files changed, 189 insertions(+), 9 deletions(-) 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], From d1e85dab962eae4bbdfed13999a74c258717ee5b Mon Sep 17 00:00:00 2001 From: en Date: Tue, 4 Feb 2025 18:47:51 +0100 Subject: [PATCH 5/6] fix(feed): hide Load More if no posts --- src/pages/feed/FeedTabBlogs.tsx | 2 +- src/pages/feed/FeedTabMods.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/feed/FeedTabBlogs.tsx b/src/pages/feed/FeedTabBlogs.tsx index 5088f2c..08b96db 100644 --- a/src/pages/feed/FeedTabBlogs.tsx +++ b/src/pages/feed/FeedTabBlogs.tsx @@ -177,7 +177,7 @@ export const FeedTabBlogs = () => { )}
- {!isFetching && isLoadMoreVisible && ( + {!isFetching && isLoadMoreVisible && filteredBlogs.length > 0 && (
- {!isFetching && isLoadMoreVisible && ( + {!isFetching && isLoadMoreVisible && filteredModList.length > 0 && (