WoT implemented, plus other fixes #160
@ -28,6 +28,8 @@ jobs:
|
||||
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
||||
echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||
cat .env
|
||||
|
||||
- name: Create Build
|
||||
|
@ -28,6 +28,8 @@ jobs:
|
||||
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
||||
echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||
cat .env
|
||||
|
||||
- name: Create Build
|
||||
|
@ -36,6 +36,8 @@ jobs:
|
||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
||||
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
||||
echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||
cat .env
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 314 KiB |
@ -1,3 +1,4 @@
|
||||
import { Dots } from 'components/Spinner'
|
||||
import { useReactions } from 'hooks'
|
||||
import { Addressable } from 'types'
|
||||
|
||||
@ -19,15 +20,13 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
|
||||
aTag: addressable.aTag
|
||||
})
|
||||
|
||||
if (!isDataLoaded) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSS_D_CRUActive' : ''
|
||||
}`}
|
||||
onClick={() => handleReaction(true)}
|
||||
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
@ -41,7 +40,9 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
|
||||
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>{likesCount}</p>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>
|
||||
{isDataLoaded ? likesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
@ -50,7 +51,7 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
|
||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSS_D_CRDActive' : ''
|
||||
}`}
|
||||
onClick={() => handleReaction()}
|
||||
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
@ -64,7 +65,9 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
|
||||
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>{disLikesCount}</p>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>
|
||||
{isDataLoaded ? disLikesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
|
9
src/components/Spinner.tsx
Normal file
9
src/components/Spinner.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import styles from '../styles/dotsSpinner.module.scss'
|
||||
|
||||
export const Spinner = () => (
|
||||
<div className='spinner'>
|
||||
<div className='spinnerCircle'></div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Dots = () => <span className={styles.loading}></span>
|
@ -1,4 +1,5 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Dots, Spinner } from 'components/Spinner'
|
||||
import { ZapPopUp } from 'components/Zap'
|
||||
import { formatDate } from 'date-fns'
|
||||
import {
|
||||
@ -59,6 +60,15 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
author: AuthorFilterEnum.All_Comments
|
||||
})
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
// Initial loading to indicate comments fetching (stop after 5 seconds)
|
||||
const t = window.setTimeout(() => setIsLoading(false), 5000)
|
||||
return () => {
|
||||
window.clearTimeout(t)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setCommentCount(commentEvents.length)
|
||||
}, [commentEvents, setCommentCount])
|
||||
@ -175,8 +185,18 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const handleDiscoveredClick = () => {
|
||||
setVisible(commentEvents)
|
||||
}
|
||||
const [visible, setVisible] = useState<CommentEvent[]>([])
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setVisible(commentEvents)
|
||||
}
|
||||
}, [commentEvents, isLoading])
|
||||
|
||||
const comments = useMemo(() => {
|
||||
let filteredComments = commentEvents
|
||||
let filteredComments = visible
|
||||
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
|
||||
filteredComments = filteredComments.filter(
|
||||
(comment) => comment.pubkey === addressable.author
|
||||
@ -190,13 +210,28 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
}
|
||||
|
||||
return filteredComments
|
||||
}, [commentEvents, filterOptions, addressable.author])
|
||||
}, [visible, filterOptions.author, filterOptions.sort, addressable.author])
|
||||
|
||||
const discoveredCount = commentEvents.length - visible.length
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsWrapper'>
|
||||
<h4 className='IBMSMSMBSSTitle'>Comments</h4>
|
||||
<div className='IBMSMSMBSSComments'>
|
||||
<CommentForm handleSubmit={handleSubmit} />
|
||||
{/* Hide comment form if aTag is missing */}
|
||||
{!!addressable.aTag && <CommentForm handleSubmit={handleSubmit} />}
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<button
|
||||
type='button'
|
||||
className='btnMain'
|
||||
onClick={discoveredCount ? handleDiscoveredClick : undefined}
|
||||
>
|
||||
<span>Load {discoveredCount} discovered comments</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Filter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
@ -355,12 +390,12 @@ const Comment = (props: CommentEvent) => {
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
<a className='IBMSMSMBSSCL_CTD_Name' href='profile.html'>
|
||||
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||
{profile?.displayName || profile?.name || ''}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CTD_Address' href='profile.html'>
|
||||
</Link>
|
||||
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||
{hexToNpub(props.pubkey)}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
@ -446,15 +481,13 @@ const Reactions = (props: Event) => {
|
||||
eTag: props.id
|
||||
})
|
||||
|
||||
if (!isDataLoaded) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||
}`}
|
||||
onClick={() => handleReaction(true)}
|
||||
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@ -466,7 +499,9 @@ const Reactions = (props: Event) => {
|
||||
>
|
||||
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>{likesCount}</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? likesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
@ -475,7 +510,7 @@ const Reactions = (props: Event) => {
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||
}`}
|
||||
onClick={() => handleReaction()}
|
||||
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@ -487,7 +522,9 @@ const Reactions = (props: Event) => {
|
||||
>
|
||||
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>{disLikesCount}</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? disLikesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
|
@ -21,7 +21,12 @@ export const LANDING_PAGE_DATA = {
|
||||
'ELDEN RING',
|
||||
'The Coffin of Andy and Leyley'
|
||||
],
|
||||
featuredBlogPosts: []
|
||||
featuredBlogPosts: [
|
||||
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c',
|
||||
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjryv3k8qenydpj94nrscmp956xgwtp94snydtz95ekgvphvfnxvvrzvyexzsvsz9y',
|
||||
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2kwjtwvahns3n0tf8j6kjxggkkz4mff499ge7xzsz',
|
||||
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2573jhg9trsu6vgav9gnn4dffkzk2ww3yrjejnc2s'
|
||||
]
|
||||
}
|
||||
// we use this object to check if a user has reacted positively or negatively to a post
|
||||
// reactions are kind 7 events and their content is either emoji icon or emoji shortcode
|
||||
|
@ -21,7 +21,8 @@ import {
|
||||
log,
|
||||
LogType,
|
||||
npubToHex,
|
||||
orderEventsChronologically
|
||||
orderEventsChronologically,
|
||||
timeout
|
||||
} from 'utils'
|
||||
|
||||
type FetchModsOptions = {
|
||||
@ -241,8 +242,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType
|
||||
): Promise<NDKEvent[]> => {
|
||||
// Find the user's relays.
|
||||
const relayUrls = await getRelayListForUser(hexKey, ndk)
|
||||
// Find the user's relays (10s timeout).
|
||||
const relayUrls = await Promise.race([
|
||||
getRelayListForUser(hexKey, ndk),
|
||||
timeout(10000)
|
||||
])
|
||||
.then((ndkRelayList) => {
|
||||
if (ndkRelayList) return ndkRelayList[userRelaysType]
|
||||
return [] // Return an empty array if ndkRelayList is undefined
|
||||
|
@ -5,7 +5,7 @@ import { Event, kinds, UnsignedEvent } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { UserRelaysType } from 'types'
|
||||
import { abbreviateNumber, log, LogType, now } from 'utils'
|
||||
import { abbreviateNumber, log, LogType, now, timeout } from 'utils'
|
||||
|
||||
type UseReactionsParams = {
|
||||
pubkey: string
|
||||
@ -32,7 +32,11 @@ export const useReactions = (params: UseReactionsParams) => {
|
||||
filter['#e'] = [params.eTag]
|
||||
}
|
||||
|
||||
fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read)
|
||||
// 1 minute timeout
|
||||
Promise.race([
|
||||
fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read),
|
||||
timeout(60000)
|
||||
])
|
||||
.then((events) => {
|
||||
setReactionEvents(events)
|
||||
})
|
||||
|
@ -381,16 +381,14 @@ const RegisterButtonWithDialog = () => {
|
||||
Browser Extensions (Windows)
|
||||
</label>
|
||||
<p className='labelDescriptionMain'>
|
||||
Once you create your "account" on any of these (
|
||||
<a
|
||||
href='https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4'
|
||||
target='blank_'
|
||||
>
|
||||
Here's a quick video guide
|
||||
</a>
|
||||
), come back and click login, then sign-in with
|
||||
extension.
|
||||
</p>
|
||||
Once you create your "account" on any of these, come back and click login, then sign-in with
|
||||
extension. Here's a quick video guide, and here's a <a
|
||||
href='https://degmods.com/blog/naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c'
|
||||
>guide post</a> to help with this process.</p>
|
||||
<div style={{ width: '100%', height: 'auto', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<video controls style={{ width: '100%' }}><source src="https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4" type="video/mp4" />
|
||||
Your browser does not support the video tag.</video>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
className='btn btnMain btnMainPopup'
|
||||
|
@ -1,17 +1,27 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useRouteError } from 'react-router-dom'
|
||||
import { appRoutes } from 'routes'
|
||||
|
||||
export const NotFoundPage = () => {
|
||||
interface NotFoundPageProps {
|
||||
title: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export const NotFoundPage = ({
|
||||
title = 'Page not found',
|
||||
message = "The page you're attempting to visit doesn't exist"
|
||||
}: Partial<NotFoundPageProps>) => {
|
||||
const error = useRouteError() as Partial<NotFoundPageProps>
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>Page not found</h2>
|
||||
<h2 className='IBMSMTitleMainHeading'>{error?.title || title}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<p>The page you're attempting to visit doesn't exist</p>
|
||||
<p>{error?.message || message}</p>
|
||||
</div>
|
||||
<div className='IBMSMAction'>
|
||||
<Link
|
||||
|
@ -94,9 +94,7 @@ export const BlogPage = () => {
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSMSplitMain'>
|
||||
{!blog ? (
|
||||
<LoadingSpinner desc={'Loading...'} />
|
||||
) : (
|
||||
{blog && (
|
||||
<>
|
||||
<div className='IBMSMSplitMainBigSide'>
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
@ -301,6 +299,7 @@ export const BlogPage = () => {
|
||||
)}
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
<Comments
|
||||
key={blog.id}
|
||||
addressable={blog as Addressable}
|
||||
setCommentCount={setCommentCount}
|
||||
/>
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { filterForEventsTaggingId, NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
import { PROFILE_BLOG_FILTER_LIMIT } from '../../constants'
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { kinds, nip19 } from 'nostr-tools'
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { appRoutes } from 'routes'
|
||||
import { store } from 'store'
|
||||
import { BlogPageLoaderResult, FilterOptions, NSFWFilter } from 'types'
|
||||
import {
|
||||
BlogPageLoaderResult,
|
||||
FilterOptions,
|
||||
ModeratedFilter,
|
||||
NSFWFilter
|
||||
} from 'types'
|
||||
import {
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
getLocalStorageItem,
|
||||
@ -16,62 +21,74 @@ import { extractBlogCardDetails, extractBlogDetails } from 'utils/blog'
|
||||
|
||||
export const blogRouteLoader =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params }: LoaderFunctionArgs) => {
|
||||
async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const { naddr } = params
|
||||
if (!naddr) {
|
||||
log(true, LogType.Error, 'Required naddr.')
|
||||
return redirect(appRoutes.blogs)
|
||||
}
|
||||
|
||||
// Decode author from naddr
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
const { pubkey } = decoded.data
|
||||
// Decode author and identifier from naddr
|
||||
let pubkey: string | undefined
|
||||
let identifier: string | undefined
|
||||
try {
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
pubkey = decoded.data.pubkey
|
||||
identifier = decoded.data.identifier
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Failed to decode naddr: ${naddr}`, error)
|
||||
throw new Error('Failed to fetch the blog. The address might be wrong')
|
||||
}
|
||||
|
||||
const userState = store.getState().user
|
||||
const loggedInUserPubkey = userState?.user?.pubkey as string | undefined
|
||||
|
||||
// Check if editing and the user is the original author
|
||||
// Redirect if NOT
|
||||
const url = new URL(request.url)
|
||||
const isEditMode = url.pathname.endsWith('/edit')
|
||||
if (isEditMode && loggedInUserPubkey !== pubkey) {
|
||||
return redirect(appRoutes.blogs)
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the filter with #a from naddr for the main blog content
|
||||
const filter = filterForEventsTaggingId(naddr)
|
||||
if (!filter) {
|
||||
log(true, LogType.Error, 'Unable to create filter from blog naddr.')
|
||||
return redirect(appRoutes.blogs)
|
||||
// Set the filter for the main blog content
|
||||
const filter = {
|
||||
kinds: [kinds.LongFormArticle],
|
||||
authors: [pubkey],
|
||||
'#d': [identifier]
|
||||
}
|
||||
// Update kinds to make sure we fetch correct event kind
|
||||
filter.kinds = [kinds.LongFormArticle]
|
||||
|
||||
const userState = store.getState().user
|
||||
|
||||
// Get the blog filter options for latest blogs
|
||||
const filterOptions = JSON.parse(
|
||||
getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
|
||||
) as FilterOptions
|
||||
|
||||
// Fetch 4 in case the current blog is included in the latest
|
||||
const latestModsFilter: NDKFilter = {
|
||||
// Fetch more in case the current blog is included in the latest and filters remove some
|
||||
const latestFilter: NDKFilter = {
|
||||
authors: [pubkey],
|
||||
kinds: [kinds.LongFormArticle],
|
||||
limit: 4
|
||||
limit: PROFILE_BLOG_FILTER_LIMIT
|
||||
}
|
||||
// Add source filter
|
||||
if (filterOptions.source === window.location.host) {
|
||||
latestModsFilter['#r'] = [filterOptions.source]
|
||||
latestFilter['#r'] = [filterOptions.source]
|
||||
}
|
||||
// Filter by NSFW tag
|
||||
// NSFWFilter.Only_NSFW -> fetch with content-warning label
|
||||
// NSFWFilter.Show_NSFW -> filter not needed
|
||||
// NSFWFilter.Only_NSFW -> true
|
||||
// NSFWFilter.Hide_NSFW -> false
|
||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
||||
latestModsFilter['#nsfw'] = [
|
||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
||||
]
|
||||
// NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
|
||||
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||
latestFilter['#L'] = ['content-warning']
|
||||
}
|
||||
|
||||
// Parallel fetch blog event, latest events, mute, and nsfw lists in parallel
|
||||
// Parallel fetch blog event, latest events, mute, and nsfw lists
|
||||
const settled = await Promise.allSettled([
|
||||
ndkContext.fetchEvent(filter),
|
||||
ndkContext.fetchEvents(latestModsFilter),
|
||||
ndkContext.getMuteLists(userState?.user?.pubkey as string),
|
||||
ndkContext.fetchEvents(latestFilter),
|
||||
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
|
||||
ndkContext.getNSFWList()
|
||||
])
|
||||
|
||||
const result: BlogPageLoaderResult = {
|
||||
blog: undefined,
|
||||
latest: [],
|
||||
@ -93,6 +110,12 @@ export const blogRouteLoader =
|
||||
)
|
||||
}
|
||||
|
||||
// Throw an error if we are missing the main blog result
|
||||
// Handle it with the react-router's errorComponent
|
||||
if (!result.blog) {
|
||||
throw new Error('We are unable to find the blog on the relays')
|
||||
}
|
||||
|
||||
// Check the lateast blog events
|
||||
const fetchEventsResult = settled[1]
|
||||
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
|
||||
@ -100,7 +123,6 @@ export const blogRouteLoader =
|
||||
result.latest = fetchEventsResult.value
|
||||
.map(extractBlogCardDetails)
|
||||
.filter((b) => b.id !== result.blog?.id) // Filter out current blog if present
|
||||
.slice(0, 3) // Take only three
|
||||
} else if (fetchEventsResult.status === 'rejected') {
|
||||
log(
|
||||
true,
|
||||
@ -110,22 +132,48 @@ export const blogRouteLoader =
|
||||
)
|
||||
}
|
||||
|
||||
const muteList = settled[2]
|
||||
if (muteList.status === 'fulfilled' && muteList.value) {
|
||||
if (muteList && muteList.value) {
|
||||
const muteLists = settled[2]
|
||||
if (muteLists.status === 'fulfilled' && muteLists.value) {
|
||||
if (muteLists && muteLists.value) {
|
||||
if (result.blog && result.blog.aTag) {
|
||||
if (
|
||||
muteList.value.admin.replaceableEvents.includes(
|
||||
muteLists.value.admin.replaceableEvents.includes(
|
||||
result.blog.aTag
|
||||
) ||
|
||||
muteList.value.user.replaceableEvents.includes(result.blog.aTag)
|
||||
muteLists.value.user.replaceableEvents.includes(result.blog.aTag)
|
||||
) {
|
||||
result.isBlocked = true
|
||||
}
|
||||
}
|
||||
|
||||
// Moderate the latest
|
||||
const isAdmin =
|
||||
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isOwner =
|
||||
userState.user?.pubkey && userState.user.pubkey === pubkey
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
// Allow "Unmoderated Fully" when author visits own profile
|
||||
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
|
||||
result.latest = result.latest.filter(
|
||||
(b) =>
|
||||
!muteLists.value.admin.authors.includes(b.author!) &&
|
||||
!muteLists.value.admin.replaceableEvents.includes(b.aTag!)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.moderated === ModeratedFilter.Moderated) {
|
||||
result.latest = result.latest.filter(
|
||||
(b) =>
|
||||
!muteLists.value.user.authors.includes(b.author!) &&
|
||||
!muteLists.value.user.replaceableEvents.includes(b.aTag!)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (muteList.status === 'rejected') {
|
||||
log(true, LogType.Error, 'Issue fetching mute list', muteList.reason)
|
||||
} else if (muteLists.status === 'rejected') {
|
||||
log(true, LogType.Error, 'Issue fetching mute list', muteLists.reason)
|
||||
}
|
||||
|
||||
const nsfwList = settled[3]
|
||||
@ -147,15 +195,14 @@ export const blogRouteLoader =
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the the latest blogs too
|
||||
// Check the latest blogs too
|
||||
result.latest = result.latest.map((b) => {
|
||||
if (b) {
|
||||
const isMissingNsfwTag =
|
||||
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
|
||||
// Add nsfw tag if it's missing
|
||||
const isMissingNsfwTag =
|
||||
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
|
||||
|
||||
if (isMissingNsfwTag) {
|
||||
b.nsfw = true
|
||||
}
|
||||
if (isMissingNsfwTag) {
|
||||
b.nsfw = true
|
||||
}
|
||||
return b
|
||||
})
|
||||
@ -163,15 +210,24 @@ export const blogRouteLoader =
|
||||
log(true, LogType.Error, 'Issue fetching nsfw list', nsfwList.reason)
|
||||
}
|
||||
|
||||
// Filter latest, sort and take only three
|
||||
result.latest = result.latest
|
||||
.filter(
|
||||
// Filter out the NSFW if selected
|
||||
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
a.published_at && b.published_at ? b.published_at - a.published_at : 0
|
||||
)
|
||||
.slice(0, 3)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'An error occurred in fetching blog details from relays',
|
||||
error
|
||||
)
|
||||
toast.error('An error occurred in fetching blog details from relays')
|
||||
return redirect(appRoutes.blogs)
|
||||
let message = 'An error occurred in fetching blog details from relays'
|
||||
log(true, LogType.Error, message, error)
|
||||
if (error instanceof Error) {
|
||||
message = error.message
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useLoaderData, useSearchParams } from 'react-router-dom'
|
||||
import { useLoaderData, useNavigation, useSearchParams } from 'react-router-dom'
|
||||
import { useLocalStorage } from 'hooks'
|
||||
import { BlogCardDetails, NSFWFilter, SortBy } from 'types'
|
||||
import { SearchInput } from '../../components/SearchInput'
|
||||
@ -10,8 +10,10 @@ import '../../styles/search.css'
|
||||
import '../../styles/styles.css'
|
||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||
import { scrollIntoView } from 'utils'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
|
||||
export const BlogsPage = () => {
|
||||
const navigation = useNavigation()
|
||||
const blogs = useLoaderData() as Partial<BlogCardDetails>[] | undefined
|
||||
const [filterOptions, setFilterOptions] = useLocalStorage(
|
||||
'filter-blog-curated',
|
||||
@ -105,6 +107,7 @@ export const BlogsPage = () => {
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
{navigation.state !== 'idle' && <LoadingSpinner desc={'Loading'} />}
|
||||
<div className='ContainerMain'>
|
||||
<div
|
||||
className='IBMSecMainGroup IBMSecMainGroupAlt'
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { BlogCardDetails } from 'types'
|
||||
import { log, LogType, npubToHex } from 'utils'
|
||||
import { extractBlogCardDetails } from 'utils/blog'
|
||||
|
||||
@ -15,14 +16,46 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
|
||||
authors: blogHexkeys,
|
||||
kinds: [kinds.LongFormArticle]
|
||||
}
|
||||
const events = await ndkContext.fetchEvents(filter)
|
||||
|
||||
if (!events) {
|
||||
log(true, LogType.Error, 'Unable to fetch the blog events.')
|
||||
return null
|
||||
const settled = await Promise.allSettled([
|
||||
ndkContext.fetchEvents(filter),
|
||||
ndkContext.getMuteLists()
|
||||
])
|
||||
|
||||
let blogs: Partial<BlogCardDetails>[] = []
|
||||
const fetchEventsResult = settled[0]
|
||||
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
|
||||
// Extract the blog card details from the events
|
||||
blogs = fetchEventsResult.value
|
||||
.map(extractBlogCardDetails)
|
||||
.filter((b) => b.naddr)
|
||||
} else if (fetchEventsResult.status === 'rejected') {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'Unable to fetch the blog events.',
|
||||
fetchEventsResult.reason
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
return events.map(extractBlogCardDetails).filter((e) => e.naddr)
|
||||
const muteListResult = settled[1]
|
||||
if (muteListResult.status === 'fulfilled' && muteListResult.value) {
|
||||
// Filter out the blocked events
|
||||
blogs = blogs.filter(
|
||||
(b) =>
|
||||
b.aTag &&
|
||||
!muteListResult.value.admin.replaceableEvents.includes(b.aTag)
|
||||
)
|
||||
} else if (muteListResult.status === 'rejected') {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'Failed to fetch mutelists.',
|
||||
muteListResult.reason
|
||||
)
|
||||
}
|
||||
return blogs
|
||||
} catch (error) {
|
||||
log(
|
||||
true,
|
||||
@ -30,6 +63,6 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
|
||||
'An error occurred in fetching blog details from relays',
|
||||
error
|
||||
)
|
||||
return null
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { kinds, nip19 } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Link, useNavigate, useNavigation } from 'react-router-dom'
|
||||
import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import { BlogCard } from '../components/BlogCard'
|
||||
import { GameCard } from '../components/GameCard'
|
||||
import { ModCard } from '../components/ModCard'
|
||||
import { LANDING_PAGE_DATA } from '../constants'
|
||||
import { LANDING_PAGE_DATA, PROFILE_BLOG_FILTER_LIMIT } from '../constants'
|
||||
import {
|
||||
useAppSelector,
|
||||
useDidMount,
|
||||
@ -32,14 +32,12 @@ import '../styles/SimpleSlider.css'
|
||||
import '../styles/styles.css'
|
||||
|
||||
// Import Swiper styles
|
||||
import {
|
||||
filterForEventsTaggingId,
|
||||
NDKEvent,
|
||||
NDKFilter
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
import 'swiper/css'
|
||||
import 'swiper/css/navigation'
|
||||
import 'swiper/css/pagination'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { Spinner } from 'components/Spinner'
|
||||
|
||||
export const HomePage = () => {
|
||||
const navigate = useNavigate()
|
||||
@ -260,6 +258,8 @@ const DisplayLatestMods = () => {
|
||||
useDidMount(() => {
|
||||
fetchMods({ source: window.location.host })
|
||||
.then((mods) => {
|
||||
// Sort by the latest (published_at descending)
|
||||
mods.sort((a, b) => b.published_at - a.published_at)
|
||||
const wotFilteredMods = mods.filter(
|
||||
(mod) => siteWot.includes(mod.author) || userWot.includes(mod.author)
|
||||
)
|
||||
@ -316,14 +316,6 @@ const DisplayLatestMods = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const Spinner = () => {
|
||||
return (
|
||||
<div className='spinner'>
|
||||
<div className='spinnerCircle'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DisplayLatestBlogs = () => {
|
||||
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>()
|
||||
const { fetchEvents } = useNDKContext()
|
||||
@ -331,86 +323,92 @@ const DisplayLatestBlogs = () => {
|
||||
sort: SortBy.Latest,
|
||||
nsfw: NSFWFilter.Hide_NSFW
|
||||
})
|
||||
const navigation = useNavigation()
|
||||
useDidMount(() => {
|
||||
const fetchBlogs = async () => {
|
||||
try {
|
||||
// Show maximum of 4 blog posts
|
||||
// 2 should be featured and the most recent 2 from blog npubs
|
||||
// Populate the filter from known naddr (constants.ts)
|
||||
const filters: NDKFilter[] = []
|
||||
const filter: NDKFilter = {
|
||||
kinds: [kinds.LongFormArticle],
|
||||
authors: [],
|
||||
'#d': []
|
||||
}
|
||||
for (let i = 0; i < LANDING_PAGE_DATA.featuredBlogPosts.length; i++) {
|
||||
try {
|
||||
const naddr = LANDING_PAGE_DATA.featuredBlogPosts[i]
|
||||
const filterId = filterForEventsTaggingId(naddr)
|
||||
if (filterId) {
|
||||
filters.push(filterId)
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
const { pubkey, identifier } = decoded.data
|
||||
if (!filter.authors?.includes(pubkey)) {
|
||||
filter.authors?.push(pubkey)
|
||||
}
|
||||
if (!filter.authors?.includes(identifier)) {
|
||||
filter['#d']?.push(identifier)
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore
|
||||
}
|
||||
}
|
||||
// Create a single filter based on multiple #a's
|
||||
const filter = filters.reduce(
|
||||
(filter, id) => {
|
||||
const a = id['#a']
|
||||
if (a) {
|
||||
filter['#a']?.push(a[0])
|
||||
}
|
||||
return filter
|
||||
},
|
||||
{
|
||||
'#a': []
|
||||
} as NDKFilter
|
||||
)
|
||||
|
||||
// Prepare filter for the latest
|
||||
const blogNpubs = import.meta.env.VITE_BLOG_NPUBS.split(',')
|
||||
const blogHexkeys = blogNpubs
|
||||
.map(npubToHex)
|
||||
.filter((hexkey) => hexkey !== null)
|
||||
|
||||
// We fetch 4 posts in case of duplicates (from featured)
|
||||
// We fetch more posts in case of duplicates (from featured)
|
||||
const latestFilter: NDKFilter = {
|
||||
authors: blogHexkeys,
|
||||
kinds: [kinds.LongFormArticle],
|
||||
limit: 4
|
||||
limit: PROFILE_BLOG_FILTER_LIMIT
|
||||
}
|
||||
|
||||
// Filter by NSFW tag
|
||||
// NSFWFilter.Show_NSFW -> filter not needed
|
||||
// NSFWFilter.Only_NSFW -> true
|
||||
// NSFWFilter.Hide_NSFW -> false
|
||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
||||
latestFilter['#nsfw'] = [
|
||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
||||
]
|
||||
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||
latestFilter['#L'] = ['content-warning']
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
fetchEvents({ ...filter, kinds: [kinds.LongFormArticle] }),
|
||||
fetchEvents(filter),
|
||||
fetchEvents(latestFilter)
|
||||
])
|
||||
|
||||
const events: NDKEvent[] = []
|
||||
const events: Partial<BlogCardDetails>[] = []
|
||||
// Get featured blogs posts result
|
||||
results.forEach((r) => {
|
||||
// Add events from both promises to the array
|
||||
if (r.status === 'fulfilled' && r.value) {
|
||||
events.push(...r.value)
|
||||
events.push(
|
||||
...r.value
|
||||
.map(extractBlogCardDetails) // Extract the blog card details
|
||||
.sort(
|
||||
// Sort each result by published_at in descending order
|
||||
// We can't sort everything at once we'd lose prefered
|
||||
(a, b) =>
|
||||
a.published_at && b.published_at
|
||||
? b.published_at - a.published_at
|
||||
: 0
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove duplicates
|
||||
const unique = Array.from(
|
||||
events
|
||||
.filter((b) => b.id)
|
||||
.reduce((map, obj) => {
|
||||
map.set(obj.id, obj)
|
||||
map.set(obj.id!, obj)
|
||||
return map
|
||||
}, new Map())
|
||||
}, new Map<string, Partial<BlogCardDetails>>())
|
||||
.values()
|
||||
).filter(
|
||||
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
|
||||
)
|
||||
const latest = unique.slice(0, 4)
|
||||
|
||||
setBlogs(latest.map(extractBlogCardDetails))
|
||||
const latest = unique.slice(0, 4)
|
||||
setBlogs(latest)
|
||||
} catch (error) {
|
||||
log(
|
||||
true,
|
||||
@ -427,6 +425,9 @@ const DisplayLatestBlogs = () => {
|
||||
|
||||
return (
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
{navigation.state !== 'idle' && (
|
||||
<LoadingSpinner desc={'Fetching blog...'} />
|
||||
)}
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>Blog Posts</h2>
|
||||
</div>
|
||||
|
@ -14,11 +14,11 @@ import {
|
||||
useNDKContext,
|
||||
useNSFWList
|
||||
} from 'hooks'
|
||||
import { kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import { kinds, UnsignedEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams, Navigate, Link, useLoaderData } from 'react-router-dom'
|
||||
import { Link, useLoaderData, useNavigation } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
import { appRoutes } from 'routes'
|
||||
import {
|
||||
BlogCardDetails,
|
||||
FilterOptions,
|
||||
@ -32,8 +32,6 @@ import {
|
||||
copyTextToClipboard,
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
extractBlogCardDetails,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
npubToHex,
|
||||
scrollIntoView,
|
||||
@ -46,23 +44,11 @@ import { BlogCard } from 'components/BlogCard'
|
||||
|
||||
export const ProfilePage = () => {
|
||||
const {
|
||||
profilePubkey,
|
||||
profile,
|
||||
isBlocked: _isBlocked,
|
||||
isOwnProfile
|
||||
} = useLoaderData() as ProfilePageLoaderResult
|
||||
// Try to decode nprofile parameter
|
||||
const { nprofile } = useParams()
|
||||
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)
|
||||
}
|
||||
|
||||
const scrollTargetRef = useRef<HTMLDivElement>(null)
|
||||
const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext()
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
@ -262,6 +248,7 @@ export const ProfilePage = () => {
|
||||
setIsLoading(true)
|
||||
switch (tab) {
|
||||
case 0:
|
||||
setLoadingSpinnerDesc('Fetching mods..')
|
||||
fetchMods({ source: filterOptions.source, author: profilePubkey })
|
||||
.then((res) => {
|
||||
setMods(res)
|
||||
@ -285,22 +272,6 @@ export const ProfilePage = () => {
|
||||
profilePubkey
|
||||
)
|
||||
|
||||
// Redirect route
|
||||
let profileRoute = appRoutes.home
|
||||
if (!nprofile && userState.auth && userState.user) {
|
||||
// Redirect to user's profile is no profile is linked
|
||||
const userHexKey = npubToHex(userState.user.npub as string)
|
||||
|
||||
if (userHexKey) {
|
||||
profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: userHexKey
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!profilePubkey) return <Navigate to={profileRoute} replace={true} />
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
@ -690,6 +661,7 @@ const ReportUserPopup = ({
|
||||
const ProfileTabBlogs = () => {
|
||||
const { profile, muteLists, nsfwList } =
|
||||
useLoaderData() as ProfilePageLoaderResult
|
||||
const navigation = useNavigation()
|
||||
const { fetchEvents } = useNDKContext()
|
||||
const [filterOptions] = useLocalStorage('filter-blog', DEFAULT_FILTER_OPTIONS)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -704,10 +676,8 @@ const ProfileTabBlogs = () => {
|
||||
filter['#r'] = [host]
|
||||
}
|
||||
|
||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
||||
filter['#nsfw'] = [
|
||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
||||
]
|
||||
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||
filter['#L'] = ['content-warning']
|
||||
}
|
||||
|
||||
return filter
|
||||
@ -725,7 +695,7 @@ const ProfileTabBlogs = () => {
|
||||
}
|
||||
fetchEvents(filter)
|
||||
.then((events) => {
|
||||
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
|
||||
setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
|
||||
setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT)
|
||||
})
|
||||
.finally(() => {
|
||||
@ -752,7 +722,7 @@ const ProfileTabBlogs = () => {
|
||||
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
|
||||
setPage((prev) => prev + 1)
|
||||
setBlogs(
|
||||
nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((e) => e.naddr)
|
||||
nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((b) => b.naddr)
|
||||
)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
@ -775,7 +745,7 @@ const ProfileTabBlogs = () => {
|
||||
.then((events) => {
|
||||
setHasMore(true)
|
||||
setPage((prev) => prev - 1)
|
||||
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
|
||||
setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
@ -799,6 +769,11 @@ const ProfileTabBlogs = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Filter nsfw (Hide_NSFW option)
|
||||
_blogs = _blogs.filter(
|
||||
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
|
||||
)
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
// Allow "Unmoderated Fully" when author visits own profile
|
||||
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
|
||||
@ -845,7 +820,9 @@ const ProfileTabBlogs = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={'Fetching blogs...'} />}
|
||||
{(isLoading || navigation.state !== 'idle') && (
|
||||
<LoadingSpinner desc={'Loading...'} />
|
||||
)}
|
||||
|
||||
<ModFilter filterKey={'filter-blog'} author={profile?.pubkey as string} />
|
||||
|
||||
|
@ -4,9 +4,10 @@ import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
import { store } from 'store'
|
||||
import { MuteLists, UserProfile } from 'types'
|
||||
import { log, LogType } from 'utils'
|
||||
import { log, LogType, npubToHex } from 'utils'
|
||||
|
||||
export interface ProfilePageLoaderResult {
|
||||
profilePubkey: string
|
||||
profile: UserProfile
|
||||
isBlocked: boolean
|
||||
isOwnProfile: boolean
|
||||
@ -24,10 +25,25 @@ export const profileRouteLoader =
|
||||
const { nprofile } = params
|
||||
let profilePubkey: string | undefined
|
||||
try {
|
||||
const value = nprofile
|
||||
? nip19.decode(nprofile as `nprofile1${string}`)
|
||||
: undefined
|
||||
profilePubkey = value?.data.pubkey
|
||||
// Decode if it starts with nprofile1
|
||||
if (nprofile?.startsWith('nprofile1')) {
|
||||
const value = nprofile
|
||||
? nip19.decode(nprofile as `nprofile1${string}`)
|
||||
: undefined
|
||||
profilePubkey = value?.data.pubkey
|
||||
} else if (nprofile?.startsWith('npub1')) {
|
||||
// Try to get hex from the npub and encode it to nprofile
|
||||
const value = npubToHex(nprofile)
|
||||
if (value) {
|
||||
return redirect(
|
||||
getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: value
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore and redirect to home or logged in user
|
||||
log(true, LogType.Error, 'Failed to decode nprofile.', error)
|
||||
@ -57,6 +73,7 @@ export const profileRouteLoader =
|
||||
|
||||
// Empty result
|
||||
const result: ProfilePageLoaderResult = {
|
||||
profilePubkey: profilePubkey,
|
||||
profile: {},
|
||||
isBlocked: false,
|
||||
isOwnProfile: false,
|
||||
|
@ -84,22 +84,27 @@ export const writeRouteAction =
|
||||
.split(',')
|
||||
.map((t) => ['t', t])
|
||||
|
||||
const tags = [
|
||||
['d', uuid],
|
||||
['a', aTag],
|
||||
['r', rTag],
|
||||
['published_at', published_at.toString()],
|
||||
['title', formSubmit.title!],
|
||||
['image', formSubmit.image!],
|
||||
['summary', formSubmit.summary!],
|
||||
...tTags
|
||||
]
|
||||
|
||||
// Add NSFW tag, L label namespace standardized tag
|
||||
// https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
|
||||
if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning'])
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: kinds.LongFormArticle,
|
||||
created_at: currentTimeStamp,
|
||||
pubkey: hexPubkey,
|
||||
content: content,
|
||||
tags: [
|
||||
['d', uuid],
|
||||
['a', aTag],
|
||||
['r', rTag],
|
||||
['published_at', published_at.toString()],
|
||||
['title', formSubmit.title!],
|
||||
['image', formSubmit.image!],
|
||||
['summary', formSubmit.summary!],
|
||||
['nsfw', (formSubmit.nsfw === 'on').toString()],
|
||||
...tTags
|
||||
]
|
||||
tags: tags
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -103,13 +103,15 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
path: appRoutes.blog,
|
||||
element: <BlogPage />,
|
||||
loader: blogRouteLoader(context),
|
||||
action: blogRouteAction(context)
|
||||
action: blogRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
},
|
||||
{
|
||||
path: appRoutes.blogEdit,
|
||||
element: <WritePage key='edit' />,
|
||||
loader: blogRouteLoader(context),
|
||||
action: writeRouteAction(context)
|
||||
action: writeRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
},
|
||||
{
|
||||
path: appRoutes.blogReport_actionOnly,
|
||||
|
18
src/styles/dotsSpinner.module.scss
Normal file
18
src/styles/dotsSpinner.module.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.loading::after {
|
||||
content: '.';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%,
|
||||
20% {
|
||||
content: '.\00a0\00a0';
|
||||
}
|
||||
40% {
|
||||
content: '..\00a0';
|
||||
}
|
||||
60%,
|
||||
100% {
|
||||
content: '...';
|
||||
}
|
||||
}
|
@ -3,22 +3,36 @@ import { BlogCardDetails, BlogDetails } from 'types'
|
||||
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
|
||||
import { kinds, nip19 } from 'nostr-tools'
|
||||
|
||||
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => ({
|
||||
title: getFirstTagValue(event, 'title'),
|
||||
content: event.content,
|
||||
summary: getFirstTagValue(event, 'summary'),
|
||||
image: getFirstTagValue(event, 'image'),
|
||||
nsfw: getFirstTagValue(event, 'nsfw') === 'true',
|
||||
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => {
|
||||
const dTag = getFirstTagValue(event, 'd')
|
||||
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
published_at: getFirstTagValueAsInt(event, 'published_at'),
|
||||
edited_at: event.created_at,
|
||||
rTag: getFirstTagValue(event, 'r') || 'N/A',
|
||||
dTag: getFirstTagValue(event, 'd'),
|
||||
aTag: getFirstTagValue(event, 'a'),
|
||||
tTags: getTagValues(event, 't') || []
|
||||
})
|
||||
// Check if the aTag exists on the blog
|
||||
let aTag = getFirstTagValue(event, 'a')
|
||||
|
||||
// Create aTag from components if aTag is not included
|
||||
if (typeof aTag === 'undefined' && event.pubkey && dTag) {
|
||||
aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}`
|
||||
}
|
||||
|
||||
return {
|
||||
title: getFirstTagValue(event, 'title'),
|
||||
content: event.content,
|
||||
summary: getFirstTagValue(event, 'summary'),
|
||||
image: getFirstTagValue(event, 'image'),
|
||||
// Check L label namespace for content warning or nsfw (backwards compatibility)
|
||||
nsfw:
|
||||
getFirstTagValue(event, 'L') === 'content-warning' ||
|
||||
getFirstTagValue(event, 'nsfw') === 'true',
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
published_at: getFirstTagValueAsInt(event, 'published_at'),
|
||||
edited_at: event.created_at,
|
||||
rTag: getFirstTagValue(event, 'r') || 'N/A',
|
||||
dTag: dTag,
|
||||
aTag: aTag,
|
||||
tTags: getTagValues(event, 't') || []
|
||||
}
|
||||
}
|
||||
|
||||
export const extractBlogCardDetails = (
|
||||
event: NDKEvent
|
||||
|
Loading…
Reference in New Issue
Block a user