Merge branch 'staging' into wot
This commit is contained in:
commit
940f400300
@ -28,6 +28,8 @@ jobs:
|
|||||||
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
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_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_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
|
cat .env
|
||||||
|
|
||||||
- name: Create Build
|
- name: Create Build
|
||||||
|
@ -28,6 +28,8 @@ jobs:
|
|||||||
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
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_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_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
|
cat .env
|
||||||
|
|
||||||
- name: Create Build
|
- name: Create Build
|
||||||
|
@ -36,6 +36,8 @@ jobs:
|
|||||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .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
|
cat .env
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 314 KiB |
@ -1,3 +1,4 @@
|
|||||||
|
import { Dots } from 'components/Spinner'
|
||||||
import { useReactions } from 'hooks'
|
import { useReactions } from 'hooks'
|
||||||
import { Addressable } from 'types'
|
import { Addressable } from 'types'
|
||||||
|
|
||||||
@ -19,15 +20,13 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
|
|||||||
aTag: addressable.aTag
|
aTag: addressable.aTag
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isDataLoaded) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp ${
|
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp ${
|
||||||
hasReactedPositively ? 'IBMSMSMBSS_D_CRUActive' : ''
|
hasReactedPositively ? 'IBMSMSMBSS_D_CRUActive' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleReaction(true)}
|
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||||
>
|
>
|
||||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||||
<svg
|
<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>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className='IBMSMSMBSS_Details_CardText'>{likesCount}</p>
|
<p className='IBMSMSMBSS_Details_CardText'>
|
||||||
|
{isDataLoaded ? likesCount : <Dots />}
|
||||||
|
</p>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +51,7 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
|
|||||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown ${
|
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown ${
|
||||||
hasReactedNegatively ? 'IBMSMSMBSS_D_CRDActive' : ''
|
hasReactedNegatively ? 'IBMSMSMBSS_D_CRDActive' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleReaction()}
|
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||||
>
|
>
|
||||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||||
<svg
|
<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>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className='IBMSMSMBSS_Details_CardText'>{disLikesCount}</p>
|
<p className='IBMSMSMBSS_Details_CardText'>
|
||||||
|
{isDataLoaded ? disLikesCount : <Dots />}
|
||||||
|
</p>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||||
</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 { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
|
import { Dots, Spinner } from 'components/Spinner'
|
||||||
import { ZapPopUp } from 'components/Zap'
|
import { ZapPopUp } from 'components/Zap'
|
||||||
import { formatDate } from 'date-fns'
|
import { formatDate } from 'date-fns'
|
||||||
import {
|
import {
|
||||||
@ -59,6 +60,15 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
|||||||
author: AuthorFilterEnum.All_Comments
|
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(() => {
|
useEffect(() => {
|
||||||
setCommentCount(commentEvents.length)
|
setCommentCount(commentEvents.length)
|
||||||
}, [commentEvents, setCommentCount])
|
}, [commentEvents, setCommentCount])
|
||||||
@ -175,8 +185,18 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDiscoveredClick = () => {
|
||||||
|
setVisible(commentEvents)
|
||||||
|
}
|
||||||
|
const [visible, setVisible] = useState<CommentEvent[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
setVisible(commentEvents)
|
||||||
|
}
|
||||||
|
}, [commentEvents, isLoading])
|
||||||
|
|
||||||
const comments = useMemo(() => {
|
const comments = useMemo(() => {
|
||||||
let filteredComments = commentEvents
|
let filteredComments = visible
|
||||||
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
|
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
|
||||||
filteredComments = filteredComments.filter(
|
filteredComments = filteredComments.filter(
|
||||||
(comment) => comment.pubkey === addressable.author
|
(comment) => comment.pubkey === addressable.author
|
||||||
@ -190,13 +210,28 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return filteredComments
|
return filteredComments
|
||||||
}, [commentEvents, filterOptions, addressable.author])
|
}, [visible, filterOptions.author, filterOptions.sort, addressable.author])
|
||||||
|
|
||||||
|
const discoveredCount = commentEvents.length - visible.length
|
||||||
return (
|
return (
|
||||||
<div className='IBMSMSMBSSCommentsWrapper'>
|
<div className='IBMSMSMBSSCommentsWrapper'>
|
||||||
<h4 className='IBMSMSMBSSTitle'>Comments</h4>
|
<h4 className='IBMSMSMBSSTitle'>Comments</h4>
|
||||||
<div className='IBMSMSMBSSComments'>
|
<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
|
<Filter
|
||||||
filterOptions={filterOptions}
|
filterOptions={filterOptions}
|
||||||
setFilterOptions={setFilterOptions}
|
setFilterOptions={setFilterOptions}
|
||||||
@ -355,12 +390,12 @@ const Comment = (props: CommentEvent) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||||
<a className='IBMSMSMBSSCL_CTD_Name' href='profile.html'>
|
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||||
{profile?.displayName || profile?.name || ''}{' '}
|
{profile?.displayName || profile?.name || ''}{' '}
|
||||||
</a>
|
</Link>
|
||||||
<a className='IBMSMSMBSSCL_CTD_Address' href='profile.html'>
|
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||||
{hexToNpub(props.pubkey)}
|
{hexToNpub(props.pubkey)}
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||||
<a className='IBMSMSMBSSCL_CADTime'>
|
<a className='IBMSMSMBSSCL_CADTime'>
|
||||||
@ -446,15 +481,13 @@ const Reactions = (props: Event) => {
|
|||||||
eTag: props.id
|
eTag: props.id
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isDataLoaded) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleReaction(true)}
|
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/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>
|
<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>
|
</svg>
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>{likesCount}</p>
|
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||||
|
{isDataLoaded ? likesCount : <Dots />}
|
||||||
|
</p>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||||
</div>
|
</div>
|
||||||
@ -475,7 +510,7 @@ const Reactions = (props: Event) => {
|
|||||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleReaction()}
|
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/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>
|
<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>
|
</svg>
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>{disLikesCount}</p>
|
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||||
|
{isDataLoaded ? disLikesCount : <Dots />}
|
||||||
|
</p>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,12 @@ export const LANDING_PAGE_DATA = {
|
|||||||
'ELDEN RING',
|
'ELDEN RING',
|
||||||
'The Coffin of Andy and Leyley'
|
'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
|
// 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
|
// reactions are kind 7 events and their content is either emoji icon or emoji shortcode
|
||||||
|
@ -21,7 +21,8 @@ import {
|
|||||||
log,
|
log,
|
||||||
LogType,
|
LogType,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
orderEventsChronologically
|
orderEventsChronologically,
|
||||||
|
timeout
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
|
|
||||||
type FetchModsOptions = {
|
type FetchModsOptions = {
|
||||||
@ -241,8 +242,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
hexKey: string,
|
hexKey: string,
|
||||||
userRelaysType: UserRelaysType
|
userRelaysType: UserRelaysType
|
||||||
): Promise<NDKEvent[]> => {
|
): Promise<NDKEvent[]> => {
|
||||||
// Find the user's relays.
|
// Find the user's relays (10s timeout).
|
||||||
const relayUrls = await getRelayListForUser(hexKey, ndk)
|
const relayUrls = await Promise.race([
|
||||||
|
getRelayListForUser(hexKey, ndk),
|
||||||
|
timeout(10000)
|
||||||
|
])
|
||||||
.then((ndkRelayList) => {
|
.then((ndkRelayList) => {
|
||||||
if (ndkRelayList) return ndkRelayList[userRelaysType]
|
if (ndkRelayList) return ndkRelayList[userRelaysType]
|
||||||
return [] // Return an empty array if ndkRelayList is undefined
|
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 { useMemo, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { UserRelaysType } from 'types'
|
import { UserRelaysType } from 'types'
|
||||||
import { abbreviateNumber, log, LogType, now } from 'utils'
|
import { abbreviateNumber, log, LogType, now, timeout } from 'utils'
|
||||||
|
|
||||||
type UseReactionsParams = {
|
type UseReactionsParams = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@ -32,7 +32,11 @@ export const useReactions = (params: UseReactionsParams) => {
|
|||||||
filter['#e'] = [params.eTag]
|
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) => {
|
.then((events) => {
|
||||||
setReactionEvents(events)
|
setReactionEvents(events)
|
||||||
})
|
})
|
||||||
|
@ -381,16 +381,14 @@ const RegisterButtonWithDialog = () => {
|
|||||||
Browser Extensions (Windows)
|
Browser Extensions (Windows)
|
||||||
</label>
|
</label>
|
||||||
<p className='labelDescriptionMain'>
|
<p className='labelDescriptionMain'>
|
||||||
Once you create your "account" on any of these (
|
Once you create your "account" on any of these, come back and click login, then sign-in with
|
||||||
<a
|
extension. Here's a quick video guide, and here's a <a
|
||||||
href='https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4'
|
href='https://degmods.com/blog/naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c'
|
||||||
target='blank_'
|
>guide post</a> to help with this process.</p>
|
||||||
>
|
<div style={{ width: '100%', height: 'auto', borderRadius: '8px', overflow: 'hidden' }}>
|
||||||
Here's a quick video guide
|
<video controls style={{ width: '100%' }}><source src="https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4" type="video/mp4" />
|
||||||
</a>
|
Your browser does not support the video tag.</video>
|
||||||
), come back and click login, then sign-in with
|
</div>
|
||||||
extension.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
className='btn btnMain btnMainPopup'
|
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'
|
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 (
|
return (
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
<div className='IBMSMTitleMain'>
|
<div className='IBMSMTitleMain'>
|
||||||
<h2 className='IBMSMTitleMainHeading'>Page not found</h2>
|
<h2 className='IBMSMTitleMainHeading'>{error?.title || title}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>The page you're attempting to visit doesn't exist</p>
|
<p>{error?.message || message}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMAction'>
|
<div className='IBMSMAction'>
|
||||||
<Link
|
<Link
|
||||||
|
@ -94,9 +94,7 @@ export const BlogPage = () => {
|
|||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||||
<div className='IBMSMSplitMain'>
|
<div className='IBMSMSplitMain'>
|
||||||
{!blog ? (
|
{blog && (
|
||||||
<LoadingSpinner desc={'Loading...'} />
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<div className='IBMSMSplitMainBigSide'>
|
<div className='IBMSMSplitMainBigSide'>
|
||||||
<div className='IBMSMSplitMainBigSideSec'>
|
<div className='IBMSMSplitMainBigSideSec'>
|
||||||
@ -301,6 +299,7 @@ export const BlogPage = () => {
|
|||||||
)}
|
)}
|
||||||
<div className='IBMSMSplitMainBigSideSec'>
|
<div className='IBMSMSplitMainBigSideSec'>
|
||||||
<Comments
|
<Comments
|
||||||
|
key={blog.id}
|
||||||
addressable={blog as Addressable}
|
addressable={blog as Addressable}
|
||||||
setCommentCount={setCommentCount}
|
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 { NDKContextType } from 'contexts/NDKContext'
|
||||||
import { kinds, nip19 } from 'nostr-tools'
|
import { kinds, nip19 } from 'nostr-tools'
|
||||||
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { appRoutes } from 'routes'
|
import { appRoutes } from 'routes'
|
||||||
import { store } from 'store'
|
import { store } from 'store'
|
||||||
import { BlogPageLoaderResult, FilterOptions, NSFWFilter } from 'types'
|
import {
|
||||||
|
BlogPageLoaderResult,
|
||||||
|
FilterOptions,
|
||||||
|
ModeratedFilter,
|
||||||
|
NSFWFilter
|
||||||
|
} from 'types'
|
||||||
import {
|
import {
|
||||||
DEFAULT_FILTER_OPTIONS,
|
DEFAULT_FILTER_OPTIONS,
|
||||||
getLocalStorageItem,
|
getLocalStorageItem,
|
||||||
@ -16,62 +21,74 @@ import { extractBlogCardDetails, extractBlogDetails } from 'utils/blog'
|
|||||||
|
|
||||||
export const blogRouteLoader =
|
export const blogRouteLoader =
|
||||||
(ndkContext: NDKContextType) =>
|
(ndkContext: NDKContextType) =>
|
||||||
async ({ params }: LoaderFunctionArgs) => {
|
async ({ params, request }: LoaderFunctionArgs) => {
|
||||||
const { naddr } = params
|
const { naddr } = params
|
||||||
if (!naddr) {
|
if (!naddr) {
|
||||||
log(true, LogType.Error, 'Required naddr.')
|
log(true, LogType.Error, 'Required naddr.')
|
||||||
return redirect(appRoutes.blogs)
|
return redirect(appRoutes.blogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode author from naddr
|
// Decode author and identifier from naddr
|
||||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
let pubkey: string | undefined
|
||||||
const { pubkey } = decoded.data
|
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 {
|
try {
|
||||||
// Get the filter with #a from naddr for the main blog content
|
// Set the filter for the main blog content
|
||||||
const filter = filterForEventsTaggingId(naddr)
|
const filter = {
|
||||||
if (!filter) {
|
kinds: [kinds.LongFormArticle],
|
||||||
log(true, LogType.Error, 'Unable to create filter from blog naddr.')
|
authors: [pubkey],
|
||||||
return redirect(appRoutes.blogs)
|
'#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
|
// Get the blog filter options for latest blogs
|
||||||
const filterOptions = JSON.parse(
|
const filterOptions = JSON.parse(
|
||||||
getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
|
getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
|
||||||
) as FilterOptions
|
) as FilterOptions
|
||||||
|
|
||||||
// Fetch 4 in case the current blog is included in the latest
|
// Fetch more in case the current blog is included in the latest and filters remove some
|
||||||
const latestModsFilter: NDKFilter = {
|
const latestFilter: NDKFilter = {
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kinds.LongFormArticle],
|
kinds: [kinds.LongFormArticle],
|
||||||
limit: 4
|
limit: PROFILE_BLOG_FILTER_LIMIT
|
||||||
}
|
}
|
||||||
// Add source filter
|
// Add source filter
|
||||||
if (filterOptions.source === window.location.host) {
|
if (filterOptions.source === window.location.host) {
|
||||||
latestModsFilter['#r'] = [filterOptions.source]
|
latestFilter['#r'] = [filterOptions.source]
|
||||||
}
|
}
|
||||||
// Filter by NSFW tag
|
// Filter by NSFW tag
|
||||||
|
// NSFWFilter.Only_NSFW -> fetch with content-warning label
|
||||||
// NSFWFilter.Show_NSFW -> filter not needed
|
// NSFWFilter.Show_NSFW -> filter not needed
|
||||||
// NSFWFilter.Only_NSFW -> true
|
// NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
|
||||||
// NSFWFilter.Hide_NSFW -> false
|
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
latestFilter['#L'] = ['content-warning']
|
||||||
latestModsFilter['#nsfw'] = [
|
|
||||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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([
|
const settled = await Promise.allSettled([
|
||||||
ndkContext.fetchEvent(filter),
|
ndkContext.fetchEvent(filter),
|
||||||
ndkContext.fetchEvents(latestModsFilter),
|
ndkContext.fetchEvents(latestFilter),
|
||||||
ndkContext.getMuteLists(userState?.user?.pubkey as string),
|
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
|
||||||
ndkContext.getNSFWList()
|
ndkContext.getNSFWList()
|
||||||
])
|
])
|
||||||
|
|
||||||
const result: BlogPageLoaderResult = {
|
const result: BlogPageLoaderResult = {
|
||||||
blog: undefined,
|
blog: undefined,
|
||||||
latest: [],
|
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
|
// Check the lateast blog events
|
||||||
const fetchEventsResult = settled[1]
|
const fetchEventsResult = settled[1]
|
||||||
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
|
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
|
||||||
@ -100,7 +123,6 @@ export const blogRouteLoader =
|
|||||||
result.latest = fetchEventsResult.value
|
result.latest = fetchEventsResult.value
|
||||||
.map(extractBlogCardDetails)
|
.map(extractBlogCardDetails)
|
||||||
.filter((b) => b.id !== result.blog?.id) // Filter out current blog if present
|
.filter((b) => b.id !== result.blog?.id) // Filter out current blog if present
|
||||||
.slice(0, 3) // Take only three
|
|
||||||
} else if (fetchEventsResult.status === 'rejected') {
|
} else if (fetchEventsResult.status === 'rejected') {
|
||||||
log(
|
log(
|
||||||
true,
|
true,
|
||||||
@ -110,22 +132,48 @@ export const blogRouteLoader =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const muteList = settled[2]
|
const muteLists = settled[2]
|
||||||
if (muteList.status === 'fulfilled' && muteList.value) {
|
if (muteLists.status === 'fulfilled' && muteLists.value) {
|
||||||
if (muteList && muteList.value) {
|
if (muteLists && muteLists.value) {
|
||||||
if (result.blog && result.blog.aTag) {
|
if (result.blog && result.blog.aTag) {
|
||||||
if (
|
if (
|
||||||
muteList.value.admin.replaceableEvents.includes(
|
muteLists.value.admin.replaceableEvents.includes(
|
||||||
result.blog.aTag
|
result.blog.aTag
|
||||||
) ||
|
) ||
|
||||||
muteList.value.user.replaceableEvents.includes(result.blog.aTag)
|
muteLists.value.user.replaceableEvents.includes(result.blog.aTag)
|
||||||
) {
|
) {
|
||||||
result.isBlocked = true
|
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') {
|
} else if (muteLists.status === 'rejected') {
|
||||||
log(true, LogType.Error, 'Issue fetching mute list', muteList.reason)
|
log(true, LogType.Error, 'Issue fetching mute list', muteLists.reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nsfwList = settled[3]
|
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) => {
|
result.latest = result.latest.map((b) => {
|
||||||
if (b) {
|
// Add nsfw tag if it's missing
|
||||||
const isMissingNsfwTag =
|
const isMissingNsfwTag =
|
||||||
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
|
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
|
||||||
|
|
||||||
if (isMissingNsfwTag) {
|
if (isMissingNsfwTag) {
|
||||||
b.nsfw = true
|
b.nsfw = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
})
|
})
|
||||||
@ -163,15 +210,24 @@ export const blogRouteLoader =
|
|||||||
log(true, LogType.Error, 'Issue fetching nsfw list', nsfwList.reason)
|
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
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(
|
let message = 'An error occurred in fetching blog details from relays'
|
||||||
true,
|
log(true, LogType.Error, message, error)
|
||||||
LogType.Error,
|
if (error instanceof Error) {
|
||||||
'An error occurred in fetching blog details from relays',
|
message = error.message
|
||||||
error
|
throw new Error(message)
|
||||||
)
|
}
|
||||||
toast.error('An error occurred in fetching blog details from relays')
|
|
||||||
return redirect(appRoutes.blogs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useRef, useState } from 'react'
|
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 { useLocalStorage } from 'hooks'
|
||||||
import { BlogCardDetails, NSFWFilter, SortBy } from 'types'
|
import { BlogCardDetails, NSFWFilter, SortBy } from 'types'
|
||||||
import { SearchInput } from '../../components/SearchInput'
|
import { SearchInput } from '../../components/SearchInput'
|
||||||
@ -10,8 +10,10 @@ import '../../styles/search.css'
|
|||||||
import '../../styles/styles.css'
|
import '../../styles/styles.css'
|
||||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||||
import { scrollIntoView } from 'utils'
|
import { scrollIntoView } from 'utils'
|
||||||
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
|
|
||||||
export const BlogsPage = () => {
|
export const BlogsPage = () => {
|
||||||
|
const navigation = useNavigation()
|
||||||
const blogs = useLoaderData() as Partial<BlogCardDetails>[] | undefined
|
const blogs = useLoaderData() as Partial<BlogCardDetails>[] | undefined
|
||||||
const [filterOptions, setFilterOptions] = useLocalStorage(
|
const [filterOptions, setFilterOptions] = useLocalStorage(
|
||||||
'filter-blog-curated',
|
'filter-blog-curated',
|
||||||
@ -105,6 +107,7 @@ export const BlogsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
|
{navigation.state !== 'idle' && <LoadingSpinner desc={'Loading'} />}
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
<div
|
<div
|
||||||
className='IBMSecMainGroup IBMSecMainGroupAlt'
|
className='IBMSecMainGroup IBMSecMainGroupAlt'
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||||
import { NDKContextType } from 'contexts/NDKContext'
|
import { NDKContextType } from 'contexts/NDKContext'
|
||||||
import { kinds } from 'nostr-tools'
|
import { kinds } from 'nostr-tools'
|
||||||
|
import { BlogCardDetails } from 'types'
|
||||||
import { log, LogType, npubToHex } from 'utils'
|
import { log, LogType, npubToHex } from 'utils'
|
||||||
import { extractBlogCardDetails } from 'utils/blog'
|
import { extractBlogCardDetails } from 'utils/blog'
|
||||||
|
|
||||||
@ -15,14 +16,46 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
|
|||||||
authors: blogHexkeys,
|
authors: blogHexkeys,
|
||||||
kinds: [kinds.LongFormArticle]
|
kinds: [kinds.LongFormArticle]
|
||||||
}
|
}
|
||||||
const events = await ndkContext.fetchEvents(filter)
|
|
||||||
|
|
||||||
if (!events) {
|
const settled = await Promise.allSettled([
|
||||||
log(true, LogType.Error, 'Unable to fetch the blog events.')
|
ndkContext.fetchEvents(filter),
|
||||||
return null
|
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) {
|
} catch (error) {
|
||||||
log(
|
log(
|
||||||
true,
|
true,
|
||||||
@ -30,6 +63,6 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
|
|||||||
'An error occurred in fetching blog details from relays',
|
'An error occurred in fetching blog details from relays',
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { kinds, nip19 } from 'nostr-tools'
|
import { kinds, nip19 } from 'nostr-tools'
|
||||||
import { useMemo, useState } from 'react'
|
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 { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||||
import { BlogCard } from '../components/BlogCard'
|
import { BlogCard } from '../components/BlogCard'
|
||||||
import { GameCard } from '../components/GameCard'
|
import { GameCard } from '../components/GameCard'
|
||||||
import { ModCard } from '../components/ModCard'
|
import { ModCard } from '../components/ModCard'
|
||||||
import { LANDING_PAGE_DATA } from '../constants'
|
import { LANDING_PAGE_DATA, PROFILE_BLOG_FILTER_LIMIT } from '../constants'
|
||||||
import {
|
import {
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
useDidMount,
|
useDidMount,
|
||||||
@ -32,14 +32,12 @@ import '../styles/SimpleSlider.css'
|
|||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
|
|
||||||
// Import Swiper styles
|
// Import Swiper styles
|
||||||
import {
|
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||||
filterForEventsTaggingId,
|
|
||||||
NDKEvent,
|
|
||||||
NDKFilter
|
|
||||||
} from '@nostr-dev-kit/ndk'
|
|
||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import 'swiper/css/navigation'
|
import 'swiper/css/navigation'
|
||||||
import 'swiper/css/pagination'
|
import 'swiper/css/pagination'
|
||||||
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
|
import { Spinner } from 'components/Spinner'
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -260,6 +258,8 @@ const DisplayLatestMods = () => {
|
|||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
fetchMods({ source: window.location.host })
|
fetchMods({ source: window.location.host })
|
||||||
.then((mods) => {
|
.then((mods) => {
|
||||||
|
// Sort by the latest (published_at descending)
|
||||||
|
mods.sort((a, b) => b.published_at - a.published_at)
|
||||||
const wotFilteredMods = mods.filter(
|
const wotFilteredMods = mods.filter(
|
||||||
(mod) => siteWot.includes(mod.author) || userWot.includes(mod.author)
|
(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 DisplayLatestBlogs = () => {
|
||||||
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>()
|
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>()
|
||||||
const { fetchEvents } = useNDKContext()
|
const { fetchEvents } = useNDKContext()
|
||||||
@ -331,86 +323,92 @@ const DisplayLatestBlogs = () => {
|
|||||||
sort: SortBy.Latest,
|
sort: SortBy.Latest,
|
||||||
nsfw: NSFWFilter.Hide_NSFW
|
nsfw: NSFWFilter.Hide_NSFW
|
||||||
})
|
})
|
||||||
|
const navigation = useNavigation()
|
||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
const fetchBlogs = async () => {
|
const fetchBlogs = async () => {
|
||||||
try {
|
try {
|
||||||
// Show maximum of 4 blog posts
|
// Show maximum of 4 blog posts
|
||||||
// 2 should be featured and the most recent 2 from blog npubs
|
// 2 should be featured and the most recent 2 from blog npubs
|
||||||
// Populate the filter from known naddr (constants.ts)
|
// 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++) {
|
for (let i = 0; i < LANDING_PAGE_DATA.featuredBlogPosts.length; i++) {
|
||||||
try {
|
try {
|
||||||
const naddr = LANDING_PAGE_DATA.featuredBlogPosts[i]
|
const naddr = LANDING_PAGE_DATA.featuredBlogPosts[i]
|
||||||
const filterId = filterForEventsTaggingId(naddr)
|
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||||
if (filterId) {
|
const { pubkey, identifier } = decoded.data
|
||||||
filters.push(filterId)
|
if (!filter.authors?.includes(pubkey)) {
|
||||||
|
filter.authors?.push(pubkey)
|
||||||
|
}
|
||||||
|
if (!filter.authors?.includes(identifier)) {
|
||||||
|
filter['#d']?.push(identifier)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silently ignore
|
// 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
|
// Prepare filter for the latest
|
||||||
const blogNpubs = import.meta.env.VITE_BLOG_NPUBS.split(',')
|
const blogNpubs = import.meta.env.VITE_BLOG_NPUBS.split(',')
|
||||||
const blogHexkeys = blogNpubs
|
const blogHexkeys = blogNpubs
|
||||||
.map(npubToHex)
|
.map(npubToHex)
|
||||||
.filter((hexkey) => hexkey !== null)
|
.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 = {
|
const latestFilter: NDKFilter = {
|
||||||
authors: blogHexkeys,
|
authors: blogHexkeys,
|
||||||
kinds: [kinds.LongFormArticle],
|
kinds: [kinds.LongFormArticle],
|
||||||
limit: 4
|
limit: PROFILE_BLOG_FILTER_LIMIT
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by NSFW tag
|
// Filter by NSFW tag
|
||||||
// NSFWFilter.Show_NSFW -> filter not needed
|
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||||
// NSFWFilter.Only_NSFW -> true
|
latestFilter['#L'] = ['content-warning']
|
||||||
// NSFWFilter.Hide_NSFW -> false
|
|
||||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
|
||||||
latestFilter['#nsfw'] = [
|
|
||||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
fetchEvents({ ...filter, kinds: [kinds.LongFormArticle] }),
|
fetchEvents(filter),
|
||||||
fetchEvents(latestFilter)
|
fetchEvents(latestFilter)
|
||||||
])
|
])
|
||||||
|
|
||||||
const events: NDKEvent[] = []
|
const events: Partial<BlogCardDetails>[] = []
|
||||||
// Get featured blogs posts result
|
// Get featured blogs posts result
|
||||||
results.forEach((r) => {
|
results.forEach((r) => {
|
||||||
// Add events from both promises to the array
|
// Add events from both promises to the array
|
||||||
if (r.status === 'fulfilled' && r.value) {
|
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
|
// Remove duplicates
|
||||||
const unique = Array.from(
|
const unique = Array.from(
|
||||||
events
|
events
|
||||||
|
.filter((b) => b.id)
|
||||||
.reduce((map, obj) => {
|
.reduce((map, obj) => {
|
||||||
map.set(obj.id, obj)
|
map.set(obj.id!, obj)
|
||||||
return map
|
return map
|
||||||
}, new Map())
|
}, new Map<string, Partial<BlogCardDetails>>())
|
||||||
.values()
|
.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) {
|
} catch (error) {
|
||||||
log(
|
log(
|
||||||
true,
|
true,
|
||||||
@ -427,6 +425,9 @@ const DisplayLatestBlogs = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
|
{navigation.state !== 'idle' && (
|
||||||
|
<LoadingSpinner desc={'Fetching blog...'} />
|
||||||
|
)}
|
||||||
<div className='IBMSMTitleMain'>
|
<div className='IBMSMTitleMain'>
|
||||||
<h2 className='IBMSMTitleMainHeading'>Blog Posts</h2>
|
<h2 className='IBMSMTitleMainHeading'>Blog Posts</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,11 +14,11 @@ import {
|
|||||||
useNDKContext,
|
useNDKContext,
|
||||||
useNSFWList
|
useNSFWList
|
||||||
} from 'hooks'
|
} from 'hooks'
|
||||||
import { kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
import { kinds, UnsignedEvent } from 'nostr-tools'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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 { toast } from 'react-toastify'
|
||||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
import { appRoutes } from 'routes'
|
||||||
import {
|
import {
|
||||||
BlogCardDetails,
|
BlogCardDetails,
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
@ -32,8 +32,6 @@ import {
|
|||||||
copyTextToClipboard,
|
copyTextToClipboard,
|
||||||
DEFAULT_FILTER_OPTIONS,
|
DEFAULT_FILTER_OPTIONS,
|
||||||
extractBlogCardDetails,
|
extractBlogCardDetails,
|
||||||
log,
|
|
||||||
LogType,
|
|
||||||
now,
|
now,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
scrollIntoView,
|
scrollIntoView,
|
||||||
@ -46,23 +44,11 @@ import { BlogCard } from 'components/BlogCard'
|
|||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
const {
|
const {
|
||||||
|
profilePubkey,
|
||||||
profile,
|
profile,
|
||||||
isBlocked: _isBlocked,
|
isBlocked: _isBlocked,
|
||||||
isOwnProfile
|
isOwnProfile
|
||||||
} = useLoaderData() as ProfilePageLoaderResult
|
} = 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 scrollTargetRef = useRef<HTMLDivElement>(null)
|
||||||
const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext()
|
const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext()
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
@ -262,6 +248,7 @@ export const ProfilePage = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 0:
|
case 0:
|
||||||
|
setLoadingSpinnerDesc('Fetching mods..')
|
||||||
fetchMods({ source: filterOptions.source, author: profilePubkey })
|
fetchMods({ source: filterOptions.source, author: profilePubkey })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setMods(res)
|
setMods(res)
|
||||||
@ -285,22 +272,6 @@ export const ProfilePage = () => {
|
|||||||
profilePubkey
|
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 (
|
return (
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
@ -690,6 +661,7 @@ const ReportUserPopup = ({
|
|||||||
const ProfileTabBlogs = () => {
|
const ProfileTabBlogs = () => {
|
||||||
const { profile, muteLists, nsfwList } =
|
const { profile, muteLists, nsfwList } =
|
||||||
useLoaderData() as ProfilePageLoaderResult
|
useLoaderData() as ProfilePageLoaderResult
|
||||||
|
const navigation = useNavigation()
|
||||||
const { fetchEvents } = useNDKContext()
|
const { fetchEvents } = useNDKContext()
|
||||||
const [filterOptions] = useLocalStorage('filter-blog', DEFAULT_FILTER_OPTIONS)
|
const [filterOptions] = useLocalStorage('filter-blog', DEFAULT_FILTER_OPTIONS)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@ -704,10 +676,8 @@ const ProfileTabBlogs = () => {
|
|||||||
filter['#r'] = [host]
|
filter['#r'] = [host]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
|
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||||
filter['#nsfw'] = [
|
filter['#L'] = ['content-warning']
|
||||||
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
@ -725,7 +695,7 @@ const ProfileTabBlogs = () => {
|
|||||||
}
|
}
|
||||||
fetchEvents(filter)
|
fetchEvents(filter)
|
||||||
.then((events) => {
|
.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)
|
setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -752,7 +722,7 @@ const ProfileTabBlogs = () => {
|
|||||||
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
|
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
|
||||||
setPage((prev) => prev + 1)
|
setPage((prev) => prev + 1)
|
||||||
setBlogs(
|
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))
|
.finally(() => setIsLoading(false))
|
||||||
@ -775,7 +745,7 @@ const ProfileTabBlogs = () => {
|
|||||||
.then((events) => {
|
.then((events) => {
|
||||||
setHasMore(true)
|
setHasMore(true)
|
||||||
setPage((prev) => prev - 1)
|
setPage((prev) => prev - 1)
|
||||||
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
|
setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false))
|
.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"
|
// 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
|
// Allow "Unmoderated Fully" when author visits own profile
|
||||||
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
|
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
|
||||||
@ -845,7 +820,9 @@ const ProfileTabBlogs = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={'Fetching blogs...'} />}
|
{(isLoading || navigation.state !== 'idle') && (
|
||||||
|
<LoadingSpinner desc={'Loading...'} />
|
||||||
|
)}
|
||||||
|
|
||||||
<ModFilter filterKey={'filter-blog'} author={profile?.pubkey as string} />
|
<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 { appRoutes, getProfilePageRoute } from 'routes'
|
||||||
import { store } from 'store'
|
import { store } from 'store'
|
||||||
import { MuteLists, UserProfile } from 'types'
|
import { MuteLists, UserProfile } from 'types'
|
||||||
import { log, LogType } from 'utils'
|
import { log, LogType, npubToHex } from 'utils'
|
||||||
|
|
||||||
export interface ProfilePageLoaderResult {
|
export interface ProfilePageLoaderResult {
|
||||||
|
profilePubkey: string
|
||||||
profile: UserProfile
|
profile: UserProfile
|
||||||
isBlocked: boolean
|
isBlocked: boolean
|
||||||
isOwnProfile: boolean
|
isOwnProfile: boolean
|
||||||
@ -24,10 +25,25 @@ export const profileRouteLoader =
|
|||||||
const { nprofile } = params
|
const { nprofile } = params
|
||||||
let profilePubkey: string | undefined
|
let profilePubkey: string | undefined
|
||||||
try {
|
try {
|
||||||
const value = nprofile
|
// Decode if it starts with nprofile1
|
||||||
? nip19.decode(nprofile as `nprofile1${string}`)
|
if (nprofile?.startsWith('nprofile1')) {
|
||||||
: undefined
|
const value = nprofile
|
||||||
profilePubkey = value?.data.pubkey
|
? 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) {
|
} catch (error) {
|
||||||
// Silently ignore and redirect to home or logged in user
|
// Silently ignore and redirect to home or logged in user
|
||||||
log(true, LogType.Error, 'Failed to decode nprofile.', error)
|
log(true, LogType.Error, 'Failed to decode nprofile.', error)
|
||||||
@ -57,6 +73,7 @@ export const profileRouteLoader =
|
|||||||
|
|
||||||
// Empty result
|
// Empty result
|
||||||
const result: ProfilePageLoaderResult = {
|
const result: ProfilePageLoaderResult = {
|
||||||
|
profilePubkey: profilePubkey,
|
||||||
profile: {},
|
profile: {},
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isOwnProfile: false,
|
isOwnProfile: false,
|
||||||
|
@ -84,22 +84,27 @@ export const writeRouteAction =
|
|||||||
.split(',')
|
.split(',')
|
||||||
.map((t) => ['t', t])
|
.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 = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
kind: kinds.LongFormArticle,
|
kind: kinds.LongFormArticle,
|
||||||
created_at: currentTimeStamp,
|
created_at: currentTimeStamp,
|
||||||
pubkey: hexPubkey,
|
pubkey: hexPubkey,
|
||||||
content: content,
|
content: content,
|
||||||
tags: [
|
tags: 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
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -103,13 +103,15 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
|||||||
path: appRoutes.blog,
|
path: appRoutes.blog,
|
||||||
element: <BlogPage />,
|
element: <BlogPage />,
|
||||||
loader: blogRouteLoader(context),
|
loader: blogRouteLoader(context),
|
||||||
action: blogRouteAction(context)
|
action: blogRouteAction(context),
|
||||||
|
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: appRoutes.blogEdit,
|
path: appRoutes.blogEdit,
|
||||||
element: <WritePage key='edit' />,
|
element: <WritePage key='edit' />,
|
||||||
loader: blogRouteLoader(context),
|
loader: blogRouteLoader(context),
|
||||||
action: writeRouteAction(context)
|
action: writeRouteAction(context),
|
||||||
|
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: appRoutes.blogReport_actionOnly,
|
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 { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
|
||||||
import { kinds, nip19 } from 'nostr-tools'
|
import { kinds, nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => ({
|
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => {
|
||||||
title: getFirstTagValue(event, 'title'),
|
const dTag = getFirstTagValue(event, 'd')
|
||||||
content: event.content,
|
|
||||||
summary: getFirstTagValue(event, 'summary'),
|
|
||||||
image: getFirstTagValue(event, 'image'),
|
|
||||||
nsfw: getFirstTagValue(event, 'nsfw') === 'true',
|
|
||||||
|
|
||||||
id: event.id,
|
// Check if the aTag exists on the blog
|
||||||
author: event.pubkey,
|
let aTag = getFirstTagValue(event, 'a')
|
||||||
published_at: getFirstTagValueAsInt(event, 'published_at'),
|
|
||||||
edited_at: event.created_at,
|
// Create aTag from components if aTag is not included
|
||||||
rTag: getFirstTagValue(event, 'r') || 'N/A',
|
if (typeof aTag === 'undefined' && event.pubkey && dTag) {
|
||||||
dTag: getFirstTagValue(event, 'd'),
|
aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}`
|
||||||
aTag: getFirstTagValue(event, 'a'),
|
}
|
||||||
tTags: getTagValues(event, 't') || []
|
|
||||||
})
|
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 = (
|
export const extractBlogCardDetails = (
|
||||||
event: NDKEvent
|
event: NDKEvent
|
||||||
|
Loading…
Reference in New Issue
Block a user