feat(profile): blogs tab
This commit is contained in:
parent
f30ac01ea6
commit
6d6ff8ce43
@ -113,7 +113,8 @@ export const REACTIONS = {
|
||||
|
||||
export const MAX_MODS_PER_PAGE = 10
|
||||
export const MAX_GAMES_PER_PAGE = 10
|
||||
|
||||
// todo: add game and mod fallback image here
|
||||
export const FALLBACK_PROFILE_IMAGE =
|
||||
'https://image.nostr.build/a305f4b43f74af3c6dcda42e6a798105a56ac1e3e7b74d7bef171896b3ba7520.png'
|
||||
|
||||
export const PROFILE_BLOG_FILTER_LIMIT = 20
|
||||
|
@ -5,7 +5,7 @@ import { ModFilter } from 'components/ModsFilter'
|
||||
import { Pagination } from 'components/Pagination'
|
||||
import { ProfileSection } from 'components/ProfileSection'
|
||||
import { Tabs } from 'components/Tabs'
|
||||
import { MOD_FILTER_LIMIT } from '../constants'
|
||||
import { MOD_FILTER_LIMIT, PROFILE_BLOG_FILTER_LIMIT } from '../../constants'
|
||||
import {
|
||||
useAppSelector,
|
||||
useFilteredMods,
|
||||
@ -14,15 +14,23 @@ import {
|
||||
useNDKContext,
|
||||
useNSFWList
|
||||
} from 'hooks'
|
||||
import { nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useParams, Navigate, Link } from 'react-router-dom'
|
||||
import { kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams, Navigate, Link, useLoaderData } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
import { FilterOptions, ModDetails, UserRelaysType } from 'types'
|
||||
import {
|
||||
BlogCardDetails,
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
NSFWFilter,
|
||||
SortBy,
|
||||
UserRelaysType
|
||||
} from 'types'
|
||||
import {
|
||||
copyTextToClipboard,
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
extractBlogCardDetails,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
@ -32,9 +40,15 @@ import {
|
||||
signAndPublish
|
||||
} from 'utils'
|
||||
import { CheckboxField } from 'components/Inputs'
|
||||
import { useProfile } from 'hooks/useProfile'
|
||||
import { ProfilePageLoaderResult } from './loader'
|
||||
import { BlogCard } from 'components/BlogCard'
|
||||
|
||||
export const ProfilePage = () => {
|
||||
const {
|
||||
profile,
|
||||
isBlocked: _isBlocked,
|
||||
isOwnProfile
|
||||
} = useLoaderData() as ProfilePageLoaderResult
|
||||
// Try to decode nprofile parameter
|
||||
const { nprofile } = useParams()
|
||||
let profilePubkey: string | undefined
|
||||
@ -51,46 +65,14 @@ export const ProfilePage = () => {
|
||||
const scrollTargetRef = useRef<HTMLDivElement>(null)
|
||||
const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext()
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const isOwnProfile =
|
||||
userState.auth && userState.user?.pubkey === profilePubkey
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const profile = useProfile(profilePubkey)
|
||||
|
||||
const displayName =
|
||||
profile?.displayName || profile?.name || '[name not set up]'
|
||||
const [showReportPopUp, setShowReportPopUp] = useState(false)
|
||||
|
||||
const [isBlocked, setIsBlocked] = useState(false)
|
||||
useEffect(() => {
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
const userHexKey = userState.user.pubkey as string
|
||||
|
||||
const muteListFilter: NDKFilter = {
|
||||
kinds: [NDKKind.MuteList],
|
||||
authors: [userHexKey]
|
||||
}
|
||||
|
||||
fetchEventFromUserRelays(
|
||||
muteListFilter,
|
||||
userHexKey,
|
||||
UserRelaysType.Write
|
||||
).then((event) => {
|
||||
if (event) {
|
||||
// get a list of tags
|
||||
const tags = event.tags
|
||||
const blocked =
|
||||
tags.findIndex(
|
||||
(item) => item[0] === 'p' && item[1] === profilePubkey
|
||||
) !== -1
|
||||
|
||||
setIsBlocked(blocked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [userState, profilePubkey, fetchEventFromUserRelays])
|
||||
|
||||
const [isBlocked, setIsBlocked] = useState(_isBlocked)
|
||||
const handleBlock = async () => {
|
||||
if (!profilePubkey) {
|
||||
toast.error(`Something went wrong. Unable to find reported user's pubkey`)
|
||||
@ -482,7 +464,7 @@ export const ProfilePage = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 1 && <>WIP</>}
|
||||
{tab === 1 && <ProfileTabBlogs />}
|
||||
{tab === 2 && <>WIP</>}
|
||||
</div>
|
||||
</div>
|
||||
@ -703,3 +685,135 @@ const ReportUserPopup = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ProfileTabBlogs = () => {
|
||||
const { profile } = useLoaderData() as ProfilePageLoaderResult
|
||||
const { fetchEvents } = useNDKContext()
|
||||
const [filterOptions] = useLocalStorage('filter-blog', DEFAULT_FILTER_OPTIONS)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const blogfilter: NDKFilter = useMemo(() => {
|
||||
const filter: NDKFilter = {
|
||||
authors: [profile?.pubkey as string],
|
||||
kinds: [kinds.LongFormArticle]
|
||||
}
|
||||
|
||||
const host = window.location.host
|
||||
if (filterOptions.source === host) {
|
||||
filter['#r'] = [host]
|
||||
}
|
||||
|
||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
||||
filter['#nsfw'] = [
|
||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
||||
]
|
||||
}
|
||||
|
||||
return filter
|
||||
}, [filterOptions.nsfw, filterOptions.source, profile?.pubkey])
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>([])
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
// Initial blog fetch, go beyond limit to check for next
|
||||
const filter: NDKFilter = {
|
||||
...blogfilter,
|
||||
limit: PROFILE_BLOG_FILTER_LIMIT + 1
|
||||
}
|
||||
fetchEvents(filter)
|
||||
.then((events) => {
|
||||
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
|
||||
setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
}, [blogfilter, fetchEvents, profile])
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (isLoading) return
|
||||
|
||||
const last = blogs.length > 0 ? blogs[blogs.length - 1] : undefined
|
||||
if (last?.published_at) {
|
||||
const until = last?.published_at - 1
|
||||
const nextFilter = {
|
||||
...blogfilter,
|
||||
limit: PROFILE_BLOG_FILTER_LIMIT + 1,
|
||||
until
|
||||
}
|
||||
setIsLoading(true)
|
||||
fetchEvents(nextFilter)
|
||||
.then((events) => {
|
||||
const nextBlogs = events.map(extractBlogCardDetails)
|
||||
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
|
||||
setPage((prev) => prev + 1)
|
||||
setBlogs(
|
||||
nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((e) => e.naddr)
|
||||
)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
}, [blogfilter, blogs, fetchEvents, isLoading])
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
if (isLoading) return
|
||||
|
||||
const first = blogs.length > 0 ? blogs[0] : undefined
|
||||
if (first?.published_at) {
|
||||
const since = first.published_at + 1
|
||||
const prevFilter = {
|
||||
...blogfilter,
|
||||
limit: PROFILE_BLOG_FILTER_LIMIT,
|
||||
since
|
||||
}
|
||||
setIsLoading(true)
|
||||
fetchEvents(prevFilter)
|
||||
.then((events) => {
|
||||
setHasMore(true)
|
||||
setPage((prev) => prev - 1)
|
||||
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
}, [blogfilter, blogs, fetchEvents, isLoading])
|
||||
|
||||
const sortedBlogs = useMemo(() => {
|
||||
const sorted = blogs || []
|
||||
if (filterOptions.sort === SortBy.Latest) {
|
||||
sorted.sort((a, b) =>
|
||||
a.published_at && b.published_at ? b.published_at - a.published_at : 0
|
||||
)
|
||||
} else if (filterOptions.sort === SortBy.Oldest) {
|
||||
sorted.sort((a, b) =>
|
||||
a.published_at && b.published_at ? a.published_at - b.published_at : 0
|
||||
)
|
||||
}
|
||||
|
||||
return sorted
|
||||
}, [blogs, filterOptions.sort])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={'Fetching blogs...'} />}
|
||||
|
||||
<ModFilter filterKey={'filter-blog'} author={profile?.pubkey as string} />
|
||||
|
||||
<div className='IBMSMList IBMSMListAlt'>
|
||||
{sortedBlogs.map((b) => (
|
||||
<BlogCard key={b.id} {...b} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!(page === 1 && !hasMore) && (
|
||||
<Pagination
|
||||
page={page}
|
||||
disabledNext={!hasMore}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
91
src/pages/profile/loader.ts
Normal file
91
src/pages/profile/loader.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
import { store } from 'store'
|
||||
import { UserProfile, UserRelaysType } from 'types'
|
||||
import { log, LogType } from 'utils'
|
||||
|
||||
export interface ProfilePageLoaderResult {
|
||||
profile: UserProfile
|
||||
isBlocked: boolean
|
||||
isOwnProfile: boolean
|
||||
}
|
||||
|
||||
export const profileRouteLoader =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params }: LoaderFunctionArgs) => {
|
||||
let profileRoute = appRoutes.home
|
||||
|
||||
// Try to decode nprofile parameter
|
||||
const { nprofile } = params
|
||||
let profilePubkey: string | undefined
|
||||
try {
|
||||
const value = nprofile
|
||||
? nip19.decode(nprofile as `nprofile1${string}`)
|
||||
: undefined
|
||||
profilePubkey = value?.data.pubkey
|
||||
} catch (error) {
|
||||
// Silently ignore and redirect to home or logged in user
|
||||
log(true, LogType.Error, 'Failed to decode nprofile.', error)
|
||||
}
|
||||
|
||||
// Get the current state
|
||||
const userState = store.getState().user
|
||||
|
||||
// Redirect route
|
||||
// Redirect home if user is not logged in or profile naddr is missing
|
||||
if (!profilePubkey && userState.auth && userState.user?.pubkey) {
|
||||
// Redirect to user's profile is no profile is linked
|
||||
const userHexKey = userState.user.pubkey as string
|
||||
|
||||
if (userHexKey) {
|
||||
profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: userHexKey
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!profilePubkey) return redirect(profileRoute)
|
||||
|
||||
const result: ProfilePageLoaderResult = {
|
||||
profile: {},
|
||||
isBlocked: false,
|
||||
isOwnProfile: false
|
||||
}
|
||||
|
||||
result.profile = await ndkContext.findMetadata(profilePubkey)
|
||||
|
||||
// Check if user the user is logged in
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
result.isOwnProfile = userState.user.pubkey === profilePubkey
|
||||
|
||||
const userHexKey = userState.user.pubkey as string
|
||||
|
||||
// Check if user has blocked this profile
|
||||
const muteListFilter: NDKFilter = {
|
||||
kinds: [NDKKind.MuteList],
|
||||
authors: [userHexKey]
|
||||
}
|
||||
const muteList = await ndkContext.fetchEventFromUserRelays(
|
||||
muteListFilter,
|
||||
userHexKey,
|
||||
UserRelaysType.Write
|
||||
)
|
||||
if (muteList) {
|
||||
// get a list of tags
|
||||
const tags = muteList.tags
|
||||
const blocked =
|
||||
tags.findIndex(
|
||||
(item) => item[0] === 'p' && item[1] === profilePubkey
|
||||
) !== -1
|
||||
|
||||
result.isBlocked = blocked
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -8,6 +8,7 @@ import { HomePage } from '../pages/home'
|
||||
import { ModPage } from '../pages/mod'
|
||||
import { ModsPage } from '../pages/mods'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { profileRouteLoader } from 'pages/profile/loader'
|
||||
import { SettingsPage } from '../pages/settings'
|
||||
import { SubmitModPage } from '../pages/submitMod'
|
||||
import { GamePage } from '../pages/game'
|
||||
@ -18,9 +19,9 @@ import { NotificationsPage } from '../pages/notifications'
|
||||
import { WritePage } from '../pages/write'
|
||||
import { writeRouteAction } from '../pages/write/action'
|
||||
import { BlogsPage } from 'pages/blogs'
|
||||
import { blogsRouteLoader } from 'pages/blogs/loader'
|
||||
import { BlogPage } from 'pages/blog'
|
||||
import { blogRouteLoader } from 'pages/blog/loader'
|
||||
import { blogsRouteLoader } from 'pages/blogs/loader'
|
||||
|
||||
export const appRoutes = {
|
||||
index: '/',
|
||||
@ -134,7 +135,8 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
},
|
||||
{
|
||||
path: appRoutes.profile,
|
||||
element: <ProfilePage />
|
||||
element: <ProfilePage />,
|
||||
loader: profileRouteLoader(context)
|
||||
},
|
||||
{
|
||||
element: <FeedLayout />,
|
||||
|
Loading…
Reference in New Issue
Block a user