chore(git): merge pull request #218 from feat/131-feed into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m11s
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m11s
Reviewed-on: #218
This commit is contained in:
commit
215c738884
83
src/components/Filters/FeedFilter.tsx
Normal file
83
src/components/Filters/FeedFilter.tsx
Normal file
@ -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<FeedFilterProps>) => {
|
||||
const filterKey = `filter-feed-${tab}`
|
||||
const [filterOptions, setFilterOptions] = useLocalStorage<FilterOptions>(
|
||||
filterKey,
|
||||
DEFAULT_FILTER_OPTIONS
|
||||
)
|
||||
|
||||
return (
|
||||
<Filter>
|
||||
{/* sort filter options */}
|
||||
{/* show only if not posts tabs */}
|
||||
{tab !== 2 && (
|
||||
<Dropdown label={filterOptions.sort}>
|
||||
{Object.values(SortBy).map((item, index) => (
|
||||
<Option
|
||||
key={`sortByItem-${index}`}
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{/* nsfw filter options */}
|
||||
<Dropdown label={filterOptions.nsfw}>
|
||||
<NsfwFilterOptions filterKey={filterKey} />
|
||||
</Dropdown>
|
||||
|
||||
{/* source filter options */}
|
||||
<Dropdown
|
||||
label={
|
||||
filterOptions.source === window.location.host
|
||||
? `Show From: ${filterOptions.source}`
|
||||
: 'Show All'
|
||||
}
|
||||
>
|
||||
<Option
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: window.location.host
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show From: {window.location.host}
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: 'Show All'
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show All
|
||||
</Option>
|
||||
</Dropdown>
|
||||
|
||||
{children}
|
||||
</Filter>
|
||||
)
|
||||
}
|
||||
)
|
@ -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<string | null> => {
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
return userState.user.pubkey as string
|
||||
|
@ -1,10 +1,39 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Profile } from 'components/ProfileSection'
|
||||
import { useAppSelector } from 'hooks'
|
||||
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 <Navigate to={appRoutes.home} />
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<h1>WIP</h1>
|
||||
<Outlet />
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSMSplitMain'>
|
||||
<div className='IBMSMSplitMainFullSide'>
|
||||
<div className='IBMSMSplitMainFullSideFeedWrapper'>
|
||||
<div className='IBMSMSplitMainFullSideFWSide'>
|
||||
{userState.auth && userState.user?.pubkey && (
|
||||
<div className='IBMSMSplitMainSmallSide'>
|
||||
<div className='IBMSMSplitMainSmallSideSecWrapper'>
|
||||
<div className='IBMSMSplitMainSmallSideSec'>
|
||||
<Profile pubkey={userState.user.pubkey as string} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='IBMSMSplitMainFullSideFWMid'>
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className='IBMSMSplitMainFullSideFWSide'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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'
|
||||
/>
|
||||
<NavButton
|
||||
to={appRoutes.feed}
|
||||
svgPath='M88 48C101.3 48 112 58.75 112 72V120C112 133.3 101.3 144 88 144H40C26.75 144 16 133.3 16 120V72C16 58.75 26.75 48 40 48H88zM480 64C497.7 64 512 78.33 512 96C512 113.7 497.7 128 480 128H192C174.3 128 160 113.7 160 96C160 78.33 174.3 64 192 64H480zM480 224C497.7 224 512 238.3 512 256C512 273.7 497.7 288 480 288H192C174.3 288 160 273.7 160 256C160 238.3 174.3 224 192 224H480zM480 384C497.7 384 512 398.3 512 416C512 433.7 497.7 448 480 448H192C174.3 448 160 433.7 160 416C160 398.3 174.3 384 192 384H480zM16 232C16 218.7 26.75 208 40 208H88C101.3 208 112 218.7 112 232V280C112 293.3 101.3 304 88 304H40C26.75 304 16 293.3 16 280V232zM88 368C101.3 368 112 378.7 112 392V440C112 453.3 101.3 464 88 464H40C26.75 464 16 453.3 16 440V392C16 378.7 26.75 368 40 368H88z'
|
||||
/>
|
||||
<NavButton
|
||||
to={appRoutes.notifications}
|
||||
svgPath='M256 32V51.2C329 66.03 384 130.6 384 208V226.8C384 273.9 401.3 319.2 432.5 354.4L439.9 362.7C448.3 372.2 450.4 385.6 445.2 397.1C440 408.6 428.6 416 416 416H32C19.4 416 7.971 408.6 2.809 397.1C-2.353 385.6-.2883 372.2 8.084 362.7L15.5 354.4C46.74 319.2 64 273.9 64 226.8V208C64 130.6 118.1 66.03 192 51.2V32C192 14.33 206.3 0 224 0C241.7 0 256 14.33 256 32H256zM224 512C207 512 190.7 505.3 178.7 493.3C166.7 481.3 160 464.1 160 448H288C288 464.1 281.3 481.3 269.3 493.3C257.3 505.3 240.1 512 224 512z'
|
||||
/>
|
||||
{!!userState.auth && (
|
||||
<>
|
||||
<NavButton
|
||||
to={appRoutes.feed}
|
||||
svgPath='M88 48C101.3 48 112 58.75 112 72V120C112 133.3 101.3 144 88 144H40C26.75 144 16 133.3 16 120V72C16 58.75 26.75 48 40 48H88zM480 64C497.7 64 512 78.33 512 96C512 113.7 497.7 128 480 128H192C174.3 128 160 113.7 160 96C160 78.33 174.3 64 192 64H480zM480 224C497.7 224 512 238.3 512 256C512 273.7 497.7 288 480 288H192C174.3 288 160 273.7 160 256C160 238.3 174.3 224 192 224H480zM480 384C497.7 384 512 398.3 512 416C512 433.7 497.7 448 480 448H192C174.3 448 160 433.7 160 416C160 398.3 174.3 384 192 384H480zM16 232C16 218.7 26.75 208 40 208H88C101.3 208 112 218.7 112 232V280C112 293.3 101.3 304 88 304H40C26.75 304 16 293.3 16 280V232zM88 368C101.3 368 112 378.7 112 392V440C112 453.3 101.3 464 88 464H40C26.75 464 16 453.3 16 440V392C16 378.7 26.75 368 40 368H88z'
|
||||
/>
|
||||
<NavButton
|
||||
to={appRoutes.notifications}
|
||||
svgPath='M256 32V51.2C329 66.03 384 130.6 384 208V226.8C384 273.9 401.3 319.2 432.5 354.4L439.9 362.7C448.3 372.2 450.4 385.6 445.2 397.1C440 408.6 428.6 416 416 416H32C19.4 416 7.971 408.6 2.809 397.1C-2.353 385.6-.2883 372.2 8.084 362.7L15.5 354.4C46.74 319.2 64 273.9 64 226.8V208C64 130.6 118.1 66.03 192 51.2V32C192 14.33 206.3 0 224 0C241.7 0 256 14.33 256 32H256zM224 512C207 512 190.7 505.3 178.7 493.3C166.7 481.3 160 464.1 160 448H288C288 464.1 281.3 481.3 269.3 493.3C257.3 505.3 240.1 512 224 512z'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<NavButton
|
||||
to={appRoutes.search}
|
||||
svgPath='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'
|
||||
|
@ -1,3 +0,0 @@
|
||||
export const FeedPage = () => {
|
||||
return <h2>Feed</h2>
|
||||
}
|
193
src/pages/feed/FeedTabBlogs.tsx
Normal file
193
src/pages/feed/FeedTabBlogs.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
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
|
||||
|
||||
const filterKey = 'filter-feed-1'
|
||||
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 (
|
||||
<>
|
||||
{isFetching && (
|
||||
<LoadingSpinner desc='Fetching blog details from relays' />
|
||||
)}
|
||||
{filteredBlogs.length === 0 && !isFetching && (
|
||||
<div className='IBMSMListFeedNoPosts'>
|
||||
<p>You aren't following people (or there are no posts to show)</p>
|
||||
</div>
|
||||
)}
|
||||
<div className='IBMSMSplitMainFullSideSec IBMSMSMFSSContent'>
|
||||
<div className='IBMSMList IBMSMListFeed'>
|
||||
{filteredBlogs.map((blog) => (
|
||||
<BlogCard key={blog.id} {...blog} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!isFetching && isLoadMoreVisible && filteredBlogs.length > 0 && (
|
||||
<div className='IBMSMListFeedLoadMore'>
|
||||
<button
|
||||
className='btn btnMain IBMSMListFeedLoadMoreBtn'
|
||||
type='button'
|
||||
onClick={handleLoadMore}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
229
src/pages/feed/FeedTabMods.tsx
Normal file
229
src/pages/feed/FeedTabMods.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
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 SHOWING_STEP = 10
|
||||
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<FilterOptions>(filterKey, {
|
||||
...DEFAULT_FILTER_OPTIONS
|
||||
})
|
||||
const { ndk } = useNDKContext()
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
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 lastMod = filteredModList[filteredModList.length - 1]
|
||||
const filter: NDKFilter = {
|
||||
authors: [...followList],
|
||||
kinds: [NDKKind.Classified],
|
||||
limit: LOAD_MORE_STEP
|
||||
}
|
||||
|
||||
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 < LOAD_MORE_STEP) {
|
||||
setIsLoadMoreVisible(false)
|
||||
}
|
||||
|
||||
return uniqueMods
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsFetching(true)
|
||||
setIsLoadMoreVisible(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 && <LoadingSpinner desc='Fetching mod details from relays' />}
|
||||
{filteredModList.length === 0 && !isFetching && (
|
||||
<div className='IBMSMListFeedNoPosts'>
|
||||
<p>You aren't following people (or there are no posts to show)</p>
|
||||
</div>
|
||||
)}
|
||||
<div className='IBMSMSplitMainFullSideSec IBMSMSMFSSContent'>
|
||||
<div className='IBMSMList IBMSMListFeed'>
|
||||
{filteredModList.map((mod) => (
|
||||
<ModCard key={mod.id} {...mod} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!isFetching && isLoadMoreVisible && filteredModList.length > 0 && (
|
||||
<div className='IBMSMListFeedLoadMore'>
|
||||
<button
|
||||
className='btn btnMain IBMSMListFeedLoadMoreBtn'
|
||||
type='button'
|
||||
onClick={handleLoadMore}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
3
src/pages/feed/FeedTabPosts.tsx
Normal file
3
src/pages/feed/FeedTabPosts.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const FeedTabPosts = () => {
|
||||
return <>WIP: Posts</>
|
||||
}
|
22
src/pages/feed/index.tsx
Normal file
22
src/pages/feed/index.tsx
Normal file
@ -0,0 +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 = () => {
|
||||
const [tab, setTab] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs tabs={['Mods', 'Blogs', 'Posts']} tab={tab} setTab={setTab} />
|
||||
|
||||
<FeedFilter tab={tab} />
|
||||
|
||||
{tab === 0 && <FeedTabMods />}
|
||||
{tab === 1 && <FeedTabBlogs />}
|
||||
{tab === 2 && <FeedTabPosts />}
|
||||
</>
|
||||
)
|
||||
}
|
101
src/pages/feed/loader.ts
Normal file
101
src/pages/feed/loader.ts
Normal file
@ -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
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
export const NotificationsPage = () => {
|
||||
return <h2>Notifications</h2>
|
||||
return <h2>WIP: Notifications</h2>
|
||||
}
|
||||
|
@ -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: <FeedPage />
|
||||
element: <FeedPage />,
|
||||
loader: feedPageLoader(context)
|
||||
},
|
||||
{
|
||||
path: appRoutes.notifications,
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user