feat(feed): add blogs feed

This commit is contained in:
en 2025-02-04 18:34:37 +01:00
parent 31d9a2b258
commit 2991dd448c
2 changed files with 189 additions and 9 deletions

View File

@ -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<FilterOptions>(filterKey, {
...DEFAULT_FILTER_OPTIONS
})
const { ndk } = useNDKContext()
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>([])
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 && (
<LoadingSpinner desc='Fetching blog details from relays' />
)}
<div className='IBMSMSplitMainFullSideSec IBMSMSMFSSContent'>
<div className='IBMSMList IBMSMListFeed'>
{filteredBlogs.map((blog) => (
<BlogCard key={blog.id} {...blog} />
))}
{filteredBlogs.length === 0 && !isFetching && (
<div className='IBMSMListFeedNoPosts'>
<p>You aren't following people (or there are no posts to show)</p>
</div>
)}
</div>
</div>
{!isFetching && isLoadMoreVisible && (
<div className='IBMSMListFeedLoadMore'>
<button
className='btn btnMain IBMSMListFeedLoadMoreBtn'
type='button'
onClick={handleLoadMore}
>
Load More
</button>
</div>
)}
</>
)
}

View File

@ -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<ModDetails[]>([])
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],