feat(profile): blogs tab

This commit is contained in:
enes 2024-11-06 17:33:15 +01:00
parent f30ac01ea6
commit 6d6ff8ce43
4 changed files with 251 additions and 43 deletions

View File

@ -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

View File

@ -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}
/>
)}
</>
)
}

View 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
}

View File

@ -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 />,