Feed - posts #227
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"names": {
|
"names": {
|
||||||
|
"_": "f4bf1fb5ba8be839f70c7331733e309f780822b311f63e01f9dc8abbb428f8d5",
|
||||||
"degmods": "f4bf1fb5ba8be839f70c7331733e309f780822b311f63e01f9dc8abbb428f8d5",
|
"degmods": "f4bf1fb5ba8be839f70c7331733e309f780822b311f63e01f9dc8abbb428f8d5",
|
||||||
"degmodsreposter": "7382a4cc21742ac3e3581f1c653a41f912e985e6a941439377803b866042e53f",
|
"degmodsreposter": "7382a4cc21742ac3e3581f1c653a41f912e985e6a941439377803b866042e53f",
|
||||||
"degmodsreport": "ca2734bb5e59427dd5d66f59dde3b4a045110b7a12eb99a4a862bf012b7850d9",
|
"degmodsreport": "ca2734bb5e59427dd5d66f59dde3b4a045110b7a12eb99a4a862bf012b7850d9",
|
||||||
@ -11,4 +12,4 @@
|
|||||||
"podcast_at_melonmancy.net": "4f66998fc435425257e5672a58b5c6fefda86a8b33514780e52d024a54f50ede",
|
"podcast_at_melonmancy.net": "4f66998fc435425257e5672a58b5c6fefda86a8b33514780e52d024a54f50ede",
|
||||||
"gradash": "e0c92699e3f3baacb775cb033b0541af381c7ed9fde255ac556048c1bcaf0432"
|
"gradash": "e0c92699e3f3baacb775cb033b0541af381c7ed9fde255ac556048c1bcaf0432"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
import { AlertPopupProps } from 'types'
|
import { AlertPopupProps } from 'types'
|
||||||
|
|
||||||
export const AlertPopup = ({
|
export const AlertPopup = ({
|
||||||
header,
|
header,
|
||||||
label,
|
label,
|
||||||
handleConfirm,
|
handleConfirm,
|
||||||
handleClose
|
handleClose,
|
||||||
}: AlertPopupProps) => {
|
yesButtonLabel = 'Yes',
|
||||||
|
noButtonLabel = 'No',
|
||||||
|
children
|
||||||
|
}: PropsWithChildren<AlertPopupProps>) => {
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className='popUpMain'>
|
<div className='popUpMain'>
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
@ -38,6 +42,7 @@ export const AlertPopup = ({
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -51,14 +56,14 @@ export const AlertPopup = ({
|
|||||||
type='button'
|
type='button'
|
||||||
onPointerDown={() => handleConfirm(true)}
|
onPointerDown={() => handleConfirm(true)}
|
||||||
>
|
>
|
||||||
Yes
|
{yesButtonLabel}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className='btn btnMain btnMainPopup'
|
className='btn btnMain btnMainPopup'
|
||||||
type='button'
|
type='button'
|
||||||
onPointerDown={() => handleConfirm(false)}
|
onPointerDown={() => handleConfirm(false)}
|
||||||
>
|
>
|
||||||
No
|
{noButtonLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
299
src/components/Notes/Note.tsx
Normal file
299
src/components/Notes/Note.tsx
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import {
|
||||||
|
NDKEvent,
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKSubscriptionCacheUsage
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
|
import { CommentContent } from 'components/comment/CommentContent'
|
||||||
|
import { Reactions } from 'components/comment/Reactions'
|
||||||
|
import { Zap } from 'components/comment/Zap'
|
||||||
|
import { Dots } from 'components/Spinner'
|
||||||
|
import { formatDate } from 'date-fns'
|
||||||
|
import { useAppSelector, useDidMount, useNDKContext } from 'hooks'
|
||||||
|
import { useComments } from 'hooks/useComments'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||||
|
import { UserProfile } from 'types'
|
||||||
|
import { hexToNpub } from 'utils'
|
||||||
|
import { NoteRepostPopup } from './NoteRepostPopup'
|
||||||
|
import { NoteQuoteRepostPopup } from './NoteQuoteRepostPopup'
|
||||||
|
|
||||||
|
interface NoteProps {
|
||||||
|
ndkEvent: NDKEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Note = ({ ndkEvent }: NoteProps) => {
|
||||||
|
const { ndk } = useNDKContext()
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
const userPubkey = userState.user?.pubkey as string | undefined
|
||||||
|
const [eventProfile, setEventProfile] = useState<UserProfile>()
|
||||||
|
const isRepost = ndkEvent.kind === NDKKind.Repost
|
||||||
|
const [repostEvent, setRepostEvent] = useState<NDKEvent | undefined>()
|
||||||
|
const [repostProfile, setRepostProfile] = useState<UserProfile | undefined>()
|
||||||
|
const noteEvent = repostEvent ?? ndkEvent
|
||||||
|
const noteProfile = repostProfile ?? eventProfile
|
||||||
|
const { commentEvents } = useComments(ndkEvent.pubkey, undefined, ndkEvent.id)
|
||||||
|
const [quoteRepostEvents, setQuoteRepostEvents] = useState<NDKEvent[]>([])
|
||||||
|
const [hasQuoted, setHasQuoted] = useState(false)
|
||||||
|
const [repostEvents, setRepostEvents] = useState<NDKEvent[]>([])
|
||||||
|
const [hasReposted, setHasReposted] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [showRepostPopup, setShowRepostPopup] = useState(false)
|
||||||
|
const [showQuoteRepostPopup, setShowQuoteRepostPopup] = useState(false)
|
||||||
|
|
||||||
|
useDidMount(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
ndkEvent.author.fetchProfile().then((res) => setEventProfile(res))
|
||||||
|
|
||||||
|
if (isRepost) {
|
||||||
|
const parsedEvent = JSON.parse(ndkEvent.content)
|
||||||
|
const ndkRepostEvent = new NDKEvent(ndk, parsedEvent)
|
||||||
|
setRepostEvent(ndkRepostEvent)
|
||||||
|
ndkRepostEvent.author.fetchProfile().then((res) => setRepostProfile(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
const repostFilter: NDKFilter = {
|
||||||
|
kinds: [NDKKind.Repost],
|
||||||
|
'#e': [ndkEvent.id]
|
||||||
|
}
|
||||||
|
const quoteFilter: NDKFilter = {
|
||||||
|
kinds: [NDKKind.Text],
|
||||||
|
'#q': [ndkEvent.id]
|
||||||
|
}
|
||||||
|
ndk
|
||||||
|
.fetchEvents([repostFilter, quoteFilter], {
|
||||||
|
closeOnEose: true,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
const ndkEvents = Array.from(ndkEventSet)
|
||||||
|
|
||||||
|
if (ndkEventSet.size) {
|
||||||
|
const quoteRepostEvents = ndkEvents.filter(
|
||||||
|
(n) => n.kind === NDKKind.Text
|
||||||
|
)
|
||||||
|
userPubkey &&
|
||||||
|
setHasQuoted(
|
||||||
|
quoteRepostEvents.some((qr) => qr.pubkey === userPubkey)
|
||||||
|
)
|
||||||
|
setQuoteRepostEvents(quoteRepostEvents)
|
||||||
|
|
||||||
|
const repostEvents = ndkEvents.filter(
|
||||||
|
(n) => n.kind === NDKKind.Repost
|
||||||
|
)
|
||||||
|
userPubkey &&
|
||||||
|
setHasReposted(repostEvents.some((qr) => qr.pubkey === userPubkey))
|
||||||
|
setRepostEvents(repostEvents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const profileRoute = getProfilePageRoute(
|
||||||
|
nip19.nprofileEncode({
|
||||||
|
pubkey: noteEvent.pubkey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const reposterRoute = repostEvent
|
||||||
|
? getProfilePageRoute(
|
||||||
|
nip19.nprofileEncode({
|
||||||
|
pubkey: ndkEvent.pubkey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const baseUrl = appRoutes.feed + '/'
|
||||||
|
|
||||||
|
// Did user already repost this
|
||||||
|
|
||||||
|
// Show who reposted the note
|
||||||
|
const reposterVisual =
|
||||||
|
repostEvent && reposterRoute ? (
|
||||||
|
<>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentRepost'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentRepostVisual'>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 -64 640 640'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className='IBMSMSMBSSCL_CommentRepostText'>
|
||||||
|
<Link to={reposterRoute}>
|
||||||
|
{eventProfile?.displayName || eventProfile?.name || ''}{' '}
|
||||||
|
</Link>
|
||||||
|
Reposted...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
const handleRepost = async (confirm: boolean) => {
|
||||||
|
setShowRepostPopup(false)
|
||||||
|
|
||||||
|
// Cancel if not confirmed
|
||||||
|
if (!confirm) return
|
||||||
|
|
||||||
|
const repostNdkEvent = await ndkEvent.repost(false)
|
||||||
|
await repostNdkEvent.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this user's repost?
|
||||||
|
const isUsersRepost =
|
||||||
|
isRepost && ndkEvent.author.pubkey === userState.user?.pubkey
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='IBMSMSMBSSCL_CommentWrapper'>
|
||||||
|
<div className='IBMSMSMBSSCL_Comment'>
|
||||||
|
{reposterVisual}
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||||
|
<Link
|
||||||
|
className='IBMSMSMBSSCL_CommentTopPP'
|
||||||
|
to={profileRoute}
|
||||||
|
style={{
|
||||||
|
background: `url('${
|
||||||
|
noteProfile?.image || ''
|
||||||
|
}') center / cover no-repeat`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||||
|
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||||
|
{noteProfile?.displayName || noteProfile?.name || ''}{' '}
|
||||||
|
</Link>
|
||||||
|
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||||
|
{hexToNpub(noteEvent.pubkey)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{noteEvent.created_at && (
|
||||||
|
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||||
|
<a className='IBMSMSMBSSCL_CADTime'>
|
||||||
|
{formatDate(noteEvent.created_at * 1000, 'hh:mm aa')}{' '}
|
||||||
|
</a>
|
||||||
|
<a className='IBMSMSMBSSCL_CADDate'>
|
||||||
|
{formatDate(noteEvent.created_at * 1000, 'dd/MM/yyyy')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||||
|
<CommentContent content={noteEvent.content} />
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||||
|
<Reactions {...noteEvent.rawEvent()} />
|
||||||
|
{/* Quote Repost, Kind 1 */}
|
||||||
|
<div
|
||||||
|
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost ${
|
||||||
|
hasQuoted ? 'IBMSMSMBSSCL_CAERepostActive' : ''
|
||||||
|
}`}
|
||||||
|
onClick={
|
||||||
|
isLoading ? undefined : () => setShowQuoteRepostPopup(true)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
className='IBMSMSMBSSCL_CAElementIcon'
|
||||||
|
>
|
||||||
|
<path d='M256 31.1c-141.4 0-255.1 93.09-255.1 208c0 49.59 21.38 94.1 56.97 130.7c-12.5 50.39-54.31 95.3-54.81 95.8C0 468.8-.5938 472.2 .6875 475.2c1.312 3 4.125 4.797 7.312 4.797c66.31 0 116-31.8 140.6-51.41c32.72 12.31 69.01 19.41 107.4 19.41C397.4 447.1 512 354.9 512 239.1S397.4 31.1 256 31.1zM368 266c0 8.836-7.164 16-16 16h-54V336c0 8.836-7.164 16-16 16h-52c-8.836 0-16-7.164-16-16V282H160c-8.836 0-16-7.164-16-16V214c0-8.838 7.164-16 16-16h53.1V144c0-8.838 7.164-16 16-16h52c8.836 0 16 7.162 16 16v54H352c8.836 0 16 7.162 16 16V266z'></path>
|
||||||
|
</svg>
|
||||||
|
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||||
|
{isLoading ? <Dots /> : quoteRepostEvents.length}
|
||||||
|
</p>
|
||||||
|
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||||
|
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showQuoteRepostPopup && (
|
||||||
|
<NoteQuoteRepostPopup
|
||||||
|
ndkEvent={repostEvent || ndkEvent}
|
||||||
|
handleClose={() => setShowQuoteRepostPopup(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Repost, Kind 6 */}
|
||||||
|
{!isUsersRepost && (
|
||||||
|
<div
|
||||||
|
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost ${
|
||||||
|
hasReposted ? 'IBMSMSMBSSCL_CAERepostActive' : ''
|
||||||
|
}`}
|
||||||
|
onClick={
|
||||||
|
isLoading || hasReposted
|
||||||
|
? undefined
|
||||||
|
: () => setShowRepostPopup(true)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 -64 640 640'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
className='IBMSMSMBSSCL_CAElementIcon'
|
||||||
|
>
|
||||||
|
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||||
|
</svg>
|
||||||
|
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||||
|
{isLoading ? <Dots /> : repostEvents.length}
|
||||||
|
</p>
|
||||||
|
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||||
|
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showRepostPopup && (
|
||||||
|
<NoteRepostPopup
|
||||||
|
ndkEvent={repostEvent || ndkEvent}
|
||||||
|
handleConfirm={handleRepost}
|
||||||
|
handleClose={() => setShowRepostPopup(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{typeof noteProfile?.lud16 !== 'undefined' &&
|
||||||
|
noteProfile.lud16 !== '' && <Zap {...noteEvent.rawEvent()} />}
|
||||||
|
<Link
|
||||||
|
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||||
|
to={baseUrl + noteEvent.encode()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
className='IBMSMSMBSSCL_CAElementIcon'
|
||||||
|
>
|
||||||
|
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||||
|
</svg>
|
||||||
|
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||||
|
{commentEvents.length}
|
||||||
|
</p>
|
||||||
|
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
||||||
|
to={baseUrl + noteEvent.encode()}
|
||||||
|
>
|
||||||
|
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
23
src/components/Notes/NotePreview.tsx
Normal file
23
src/components/Notes/NotePreview.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { NoteRender } from './NoteRender'
|
||||||
|
|
||||||
|
interface NotePreviewProps {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotePreview = ({ content }: NotePreviewProps) => {
|
||||||
|
return (
|
||||||
|
<div className='feedPostsPostPreview'>
|
||||||
|
<div className='feedPostsPostPreviewNote'>
|
||||||
|
<p>
|
||||||
|
Previewing post
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||||
|
<div className='IBMSMSMBSSCL_CBText'>
|
||||||
|
<NoteRender content={content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
61
src/components/Notes/NoteQuoteRepostPopup.tsx
Normal file
61
src/components/Notes/NoteQuoteRepostPopup.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
|
import { NoteSubmit } from './NoteSubmit'
|
||||||
|
|
||||||
|
interface NoteQuoteRepostPopup {
|
||||||
|
ndkEvent: NDKEvent
|
||||||
|
handleClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoteQuoteRepostPopup = ({
|
||||||
|
ndkEvent,
|
||||||
|
handleClose
|
||||||
|
}: PropsWithChildren<NoteQuoteRepostPopup>) => {
|
||||||
|
const content = `nostr:${ndkEvent.encode()}`
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className='popUpMain'>
|
||||||
|
<div className='ContainerMain'>
|
||||||
|
<div className='popUpMainCardWrapper'>
|
||||||
|
<div className='popUpMainCard'>
|
||||||
|
<div className='popUpMainCardTop'>
|
||||||
|
<div className='popUpMainCardTopInfo'>
|
||||||
|
<h3>Quote Repost</h3>
|
||||||
|
</div>
|
||||||
|
<div className='popUpMainCardTopClose' onClick={handleClose}>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-96 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pUMCB_Zaps'>
|
||||||
|
<div className='pUMCB_ZapsInside'>
|
||||||
|
<div className='inputLabelWrapperMain'>
|
||||||
|
<label
|
||||||
|
className='form-label labelMain'
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
Quote repost this?
|
||||||
|
</label>
|
||||||
|
<NoteSubmit
|
||||||
|
initialContent={`\n\n:${content}`}
|
||||||
|
handleClose={handleClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
}
|
86
src/components/Notes/NoteRender.tsx
Normal file
86
src/components/Notes/NoteRender.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { NDKKind } from '@nostr-dev-kit/ndk'
|
||||||
|
import { ProfileLink } from 'components/ProfileSection'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Fragment } from 'react/jsx-runtime'
|
||||||
|
import { BlogPreview } from './internal/BlogPreview'
|
||||||
|
import { ModPreview } from './internal/ModPreview'
|
||||||
|
import { NoteWrapper } from './internal/NoteWrapper'
|
||||||
|
|
||||||
|
interface NoteRenderProps {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
const link =
|
||||||
|
/(?:https?:\/\/|www\.)(?:[a-zA-Z0-9.-]+\.[a-zA-Z]+(?::\d+)?)(?:[/?#][\p{L}\p{N}\p{M}&.-/?=#\-@%+_,:!~*]*)?/gu
|
||||||
|
const nostrMention =
|
||||||
|
/(?:nostr:|@)?(?:npub|note|nprofile|nevent|naddr)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,}/gi
|
||||||
|
const nostrEntity =
|
||||||
|
/(npub|note|nprofile|nevent|naddr)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,}/gi
|
||||||
|
|
||||||
|
export const NoteRender = ({ content }: NoteRenderProps) => {
|
||||||
|
const _content = useMemo(() => {
|
||||||
|
if (!content) return
|
||||||
|
|
||||||
|
const parts = content.split(
|
||||||
|
new RegExp(`(${link.source})|(${nostrMention.source})`, 'gui')
|
||||||
|
)
|
||||||
|
|
||||||
|
const _parts = parts.map((part, index) => {
|
||||||
|
if (link.test(part)) {
|
||||||
|
const [href] = part.match(link) || []
|
||||||
|
return (
|
||||||
|
<a key={index} href={href}>
|
||||||
|
{href}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
} else if (nostrMention.test(part)) {
|
||||||
|
const [encoded] = part.match(nostrEntity) || []
|
||||||
|
|
||||||
|
if (!encoded) return part
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(encoded)
|
||||||
|
|
||||||
|
switch (decoded.type) {
|
||||||
|
case 'nprofile':
|
||||||
|
return <ProfileLink key={index} pubkey={decoded.data.pubkey} />
|
||||||
|
case 'npub':
|
||||||
|
return <ProfileLink key={index} pubkey={decoded.data} />
|
||||||
|
case 'note':
|
||||||
|
return <NoteWrapper key={index} noteEntity={encoded} />
|
||||||
|
case 'nevent':
|
||||||
|
return <NoteWrapper key={index} noteEntity={encoded} />
|
||||||
|
case 'naddr':
|
||||||
|
return (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{handleNaddr(decoded.data, part)}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return _parts
|
||||||
|
}, [content])
|
||||||
|
|
||||||
|
return _content
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNaddr(data: nip19.AddressPointer, original: string) {
|
||||||
|
const { kind } = data
|
||||||
|
|
||||||
|
if (kind === NDKKind.Article) {
|
||||||
|
return <BlogPreview {...data} original={original} />
|
||||||
|
} else if (kind === NDKKind.Classified) {
|
||||||
|
return <ModPreview {...data} original={original} />
|
||||||
|
} else {
|
||||||
|
return <>{original}</>
|
||||||
|
}
|
||||||
|
}
|
117
src/components/Notes/NoteRepostPopup.tsx
Normal file
117
src/components/Notes/NoteRepostPopup.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { AlertPopup } from 'components/AlertPopup'
|
||||||
|
import { useAppSelector, useDidMount } from 'hooks'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { CommentContent } from 'components/comment/CommentContent'
|
||||||
|
import { getProfilePageRoute } from 'routes'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { UserProfile } from 'types'
|
||||||
|
import { hexToNpub } from 'utils'
|
||||||
|
import { formatDate } from 'date-fns'
|
||||||
|
|
||||||
|
interface NoteRepostProps {
|
||||||
|
ndkEvent: NDKEvent
|
||||||
|
handleConfirm: (confirm: boolean) => void
|
||||||
|
handleClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoteRepostPopup = ({
|
||||||
|
ndkEvent,
|
||||||
|
handleConfirm,
|
||||||
|
handleClose
|
||||||
|
}: NoteRepostProps) => {
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
const userPubkey = userState.user?.pubkey as string | undefined
|
||||||
|
const [content, setContent] = useState<string>('')
|
||||||
|
const [profile, setProfile] = useState<UserProfile>()
|
||||||
|
|
||||||
|
useDidMount(async () => {
|
||||||
|
const repost = await ndkEvent.repost(false)
|
||||||
|
setContent(JSON.parse(repost.content).content)
|
||||||
|
ndkEvent.author.fetchProfile().then((res) => setProfile(res))
|
||||||
|
})
|
||||||
|
|
||||||
|
const profileRoute = getProfilePageRoute(
|
||||||
|
nip19.nprofileEncode({
|
||||||
|
pubkey: ndkEvent.pubkey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const reposterRoute = getProfilePageRoute(
|
||||||
|
nip19.nprofileEncode({
|
||||||
|
pubkey: userPubkey!
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertPopup
|
||||||
|
handleConfirm={handleConfirm}
|
||||||
|
handleClose={handleClose}
|
||||||
|
header={'Repost'}
|
||||||
|
label={`Repost this?`}
|
||||||
|
yesButtonLabel='Repost Now'
|
||||||
|
noButtonLabel='Never mind'
|
||||||
|
>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentQP'>
|
||||||
|
<div className='IBMSMSMBSSCL_Comment'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentRepost'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentRepostVisual'>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 -64 640 640'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className='IBMSMSMBSSCL_CommentRepostText'>
|
||||||
|
<Link to={reposterRoute}>
|
||||||
|
{userState.user?.displayName || userState.user?.name || ''}{' '}
|
||||||
|
</Link>
|
||||||
|
Reposted...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||||
|
<Link
|
||||||
|
className='IBMSMSMBSSCL_CommentTopPP'
|
||||||
|
to={profileRoute}
|
||||||
|
style={{
|
||||||
|
background: `url('${
|
||||||
|
profile?.image || ''
|
||||||
|
}') center / cover no-repeat`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||||
|
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||||
|
{profile?.displayName || profile?.name || ''}{' '}
|
||||||
|
</Link>
|
||||||
|
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||||
|
{hexToNpub(ndkEvent.pubkey)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{ndkEvent.created_at && (
|
||||||
|
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||||
|
<a className='IBMSMSMBSSCL_CADTime'>
|
||||||
|
{formatDate(ndkEvent.created_at * 1000, 'hh:mm aa')}{' '}
|
||||||
|
</a>
|
||||||
|
<a className='IBMSMSMBSSCL_CADDate'>
|
||||||
|
{formatDate(ndkEvent.created_at * 1000, 'dd/MM/yyyy')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||||
|
<CommentContent content={content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertPopup>
|
||||||
|
)
|
||||||
|
}
|
152
src/components/Notes/NoteSubmit.tsx
Normal file
152
src/components/Notes/NoteSubmit.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk'
|
||||||
|
import { FALLBACK_PROFILE_IMAGE } from '../../constants'
|
||||||
|
import { useAppSelector } from 'hooks'
|
||||||
|
import { useProfile } from 'hooks/useProfile'
|
||||||
|
import { Navigate, useNavigation, useSubmit } from 'react-router-dom'
|
||||||
|
import { appRoutes } from 'routes'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { adjustTextareaHeight } from 'utils'
|
||||||
|
import { NotePreview } from './NotePreview'
|
||||||
|
|
||||||
|
interface NoteSubmitProps {
|
||||||
|
initialContent?: string | undefined
|
||||||
|
handleClose?: () => void | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoteSubmit = ({
|
||||||
|
initialContent,
|
||||||
|
handleClose
|
||||||
|
}: NoteSubmitProps) => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
const profile = useProfile(userState.user?.pubkey as string | undefined, {
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
const [content, setContent] = useState(initialContent ?? '')
|
||||||
|
const [nsfw, setNsfw] = useState(false)
|
||||||
|
const [showPreview, setShowPreview] = useState(!!initialContent)
|
||||||
|
const image = profile?.image || FALLBACK_PROFILE_IMAGE
|
||||||
|
const ref = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const submit = useSubmit()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current && !!initialContent) {
|
||||||
|
adjustTextareaHeight(ref.current)
|
||||||
|
ref.current.focus()
|
||||||
|
}
|
||||||
|
}, [initialContent])
|
||||||
|
|
||||||
|
const handleContentChange = (
|
||||||
|
event: React.ChangeEvent<HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
setContent(event.currentTarget.value)
|
||||||
|
adjustTextareaHeight(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const formSubmit = {
|
||||||
|
content,
|
||||||
|
nsfw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setContent('')
|
||||||
|
setNsfw(false)
|
||||||
|
|
||||||
|
submit(JSON.stringify(formSubmit), {
|
||||||
|
method: 'post',
|
||||||
|
encType: 'application/json'
|
||||||
|
})
|
||||||
|
|
||||||
|
typeof handleClose === 'function' && handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviewToggle = () => {
|
||||||
|
setShowPreview((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userState.user?.pubkey) return <Navigate to={appRoutes.home} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form className='feedPostsPost' onSubmit={handleFormSubmit}>
|
||||||
|
<div className='feedPostsPostInside'>
|
||||||
|
<div className='feedPostsPostInsideInputWrapper'>
|
||||||
|
<div className='feedPostsPostInsidePP'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||||
|
<div
|
||||||
|
className='IBMSMSMBSSCL_CommentTopPP'
|
||||||
|
style={{
|
||||||
|
background: `url(${image}) center/cover no-repeat`,
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id='postSocialTextarea'
|
||||||
|
name='content'
|
||||||
|
className='inputMain feedPostsPostInsideInput'
|
||||||
|
placeholder='Watcha thinking about?'
|
||||||
|
ref={ref}
|
||||||
|
value={content}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showPreview && <NotePreview content={content} />}
|
||||||
|
<div className='feedPostsPostInsideAction'>
|
||||||
|
<div
|
||||||
|
className='inputLabelWrapperMain inputLabelWrapperMainAlt'
|
||||||
|
style={{ width: 'unset' }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='CheckboxMain'
|
||||||
|
checked={nsfw}
|
||||||
|
id='nsfw'
|
||||||
|
name='nsfw'
|
||||||
|
onChange={() => setNsfw((nsfw) => !nsfw)}
|
||||||
|
/>
|
||||||
|
<label htmlFor='nsfw' className='form-label labelMain'>
|
||||||
|
NSFW
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', flexDirection: 'row', gridGap: '10px' }}
|
||||||
|
>
|
||||||
|
{typeof handleClose === 'function' && (
|
||||||
|
<button
|
||||||
|
className='btn btnMain'
|
||||||
|
type='button'
|
||||||
|
style={{ padding: '5px 20px', borderRadius: '8px' }}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className='btn btnMain'
|
||||||
|
type='button'
|
||||||
|
style={{ padding: '5px 20px', borderRadius: '8px' }}
|
||||||
|
onClick={handlePreviewToggle}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='btn btnMain'
|
||||||
|
type='submit'
|
||||||
|
style={{ padding: '5px 20px', borderRadius: '8px' }}
|
||||||
|
disabled={navigation.state !== 'idle'}
|
||||||
|
>
|
||||||
|
{navigation.state === 'idle' ? 'Post' : 'Posting...'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
25
src/components/Notes/internal/BlogPreview.tsx
Normal file
25
src/components/Notes/internal/BlogPreview.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { getBlogPageRoute } from 'routes'
|
||||||
|
import { truncate } from 'utils'
|
||||||
|
|
||||||
|
type BlogPreviewProps = nip19.AddressPointer & {
|
||||||
|
original: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlogPreview = ({
|
||||||
|
identifier,
|
||||||
|
kind,
|
||||||
|
pubkey,
|
||||||
|
original
|
||||||
|
}: BlogPreviewProps) => {
|
||||||
|
const route = getBlogPageRoute(
|
||||||
|
nip19.naddrEncode({
|
||||||
|
identifier,
|
||||||
|
pubkey,
|
||||||
|
kind
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Link to={route}>{truncate(original)}</Link>
|
||||||
|
}
|
25
src/components/Notes/internal/ModPreview.tsx
Normal file
25
src/components/Notes/internal/ModPreview.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { getModPageRoute } from 'routes'
|
||||||
|
import { truncate } from 'utils'
|
||||||
|
|
||||||
|
type ModPreviewProps = nip19.AddressPointer & {
|
||||||
|
original: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModPreview = ({
|
||||||
|
identifier,
|
||||||
|
kind,
|
||||||
|
pubkey,
|
||||||
|
original
|
||||||
|
}: ModPreviewProps) => {
|
||||||
|
const route = getModPageRoute(
|
||||||
|
nip19.naddrEncode({
|
||||||
|
identifier,
|
||||||
|
pubkey,
|
||||||
|
kind
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Link to={route}>{truncate(original)}</Link>
|
||||||
|
}
|
77
src/components/Notes/internal/NoteWrapper.tsx
Normal file
77
src/components/Notes/internal/NoteWrapper.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useDidMount, useNDKContext } from 'hooks'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||||
|
import { filterFromId, NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { UserProfile } from 'types'
|
||||||
|
import { hexToNpub } from 'utils'
|
||||||
|
import { formatDate } from 'date-fns'
|
||||||
|
import { CommentContent } from 'components/comment/CommentContent'
|
||||||
|
import { Dots } from 'components/Spinner'
|
||||||
|
|
||||||
|
interface NoteWrapperProps {
|
||||||
|
noteEntity: string
|
||||||
|
}
|
||||||
|
export const NoteWrapper = ({ noteEntity }: NoteWrapperProps) => {
|
||||||
|
const { fetchEvent } = useNDKContext()
|
||||||
|
const [note, setNote] = useState<NDKEvent>()
|
||||||
|
const [profile, setProfile] = useState<UserProfile>()
|
||||||
|
useDidMount(() => {
|
||||||
|
const filter = filterFromId(noteEntity)
|
||||||
|
fetchEvent(filter).then((ndkEvent) => {
|
||||||
|
if (ndkEvent) {
|
||||||
|
setNote(ndkEvent)
|
||||||
|
ndkEvent.author.fetchProfile().then((res) => setProfile(res))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const profileRoute = note?.author.nprofile
|
||||||
|
? getProfilePageRoute(note?.author.nprofile)
|
||||||
|
: appRoutes.home
|
||||||
|
|
||||||
|
if (!note) return <Dots />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='IBMSMSMBSSCL_CommentQP'>
|
||||||
|
<div className='IBMSMSMBSSCL_Comment'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||||
|
<Link
|
||||||
|
className='IBMSMSMBSSCL_CommentTopPP'
|
||||||
|
to={profileRoute}
|
||||||
|
style={{
|
||||||
|
background: `url('${
|
||||||
|
profile?.image || ''
|
||||||
|
}') center / cover no-repeat`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||||
|
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
|
||||||
|
{profile?.displayName || profile?.name || ''}{' '}
|
||||||
|
</Link>
|
||||||
|
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
|
||||||
|
{hexToNpub(note.pubkey)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{note.created_at && (
|
||||||
|
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||||
|
<a className='IBMSMSMBSSCL_CADTime'>
|
||||||
|
{formatDate(note.created_at * 1000, 'hh:mm aa')}{' '}
|
||||||
|
</a>
|
||||||
|
<a className='IBMSMSMBSSCL_CADDate'>
|
||||||
|
{formatDate(note.created_at * 1000, 'dd/MM/yyyy')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||||
|
<CommentContent content={note.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -21,7 +21,8 @@ import {
|
|||||||
log,
|
log,
|
||||||
LogType,
|
LogType,
|
||||||
now,
|
now,
|
||||||
npubToHex
|
npubToHex,
|
||||||
|
truncate
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { LoadingSpinner } from './LoadingSpinner'
|
import { LoadingSpinner } from './LoadingSpinner'
|
||||||
import { ZapPopUp } from './Zap'
|
import { ZapPopUp } from './Zap'
|
||||||
@ -575,3 +576,33 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ProfileLink = ({ pubkey }: Props) => {
|
||||||
|
let hexPubkey: string | null = null
|
||||||
|
let profileRoute: string | undefined = appRoutes.home
|
||||||
|
let nprofile: string | undefined
|
||||||
|
const npub = hexToNpub(pubkey)
|
||||||
|
|
||||||
|
try {
|
||||||
|
hexPubkey = npubToHex(pubkey)
|
||||||
|
|
||||||
|
if (hexPubkey) {
|
||||||
|
nprofile = hexPubkey
|
||||||
|
? nip19.nprofileEncode({
|
||||||
|
pubkey: hexPubkey
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently ignore
|
||||||
|
log(true, LogType.Error, 'Failed to encode profile.', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
profileRoute = nprofile ? getProfilePageRoute(nprofile) : appRoutes.home
|
||||||
|
const profile = useProfile(hexPubkey!, {
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayName = profile?.displayName || profile?.name || truncate(npub)
|
||||||
|
return <Link to={profileRoute}>@{displayName}</Link>
|
||||||
|
}
|
||||||
|
@ -3,7 +3,12 @@ import { formatDate } from 'date-fns'
|
|||||||
import { useDidMount, useNDKContext } from 'hooks'
|
import { useDidMount, useNDKContext } from 'hooks'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useParams, useLocation, Link } from 'react-router-dom'
|
import { useParams, useLocation, Link } from 'react-router-dom'
|
||||||
import { getModPageRoute, getBlogPageRoute, getProfilePageRoute } from 'routes'
|
import {
|
||||||
|
getModPageRoute,
|
||||||
|
getBlogPageRoute,
|
||||||
|
getProfilePageRoute,
|
||||||
|
appRoutes
|
||||||
|
} from 'routes'
|
||||||
import { CommentEvent, UserProfile } from 'types'
|
import { CommentEvent, UserProfile } from 'types'
|
||||||
import { hexToNpub } from 'utils'
|
import { hexToNpub } from 'utils'
|
||||||
import { Reactions } from './Reactions'
|
import { Reactions } from './Reactions'
|
||||||
@ -20,12 +25,15 @@ export const Comment = ({ comment }: CommentProps) => {
|
|||||||
const { ndk } = useNDKContext()
|
const { ndk } = useNDKContext()
|
||||||
const isMod = location.pathname.includes('/mod/')
|
const isMod = location.pathname.includes('/mod/')
|
||||||
const isBlog = location.pathname.includes('/blog/')
|
const isBlog = location.pathname.includes('/blog/')
|
||||||
|
const isNote = location.pathname.includes('/feed')
|
||||||
const baseUrl = naddr
|
const baseUrl = naddr
|
||||||
? isMod
|
? isMod
|
||||||
? getModPageRoute(naddr)
|
? getModPageRoute(naddr)
|
||||||
: isBlog
|
: isBlog
|
||||||
? getBlogPageRoute(naddr)
|
? getBlogPageRoute(naddr)
|
||||||
: undefined
|
: undefined
|
||||||
|
: isNote
|
||||||
|
? `${appRoutes.feed}/`
|
||||||
: undefined
|
: undefined
|
||||||
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
||||||
const [profile, setProfile] = useState<UserProfile>()
|
const [profile, setProfile] = useState<UserProfile>()
|
||||||
@ -121,35 +129,29 @@ export const Comment = ({ comment }: CommentProps) => {
|
|||||||
{typeof profile?.lud16 !== 'undefined' && profile.lud16 !== '' && (
|
{typeof profile?.lud16 !== 'undefined' && profile.lud16 !== '' && (
|
||||||
<Zap {...comment.event.rawEvent()} />
|
<Zap {...comment.event.rawEvent()} />
|
||||||
)}
|
)}
|
||||||
{comment.event.kind === NDKKind.GenericReply && (
|
<Link
|
||||||
<>
|
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||||
<Link
|
to={baseUrl + comment.event.encode()}
|
||||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
>
|
||||||
to={baseUrl + comment.event.encode()}
|
<svg
|
||||||
>
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
<svg
|
viewBox='0 0 512 512'
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
width='1em'
|
||||||
viewBox='0 0 512 512'
|
height='1em'
|
||||||
width='1em'
|
fill='currentColor'
|
||||||
height='1em'
|
className='IBMSMSMBSSCL_CAElementIcon'
|
||||||
fill='currentColor'
|
>
|
||||||
className='IBMSMSMBSSCL_CAElementIcon'
|
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||||
>
|
</svg>
|
||||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
<p className='IBMSMSMBSSCL_CAElementText'>{commentEvents.length}</p>
|
||||||
</svg>
|
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
</Link>
|
||||||
{commentEvents.length}
|
<Link
|
||||||
</p>
|
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
to={baseUrl + comment.event.encode()}
|
||||||
</Link>
|
>
|
||||||
<Link
|
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
</Link>
|
||||||
to={baseUrl + comment.event.encode()}
|
|
||||||
>
|
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
import { useTextLimit } from 'hooks/useTextLimit'
|
import { NoteRender } from 'components/Notes/NoteRender'
|
||||||
|
import { useTextLimit } from 'hooks'
|
||||||
|
|
||||||
interface CommentContentProps {
|
interface CommentContentProps {
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommentContent = ({ content }: CommentContentProps) => {
|
export const CommentContent = ({ content }: CommentContentProps) => {
|
||||||
const { text, isTextOverflowing, isExpanded, toggle } = useTextLimit(content)
|
const { text, isTextOverflowing, isExpanded, toggle } = useTextLimit(content)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className='IBMSMSMBSSCL_CBText'>{text}</p>
|
{isExpanded && (
|
||||||
|
<div
|
||||||
|
className='IBMSMSMBSSCL_CBExpand IBMSMSMBSSCL_CBExpandAlt'
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<p>Hide full post</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className='IBMSMSMBSSCL_CBText'>
|
||||||
|
<NoteRender content={text} />
|
||||||
|
</p>
|
||||||
{isTextOverflowing && (
|
{isTextOverflowing && (
|
||||||
<div className='IBMSMSMBSSCL_CBExpand' onClick={toggle}>
|
<div className='IBMSMSMBSSCL_CBExpand' onClick={toggle}>
|
||||||
<p>{isExpanded ? 'Hide' : 'View'} full post</p>
|
<p>{isExpanded ? 'Hide' : 'View'} full post</p>
|
||||||
|
@ -9,13 +9,17 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
useParams
|
useParams
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { getBlogPageRoute, getModPageRoute, getProfilePageRoute } from 'routes'
|
import {
|
||||||
|
appRoutes,
|
||||||
|
getBlogPageRoute,
|
||||||
|
getModPageRoute,
|
||||||
|
getProfilePageRoute
|
||||||
|
} from 'routes'
|
||||||
import { CommentEvent, UserProfile } from 'types'
|
import { CommentEvent, UserProfile } from 'types'
|
||||||
import { CommentsLoaderResult } from 'types/comments'
|
import { CommentsLoaderResult } from 'types/comments'
|
||||||
import { adjustTextareaHeight, handleCommentSubmit, hexToNpub } from 'utils'
|
import { adjustTextareaHeight, handleCommentSubmit, hexToNpub } from 'utils'
|
||||||
import { Reactions } from './Reactions'
|
import { Reactions } from './Reactions'
|
||||||
import { Zap } from './Zap'
|
import { Zap } from './Zap'
|
||||||
import { NDKKind } from '@nostr-dev-kit/ndk'
|
|
||||||
import { Comment } from './Comment'
|
import { Comment } from './Comment'
|
||||||
import { useComments } from 'hooks/useComments'
|
import { useComments } from 'hooks/useComments'
|
||||||
import { CommentContent } from './CommentContent'
|
import { CommentContent } from './CommentContent'
|
||||||
@ -28,14 +32,16 @@ export const CommentsPopup = () => {
|
|||||||
useBodyScrollDisable(true)
|
useBodyScrollDisable(true)
|
||||||
const isMod = location.pathname.includes('/mod/')
|
const isMod = location.pathname.includes('/mod/')
|
||||||
const isBlog = location.pathname.includes('/blog/')
|
const isBlog = location.pathname.includes('/blog/')
|
||||||
|
const isNote = location.pathname.includes('/feed')
|
||||||
const baseUrl = naddr
|
const baseUrl = naddr
|
||||||
? isMod
|
? isMod
|
||||||
? getModPageRoute(naddr)
|
? getModPageRoute(naddr)
|
||||||
: isBlog
|
: isBlog
|
||||||
? getBlogPageRoute(naddr)
|
? getBlogPageRoute(naddr)
|
||||||
: undefined
|
: undefined
|
||||||
|
: isNote
|
||||||
|
? `${appRoutes.feed}/`
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const { event } = useLoaderData() as CommentsLoaderResult
|
const { event } = useLoaderData() as CommentsLoaderResult
|
||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
@ -244,28 +250,22 @@ export const CommentsPopup = () => {
|
|||||||
{typeof profile?.lud16 !== 'undefined' &&
|
{typeof profile?.lud16 !== 'undefined' &&
|
||||||
profile.lud16 !== '' && <Zap {...event.rawEvent()} />}
|
profile.lud16 !== '' && <Zap {...event.rawEvent()} />}
|
||||||
|
|
||||||
{event.kind === NDKKind.GenericReply && (
|
<span className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'>
|
||||||
<>
|
<svg
|
||||||
<span className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'>
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
<svg
|
viewBox='0 0 512 512'
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
width='1em'
|
||||||
viewBox='0 0 512 512'
|
height='1em'
|
||||||
width='1em'
|
fill='currentColor'
|
||||||
height='1em'
|
className='IBMSMSMBSSCL_CAElementIcon'
|
||||||
fill='currentColor'
|
>
|
||||||
className='IBMSMSMBSSCL_CAElementIcon'
|
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
||||||
>
|
</svg>
|
||||||
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
|
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||||
</svg>
|
{commentEvents.length}
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
</p>
|
||||||
{commentEvents.length}
|
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||||
</p>
|
</span>
|
||||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
|
||||||
Replies
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,3 +124,6 @@ export const FALLBACK_PROFILE_IMAGE =
|
|||||||
|
|
||||||
export const PROFILE_BLOG_FILTER_LIMIT = 20
|
export const PROFILE_BLOG_FILTER_LIMIT = 20
|
||||||
export const MAX_VISIBLE_TEXT_PER_COMMENT = 500
|
export const MAX_VISIBLE_TEXT_PER_COMMENT = 500
|
||||||
|
export const CLIENT_NAME_VALUE = 'DEG Mods'
|
||||||
|
export const CLIENT_TAG_VALUE =
|
||||||
|
'31990:f4bf1fb5ba8be839f70c7331733e309f780822b311f63e01f9dc8abbb428f8d5:bf1987d6-b772-43c6-bce7-42b638a9ffed'
|
||||||
|
@ -11,7 +11,12 @@ import NDK, {
|
|||||||
zapInvoiceFromEvent
|
zapInvoiceFromEvent
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
|
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
|
||||||
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts'
|
import {
|
||||||
|
CLIENT_NAME_VALUE,
|
||||||
|
CLIENT_TAG_VALUE,
|
||||||
|
MOD_FILTER_LIMIT,
|
||||||
|
T_TAG_VALUE
|
||||||
|
} from 'constants.ts'
|
||||||
import { Dexie } from 'dexie'
|
import { Dexie } from 'dexie'
|
||||||
import { createContext, ReactNode, useEffect, useMemo } from 'react'
|
import { createContext, ReactNode, useEffect, useMemo } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
@ -127,6 +132,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
enableOutboxModel: true,
|
enableOutboxModel: true,
|
||||||
autoConnectUserRelays: true,
|
autoConnectUserRelays: true,
|
||||||
autoFetchUserMutelist: true,
|
autoFetchUserMutelist: true,
|
||||||
|
clientName: CLIENT_NAME_VALUE,
|
||||||
|
clientNip89: CLIENT_TAG_VALUE,
|
||||||
explicitRelayUrls: [
|
explicitRelayUrls: [
|
||||||
'wss://user.kindpag.es',
|
'wss://user.kindpag.es',
|
||||||
'wss://purplepag.es',
|
'wss://purplepag.es',
|
||||||
|
@ -12,3 +12,4 @@ export * from './useLocalStorage'
|
|||||||
export * from './useSessionStorage'
|
export * from './useSessionStorage'
|
||||||
export * from './useLocalCache'
|
export * from './useLocalCache'
|
||||||
export * from './useReplies'
|
export * from './useReplies'
|
||||||
|
export * from './useTextLimit'
|
||||||
|
@ -1,17 +1,52 @@
|
|||||||
import { MAX_VISIBLE_TEXT_PER_COMMENT } from '../constants'
|
import { MAX_VISIBLE_TEXT_PER_COMMENT } from '../constants'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
interface UseTextLimitResult {
|
||||||
|
text: string
|
||||||
|
isTextOverflowing: boolean
|
||||||
|
isExpanded: boolean
|
||||||
|
toggle: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export const useTextLimit = (
|
export const useTextLimit = (
|
||||||
text: string,
|
text: string,
|
||||||
limit: number = MAX_VISIBLE_TEXT_PER_COMMENT
|
limit: number = MAX_VISIBLE_TEXT_PER_COMMENT
|
||||||
) => {
|
): UseTextLimitResult => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false)
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
const isTextOverflowing = text.length > limit
|
|
||||||
const updated =
|
const getFilteredTextAndIndices = (): {
|
||||||
isExpanded || !isTextOverflowing ? text : text.slice(0, limit) + '…'
|
filteredText: string
|
||||||
|
indices: number[]
|
||||||
|
} => {
|
||||||
|
const words = text.split(' ')
|
||||||
|
const filteredWords: string[] = []
|
||||||
|
const indices: number[] = []
|
||||||
|
let currentIndex = 0
|
||||||
|
|
||||||
|
words.forEach((word) => {
|
||||||
|
if (!word.startsWith('nostr:')) {
|
||||||
|
filteredWords.push(word)
|
||||||
|
indices.push(currentIndex)
|
||||||
|
}
|
||||||
|
currentIndex += word.length + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return { filteredText: filteredWords.join(' '), indices }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filteredText, indices } = getFilteredTextAndIndices()
|
||||||
|
const isTextOverflowing = filteredText.length > limit
|
||||||
|
|
||||||
|
const getUpdatedText = (): string => {
|
||||||
|
if (isExpanded || !isTextOverflowing) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
const sliceEndIndex = indices.find((index) => index >= limit) || limit
|
||||||
|
return text.slice(0, sliceEndIndex) + '…'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: updated,
|
text: getUpdatedText(),
|
||||||
isTextOverflowing,
|
isTextOverflowing,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
toggle: () => setIsExpanded((prev) => !prev)
|
toggle: () => setIsExpanded((prev) => !prev)
|
||||||
|
@ -34,15 +34,6 @@ body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.2em;
|
font-size: 3.2em;
|
||||||
|
@ -6,15 +6,19 @@ import { log, LogType } from 'utils'
|
|||||||
export const commentsLoader =
|
export const commentsLoader =
|
||||||
(ndkContext: NDKContextType) =>
|
(ndkContext: NDKContextType) =>
|
||||||
async ({ params }: LoaderFunctionArgs) => {
|
async ({ params }: LoaderFunctionArgs) => {
|
||||||
const { nevent } = params
|
const { nevent, note } = params
|
||||||
|
const target = nevent || note
|
||||||
if (!nevent) {
|
if (!target) {
|
||||||
log(true, LogType.Error, 'Required nevent.')
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
'Missing event parameter in the URL (nevent, note).'
|
||||||
|
)
|
||||||
return redirect('..')
|
return redirect('..')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const replyEvent = await ndkContext.ndk.fetchEvent(nevent)
|
const replyEvent = await ndkContext.ndk.fetchEvent(target)
|
||||||
|
|
||||||
if (!replyEvent) {
|
if (!replyEvent) {
|
||||||
throw new Error('We are unable to find the comment on the relays')
|
throw new Error('We are unable to find the comment on the relays')
|
||||||
|
@ -29,12 +29,91 @@ export const FeedTabBlogs = () => {
|
|||||||
const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true)
|
const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true)
|
||||||
const [showing, setShowing] = useState(SHOWING_STEP)
|
const [showing, setShowing] = useState(SHOWING_STEP)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userPubkey) return
|
||||||
|
|
||||||
|
setIsFetching(true)
|
||||||
|
setIsLoadMoreVisible(true)
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
authors: [...followList, userPubkey],
|
||||||
|
kinds: [NDKKind.Article],
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||||
|
filter['#L'] = ['content-warning']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterOptions.source === window.location.host) {
|
||||||
|
filter['#r'] = [window.location.host]
|
||||||
|
}
|
||||||
|
|
||||||
|
ndk
|
||||||
|
.fetchEvents(filter, {
|
||||||
|
closeOnEose: true,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
const ndkEvents = Array.from(ndkEventSet)
|
||||||
|
setBlogs(ndkEvents.map(extractBlogCardDetails))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsFetching(false)
|
||||||
|
})
|
||||||
|
}, [filterOptions.nsfw, filterOptions.source, followList, ndk, userPubkey])
|
||||||
|
|
||||||
|
const filteredBlogs = useMemo(() => {
|
||||||
|
let _blogs = blogs || []
|
||||||
|
|
||||||
|
// Add nsfw tag to blogs included in nsfwList
|
||||||
|
if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) {
|
||||||
|
_blogs = _blogs.map((b) => {
|
||||||
|
return !b.nsfw && b.aTag && nsfwList.includes(b.aTag)
|
||||||
|
? { ...b, nsfw: true }
|
||||||
|
: b
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Filter nsfw (Hide_NSFW option)
|
||||||
|
_blogs = _blogs.filter(
|
||||||
|
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
|
||||||
|
)
|
||||||
|
|
||||||
|
_blogs = _blogs.filter(
|
||||||
|
(b) =>
|
||||||
|
!muteLists.admin.authors.includes(b.author!) &&
|
||||||
|
!muteLists.admin.replaceableEvents.includes(b.aTag!)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filterOptions.sort === SortBy.Latest) {
|
||||||
|
_blogs.sort((a, b) =>
|
||||||
|
a.published_at && b.published_at ? b.published_at - a.published_at : 0
|
||||||
|
)
|
||||||
|
} else if (filterOptions.sort === SortBy.Oldest) {
|
||||||
|
_blogs.sort((a, b) =>
|
||||||
|
a.published_at && b.published_at ? a.published_at - b.published_at : 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
showing > 0 && _blogs.splice(showing)
|
||||||
|
return _blogs
|
||||||
|
}, [
|
||||||
|
blogs,
|
||||||
|
filterOptions.nsfw,
|
||||||
|
filterOptions.sort,
|
||||||
|
muteLists.admin.authors,
|
||||||
|
muteLists.admin.replaceableEvents,
|
||||||
|
nsfwList,
|
||||||
|
showing
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!userPubkey) return null
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
const LOAD_MORE_STEP = SHOWING_STEP * 2
|
const LOAD_MORE_STEP = SHOWING_STEP * 2
|
||||||
setShowing((prev) => prev + SHOWING_STEP)
|
setShowing((prev) => prev + SHOWING_STEP)
|
||||||
const lastBlog = filteredBlogs[filteredBlogs.length - 1]
|
const lastBlog = filteredBlogs[filteredBlogs.length - 1]
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
authors: [...followList],
|
authors: [...followList, userPubkey],
|
||||||
kinds: [NDKKind.Article],
|
kinds: [NDKKind.Article],
|
||||||
limit: LOAD_MORE_STEP
|
limit: LOAD_MORE_STEP
|
||||||
}
|
}
|
||||||
@ -84,82 +163,6 @@ export const FeedTabBlogs = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsFetching(true)
|
|
||||||
setIsLoadMoreVisible(true)
|
|
||||||
const filter: NDKFilter = {
|
|
||||||
authors: [...followList],
|
|
||||||
kinds: [NDKKind.Article],
|
|
||||||
limit: 50
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
|
||||||
filter['#L'] = ['content-warning']
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterOptions.source === window.location.host) {
|
|
||||||
filter['#r'] = [window.location.host]
|
|
||||||
}
|
|
||||||
|
|
||||||
ndk
|
|
||||||
.fetchEvents(filter, {
|
|
||||||
closeOnEose: true,
|
|
||||||
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
|
||||||
})
|
|
||||||
.then((ndkEventSet) => {
|
|
||||||
const ndkEvents = Array.from(ndkEventSet)
|
|
||||||
setBlogs(ndkEvents.map(extractBlogCardDetails))
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsFetching(false)
|
|
||||||
})
|
|
||||||
}, [filterOptions.nsfw, filterOptions.source, followList, ndk])
|
|
||||||
|
|
||||||
const filteredBlogs = useMemo(() => {
|
|
||||||
let _blogs = blogs || []
|
|
||||||
|
|
||||||
// Add nsfw tag to blogs included in nsfwList
|
|
||||||
if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) {
|
|
||||||
_blogs = _blogs.map((b) => {
|
|
||||||
return !b.nsfw && b.aTag && nsfwList.includes(b.aTag)
|
|
||||||
? { ...b, nsfw: true }
|
|
||||||
: b
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Filter nsfw (Hide_NSFW option)
|
|
||||||
_blogs = _blogs.filter(
|
|
||||||
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
|
|
||||||
)
|
|
||||||
|
|
||||||
_blogs = _blogs.filter(
|
|
||||||
(b) =>
|
|
||||||
!muteLists.admin.authors.includes(b.author!) &&
|
|
||||||
!muteLists.admin.replaceableEvents.includes(b.aTag!)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filterOptions.sort === SortBy.Latest) {
|
|
||||||
_blogs.sort((a, b) =>
|
|
||||||
a.published_at && b.published_at ? b.published_at - a.published_at : 0
|
|
||||||
)
|
|
||||||
} else if (filterOptions.sort === SortBy.Oldest) {
|
|
||||||
_blogs.sort((a, b) =>
|
|
||||||
a.published_at && b.published_at ? a.published_at - b.published_at : 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
showing > 0 && _blogs.splice(showing)
|
|
||||||
return _blogs
|
|
||||||
}, [
|
|
||||||
blogs,
|
|
||||||
filterOptions.nsfw,
|
|
||||||
filterOptions.sort,
|
|
||||||
muteLists.admin.authors,
|
|
||||||
muteLists.admin.replaceableEvents,
|
|
||||||
nsfwList,
|
|
||||||
showing
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!userPubkey) return null
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFetching && (
|
{isFetching && (
|
||||||
|
@ -39,64 +39,13 @@ export const FeedTabMods = () => {
|
|||||||
const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true)
|
const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true)
|
||||||
const [showing, setShowing] = useState(SHOWING_STEP)
|
const [showing, setShowing] = useState(SHOWING_STEP)
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
|
||||||
const LOAD_MORE_STEP = SHOWING_STEP * 2
|
|
||||||
setShowing((prev) => prev + SHOWING_STEP)
|
|
||||||
const lastMod = filteredModList[filteredModList.length - 1]
|
|
||||||
const filter: NDKFilter = {
|
|
||||||
authors: [...followList],
|
|
||||||
kinds: [NDKKind.Classified],
|
|
||||||
limit: LOAD_MORE_STEP
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterOptions.source === window.location.host) {
|
|
||||||
filter['#r'] = [window.location.host]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterOptions.sort === SortBy.Latest) {
|
|
||||||
filter.until = lastMod.published_at
|
|
||||||
} else if (filterOptions.sort === SortBy.Oldest) {
|
|
||||||
filter.since = lastMod.published_at
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFetching(true)
|
|
||||||
ndk
|
|
||||||
.fetchEvents(filter, {
|
|
||||||
closeOnEose: true,
|
|
||||||
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
|
||||||
})
|
|
||||||
.then((ndkEventSet) => {
|
|
||||||
const ndkEvents = Array.from(ndkEventSet)
|
|
||||||
orderEventsChronologically(
|
|
||||||
ndkEvents,
|
|
||||||
filterOptions.sort === SortBy.Latest
|
|
||||||
)
|
|
||||||
setMods((prevMods) => {
|
|
||||||
const newMods = constructModListFromEvents(ndkEvents)
|
|
||||||
const combinedMods = [...prevMods, ...newMods]
|
|
||||||
const uniqueMods = Array.from(
|
|
||||||
new Set(combinedMods.map((mod) => mod.id))
|
|
||||||
)
|
|
||||||
.map((id) => combinedMods.find((mod) => mod.id === id))
|
|
||||||
.filter((mod): mod is ModDetails => mod !== undefined)
|
|
||||||
|
|
||||||
if (newMods.length < LOAD_MORE_STEP) {
|
|
||||||
setIsLoadMoreVisible(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueMods
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsFetching(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!userPubkey) return
|
||||||
|
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
setIsLoadMoreVisible(true)
|
setIsLoadMoreVisible(true)
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
authors: [...followList],
|
authors: [...followList, userPubkey],
|
||||||
kinds: [NDKKind.Classified],
|
kinds: [NDKKind.Classified],
|
||||||
limit: 50
|
limit: 50
|
||||||
}
|
}
|
||||||
@ -117,7 +66,7 @@ export const FeedTabMods = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetching(false)
|
setIsFetching(false)
|
||||||
})
|
})
|
||||||
}, [filterOptions.source, followList, ndk])
|
}, [filterOptions.source, followList, ndk, userPubkey])
|
||||||
|
|
||||||
const filteredModList = useMemo(() => {
|
const filteredModList = useMemo(() => {
|
||||||
const nsfwFilter = (mods: ModDetails[]) => {
|
const nsfwFilter = (mods: ModDetails[]) => {
|
||||||
@ -198,6 +147,60 @@ export const FeedTabMods = () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (!userPubkey) return null
|
if (!userPubkey) return null
|
||||||
|
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
const LOAD_MORE_STEP = SHOWING_STEP * 2
|
||||||
|
setShowing((prev) => prev + SHOWING_STEP)
|
||||||
|
const lastMod = filteredModList[filteredModList.length - 1]
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
authors: [...followList, userPubkey],
|
||||||
|
kinds: [NDKKind.Classified],
|
||||||
|
limit: LOAD_MORE_STEP
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterOptions.source === window.location.host) {
|
||||||
|
filter['#r'] = [window.location.host]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterOptions.sort === SortBy.Latest) {
|
||||||
|
filter.until = lastMod.published_at
|
||||||
|
} else if (filterOptions.sort === SortBy.Oldest) {
|
||||||
|
filter.since = lastMod.published_at
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetching(true)
|
||||||
|
ndk
|
||||||
|
.fetchEvents(filter, {
|
||||||
|
closeOnEose: true,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
const ndkEvents = Array.from(ndkEventSet)
|
||||||
|
orderEventsChronologically(
|
||||||
|
ndkEvents,
|
||||||
|
filterOptions.sort === SortBy.Latest
|
||||||
|
)
|
||||||
|
setMods((prevMods) => {
|
||||||
|
const newMods = constructModListFromEvents(ndkEvents)
|
||||||
|
const combinedMods = [...prevMods, ...newMods]
|
||||||
|
const uniqueMods = Array.from(
|
||||||
|
new Set(combinedMods.map((mod) => mod.id))
|
||||||
|
)
|
||||||
|
.map((id) => combinedMods.find((mod) => mod.id === id))
|
||||||
|
.filter((mod): mod is ModDetails => mod !== undefined)
|
||||||
|
|
||||||
|
if (newMods.length < LOAD_MORE_STEP) {
|
||||||
|
setIsLoadMoreVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueMods
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsFetching(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFetching && <LoadingSpinner desc='Fetching mod details from relays' />}
|
{isFetching && <LoadingSpinner desc='Fetching mod details from relays' />}
|
||||||
|
@ -1,3 +1,161 @@
|
|||||||
|
import { useLoaderData } from 'react-router-dom'
|
||||||
|
import { FeedPageLoaderResult } from './loader'
|
||||||
|
import { useAppSelector, useLocalStorage, useNDKContext } from 'hooks'
|
||||||
|
import { FilterOptions, NSFWFilter } from 'types'
|
||||||
|
import { DEFAULT_FILTER_OPTIONS } from 'utils'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
|
import {
|
||||||
|
NDKEvent,
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKSubscriptionCacheUsage
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
|
import { NoteSubmit } from 'components/Notes/NoteSubmit'
|
||||||
|
import { Note } from 'components/Notes/Note'
|
||||||
|
|
||||||
export const FeedTabPosts = () => {
|
export const FeedTabPosts = () => {
|
||||||
return <>WIP: Posts</>
|
const SHOWING_STEP = 20
|
||||||
|
const { followList } = useLoaderData() as FeedPageLoaderResult
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
const userPubkey = userState.user?.pubkey as string | undefined
|
||||||
|
const filterKey = 'filter-feed-2'
|
||||||
|
const [filterOptions] = useLocalStorage<FilterOptions>(filterKey, {
|
||||||
|
...DEFAULT_FILTER_OPTIONS
|
||||||
|
})
|
||||||
|
const { ndk } = useNDKContext()
|
||||||
|
const [notes, setNotes] = useState<NDKEvent[]>([])
|
||||||
|
const [isFetching, setIsFetching] = useState(false)
|
||||||
|
const [isLoadMoreVisible, setIsLoadMoreVisible] = useState(true)
|
||||||
|
const [showing, setShowing] = useState(SHOWING_STEP)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userPubkey) return
|
||||||
|
|
||||||
|
setIsFetching(true)
|
||||||
|
setIsLoadMoreVisible(true)
|
||||||
|
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
authors: [...followList, userPubkey],
|
||||||
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
ndk
|
||||||
|
.fetchEvents(filter, {
|
||||||
|
closeOnEose: true,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
const ndkEvents = Array.from(ndkEventSet)
|
||||||
|
setNotes(ndkEvents)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsFetching(false)
|
||||||
|
})
|
||||||
|
}, [followList, ndk, userPubkey])
|
||||||
|
|
||||||
|
const filteredNotes = useMemo(() => {
|
||||||
|
let _notes = notes || []
|
||||||
|
|
||||||
|
// Filter nsfw (Hide_NSFW option)
|
||||||
|
_notes = _notes.filter(
|
||||||
|
(n) =>
|
||||||
|
!(
|
||||||
|
filterOptions.nsfw === NSFWFilter.Hide_NSFW &&
|
||||||
|
n.tagValue('L') === 'content-warning'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter source
|
||||||
|
_notes = _notes.filter(
|
||||||
|
(n) =>
|
||||||
|
!(
|
||||||
|
filterOptions.source === window.location.host &&
|
||||||
|
n
|
||||||
|
.getMatchingTags('l')
|
||||||
|
.some((l) => l[1] === window.location.host && l[2] === 'source')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter reply events
|
||||||
|
_notes = _notes.filter((n) => n.getMatchingTags('e').length === 0)
|
||||||
|
|
||||||
|
_notes = _notes.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||||
|
|
||||||
|
showing > 0 && _notes.splice(showing)
|
||||||
|
return _notes
|
||||||
|
}, [filterOptions.nsfw, filterOptions.source, notes, showing])
|
||||||
|
|
||||||
|
if (!userPubkey) return null
|
||||||
|
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
const LOAD_MORE_STEP = SHOWING_STEP * 2
|
||||||
|
setShowing((prev) => prev + SHOWING_STEP)
|
||||||
|
const lastNote = filteredNotes[filteredNotes.length - 1]
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
authors: [...followList, userPubkey],
|
||||||
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
|
limit: LOAD_MORE_STEP
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.until = lastNote.created_at
|
||||||
|
|
||||||
|
setIsFetching(true)
|
||||||
|
ndk
|
||||||
|
.fetchEvents(filter, {
|
||||||
|
closeOnEose: true,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
setNotes((prevNotes) => {
|
||||||
|
const newNotes = Array.from(ndkEventSet)
|
||||||
|
const combinedNotes = [...prevNotes, ...newNotes]
|
||||||
|
const uniqueBlogs = Array.from(
|
||||||
|
new Set(combinedNotes.map((b) => b.id))
|
||||||
|
)
|
||||||
|
.map((id) => combinedNotes.find((b) => b.id === id))
|
||||||
|
.filter((b) => b !== undefined)
|
||||||
|
|
||||||
|
if (newNotes.length < LOAD_MORE_STEP) {
|
||||||
|
setIsLoadMoreVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueBlogs
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsFetching(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NoteSubmit />
|
||||||
|
{isFetching && <LoadingSpinner desc='Fetching notes from relays' />}
|
||||||
|
{filteredNotes.length === 0 && !isFetching && (
|
||||||
|
<div className='IBMSMListFeedNoPosts'>
|
||||||
|
<p>You aren't following people (or there are no posts to show)</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='IBMSMSplitMainFullSideSec IBMSMSMFSSContent'>
|
||||||
|
<div className='IBMSMSMFSSContentPosts'>
|
||||||
|
{filteredNotes.map((note) => (
|
||||||
|
<Note key={note.id} ndkEvent={note} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isFetching && isLoadMoreVisible && filteredNotes.length > 0 && (
|
||||||
|
<div className='IBMSMListFeedLoadMore'>
|
||||||
|
<button
|
||||||
|
className='btn btnMain IBMSMListFeedLoadMoreBtn'
|
||||||
|
type='button'
|
||||||
|
onClick={handleLoadMore}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
82
src/pages/feed/action.ts
Normal file
82
src/pages/feed/action.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'
|
||||||
|
import { NDKContextType } from 'contexts/NDKContext'
|
||||||
|
import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { getFeedNotePageRoute } from 'routes'
|
||||||
|
import { store } from 'store'
|
||||||
|
import { NoteSubmitForm, NoteSubmitFormErrors } from 'types'
|
||||||
|
import { log, LogType, now } from 'utils'
|
||||||
|
|
||||||
|
export const feedPostRouteAction =
|
||||||
|
(ndkContext: NDKContextType) =>
|
||||||
|
async ({ request }: ActionFunctionArgs) => {
|
||||||
|
const userState = store.getState().user
|
||||||
|
let hexPubkey: string
|
||||||
|
if (userState.auth && userState.user?.pubkey) {
|
||||||
|
hexPubkey = userState.user.pubkey as string
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
log(true, LogType.Error, 'Failed to get public key.', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error('Failed to get public key.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hexPubkey) {
|
||||||
|
toast.error('Could not get pubkey')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSubmit = (await request.json()) as NoteSubmitForm
|
||||||
|
const formErrors = validateFormData(formSubmit)
|
||||||
|
|
||||||
|
if (Object.keys(formErrors).length) return formErrors
|
||||||
|
|
||||||
|
const content = decodeURIComponent(formSubmit.content!)
|
||||||
|
const currentTimeStamp = now()
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(ndkContext.ndk, {
|
||||||
|
kind: NDKKind.Text,
|
||||||
|
created_at: currentTimeStamp,
|
||||||
|
content: content,
|
||||||
|
tags: [
|
||||||
|
['L', 'source'],
|
||||||
|
['l', window.location.host, 'source']
|
||||||
|
],
|
||||||
|
pubkey: hexPubkey
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (formSubmit.nsfw) ndkEvent.tags.push(['L', 'content-warning'])
|
||||||
|
|
||||||
|
await ndkEvent.sign()
|
||||||
|
const note1 = ndkEvent.encode()
|
||||||
|
const publishedOnRelays = await ndkEvent.publish()
|
||||||
|
if (publishedOnRelays.size === 0) {
|
||||||
|
toast.error('Failed to publish note on any relay')
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
toast.success('Note published successfully')
|
||||||
|
return redirect(getFeedNotePageRoute(note1))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(true, LogType.Error, 'Failed to publish note', error)
|
||||||
|
toast.error('Failed to publish note')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFormData = (formSubmit: NoteSubmitForm): NoteSubmitFormErrors => {
|
||||||
|
const errors: NoteSubmitFormErrors = {}
|
||||||
|
|
||||||
|
if (!formSubmit.content.trim()) {
|
||||||
|
errors.content = 'Content is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
@ -4,9 +4,12 @@ import { FeedTabBlogs } from './FeedTabBlogs'
|
|||||||
import { FeedTabMods } from './FeedTabMods'
|
import { FeedTabMods } from './FeedTabMods'
|
||||||
import { FeedTabPosts } from './FeedTabPosts'
|
import { FeedTabPosts } from './FeedTabPosts'
|
||||||
import { FeedFilter } from 'components/Filters/FeedFilter'
|
import { FeedFilter } from 'components/Filters/FeedFilter'
|
||||||
|
import { Outlet, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
export const FeedPage = () => {
|
export const FeedPage = () => {
|
||||||
const [tab, setTab] = useState(0)
|
const { note } = useParams()
|
||||||
|
// Open posts tab if note is present
|
||||||
|
const [tab, setTab] = useState(note ? 2 : 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -17,6 +20,8 @@ export const FeedPage = () => {
|
|||||||
{tab === 0 && <FeedTabMods />}
|
{tab === 0 && <FeedTabMods />}
|
||||||
{tab === 1 && <FeedTabBlogs />}
|
{tab === 1 && <FeedTabBlogs />}
|
||||||
{tab === 2 && <FeedTabPosts />}
|
{tab === 2 && <FeedTabPosts />}
|
||||||
|
|
||||||
|
<Outlet key={note} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import { BackupPage } from 'pages/backup'
|
|||||||
import { SupportersPage } from 'pages/supporters'
|
import { SupportersPage } from 'pages/supporters'
|
||||||
import { commentsLoader } from 'loaders/comment'
|
import { commentsLoader } from 'loaders/comment'
|
||||||
import { CommentsPopup } from 'components/comment/CommentsPopup'
|
import { CommentsPopup } from 'components/comment/CommentsPopup'
|
||||||
|
import { feedPostRouteAction } from 'pages/feed/action'
|
||||||
|
|
||||||
export const appRoutes = {
|
export const appRoutes = {
|
||||||
home: '/',
|
home: '/',
|
||||||
@ -56,6 +57,7 @@ export const appRoutes = {
|
|||||||
settingsAdmin: '/settings-admin',
|
settingsAdmin: '/settings-admin',
|
||||||
profile: '/profile/:nprofile?',
|
profile: '/profile/:nprofile?',
|
||||||
feed: '/feed',
|
feed: '/feed',
|
||||||
|
note: '/feed/:note',
|
||||||
notifications: '/notifications',
|
notifications: '/notifications',
|
||||||
backup: '/backup',
|
backup: '/backup',
|
||||||
supporters: '/supporters'
|
supporters: '/supporters'
|
||||||
@ -76,6 +78,9 @@ export const getBlogPageRoute = (eventId: string) =>
|
|||||||
export const getProfilePageRoute = (nprofile: string) =>
|
export const getProfilePageRoute = (nprofile: string) =>
|
||||||
appRoutes.profile.replace(':nprofile', nprofile)
|
appRoutes.profile.replace(':nprofile', nprofile)
|
||||||
|
|
||||||
|
export const getFeedNotePageRoute = (note: string) =>
|
||||||
|
appRoutes.note.replace(':note', note)
|
||||||
|
|
||||||
export const routerWithNdkContext = (context: NDKContextType) =>
|
export const routerWithNdkContext = (context: NDKContextType) =>
|
||||||
createBrowserRouter([
|
createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -199,7 +204,15 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
|||||||
{
|
{
|
||||||
path: appRoutes.feed,
|
path: appRoutes.feed,
|
||||||
element: <FeedPage />,
|
element: <FeedPage />,
|
||||||
loader: feedPageLoader(context)
|
loader: feedPageLoader(context),
|
||||||
|
action: feedPostRouteAction(context),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':note',
|
||||||
|
element: <CommentsPopup />,
|
||||||
|
loader: commentsLoader(context)
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: appRoutes.notifications,
|
path: appRoutes.notifications,
|
||||||
|
@ -8,3 +8,4 @@ export * from './category'
|
|||||||
export * from './popup'
|
export * from './popup'
|
||||||
export * from './errors'
|
export * from './errors'
|
||||||
export * from './comments'
|
export * from './comments'
|
||||||
|
export * from './note'
|
||||||
|
6
src/types/note.ts
Normal file
6
src/types/note.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface NoteSubmitForm {
|
||||||
|
content: string
|
||||||
|
nsfw: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoteSubmitFormErrors extends Partial<NoteSubmitForm> {}
|
@ -6,4 +6,6 @@ export interface AlertPopupProps extends PopupProps {
|
|||||||
header: string
|
header: string
|
||||||
label: string
|
label: string
|
||||||
handleConfirm: (confirm: boolean) => void
|
handleConfirm: (confirm: boolean) => void
|
||||||
|
yesButtonLabel?: string | undefined
|
||||||
|
noButtonLabel?: string | undefined
|
||||||
}
|
}
|
||||||
|
@ -273,3 +273,9 @@ export const normalizeUserSearchString = (str: string): string => {
|
|||||||
str = removeAccents(str)
|
str = removeAccents(str)
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const truncate = (npub: string): string => {
|
||||||
|
const start = npub.substring(0, 4)
|
||||||
|
const end = npub.substring(npub.length - 4, npub.length)
|
||||||
|
return start + '…' + end
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user