Adv. comments #207
157
src/components/comment/Comment.tsx
Normal file
157
src/components/comment/Comment.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { formatDate } from 'date-fns'
|
||||
import { useDidMount, useNDKContext } from 'hooks'
|
||||
import { useState } from 'react'
|
||||
import { useParams, useLocation, Link } from 'react-router-dom'
|
||||
import { getModPageRoute, getBlogPageRoute, getProfilePageRoute } from 'routes'
|
||||
import { CommentEvent, UserProfile } from 'types'
|
||||
import { hexToNpub } from 'utils'
|
||||
import { Reactions } from './Reactions'
|
||||
import { Zap } from './Zap'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { CommentContent } from './CommentContent'
|
||||
|
||||
interface CommentProps {
|
||||
comment: CommentEvent
|
||||
}
|
||||
export const Comment = ({ comment }: CommentProps) => {
|
||||
const { naddr } = useParams()
|
||||
const location = useLocation()
|
||||
const { ndk } = useNDKContext()
|
||||
const isMod = location.pathname.includes('/mod/')
|
||||
const isBlog = location.pathname.includes('/blog/')
|
||||
const baseUrl = naddr
|
||||
? isMod
|
||||
? getModPageRoute(naddr)
|
||||
: isBlog
|
||||
? getBlogPageRoute(naddr)
|
||||
: undefined
|
||||
: undefined
|
||||
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(() => {
|
||||
comment.event.author.fetchProfile().then((res) => setProfile(res))
|
||||
ndk
|
||||
.fetchEvents({
|
||||
kinds: [NDKKind.Text, NDKKind.GenericReply],
|
||||
'#e': [comment.event.id]
|
||||
})
|
||||
.then((ndkEventsSet) => {
|
||||
setCommentEvents(
|
||||
Array.from(ndkEventsSet).map((ndkEvent) => ({
|
||||
event: ndkEvent
|
||||
}))
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: comment.event.pubkey
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<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(comment.event.pubkey)}
|
||||
</Link>
|
||||
</div>
|
||||
{comment.event.created_at && (
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(comment.event.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(comment.event.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
{comment.status && (
|
||||
<p className='IBMSMSMBSSCL_CBTextStatus'>
|
||||
<span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span>
|
||||
{comment.status}
|
||||
</p>
|
||||
)}
|
||||
<CommentContent content={comment.event.content} />
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...comment.event.rawEvent()} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<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'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{typeof profile?.lud16 !== 'undefined' && profile.lud16 !== '' && (
|
||||
<Zap {...comment.event.rawEvent()} />
|
||||
)}
|
||||
{comment.event.kind === NDKKind.GenericReply && (
|
||||
<>
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||
to={baseUrl + comment.event.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 + comment.event.encode()}
|
||||
>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
18
src/components/comment/CommentContent.tsx
Normal file
18
src/components/comment/CommentContent.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useTextLimit } from 'hooks/useTextLimit'
|
||||
interface CommentContentProps {
|
||||
content: string
|
||||
}
|
||||
export const CommentContent = ({ content }: CommentContentProps) => {
|
||||
const { text, isTextOverflowing, isExpanded, toggle } = useTextLimit(content)
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='IBMSMSMBSSCL_CBText'>{text} </p>
|
||||
{isTextOverflowing && (
|
||||
<div className='IBMSMSMBSSCL_CBExpand' onClick={toggle}>
|
||||
<p>{isExpanded ? 'Hide' : 'View'} full post</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
41
src/components/comment/CommentForm.tsx
Normal file
41
src/components/comment/CommentForm.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
type CommentFormProps = {
|
||||
handleSubmit: (content: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
export const CommentForm = ({ handleSubmit }: CommentFormProps) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(commentText)
|
||||
if (submitted) setCommentText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsCreation'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box'
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
<button
|
||||
className='btnMain'
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Comment'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
279
src/components/comment/CommentsPopup.tsx
Normal file
279
src/components/comment/CommentsPopup.tsx
Normal file
@ -0,0 +1,279 @@
|
||||
import { formatDate } from 'date-fns'
|
||||
import { useBodyScrollDisable, useNDKContext } from 'hooks'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Link,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams
|
||||
} from 'react-router-dom'
|
||||
import { getBlogPageRoute, getModPageRoute, getProfilePageRoute } from 'routes'
|
||||
import { UserProfile } from 'types'
|
||||
import { CommentsLoaderResult } from 'types/comments'
|
||||
import { adjustTextareaHeight, handleCommentSubmit, hexToNpub } from 'utils'
|
||||
import { Reactions } from './Reactions'
|
||||
import { Zap } from './Zap'
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { Comment } from './Comment'
|
||||
import { useComments } from 'hooks/useComments'
|
||||
import { CommentContent } from './CommentContent'
|
||||
|
||||
export const CommentsPopup = () => {
|
||||
const { naddr } = useParams()
|
||||
const location = useLocation()
|
||||
const { ndk } = useNDKContext()
|
||||
useBodyScrollDisable(true)
|
||||
const isMod = location.pathname.includes('/mod/')
|
||||
const isBlog = location.pathname.includes('/blog/')
|
||||
const baseUrl = naddr
|
||||
? isMod
|
||||
? getModPageRoute(naddr)
|
||||
: isBlog
|
||||
? getBlogPageRoute(naddr)
|
||||
: undefined
|
||||
: undefined
|
||||
|
||||
const { event, parents } = useLoaderData() as CommentsLoaderResult
|
||||
const isRoot = event.tagValue('a') === event.tagValue('A')
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
const { commentEvents, setCommentEvents } = useComments(
|
||||
event.author.pubkey,
|
||||
undefined,
|
||||
event.id
|
||||
)
|
||||
useEffect(() => {
|
||||
event.author.fetchProfile().then((res) => setProfile(res))
|
||||
}, [event.author])
|
||||
const profileRoute = useMemo(
|
||||
() =>
|
||||
getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: event.pubkey
|
||||
})
|
||||
),
|
||||
[event.pubkey]
|
||||
)
|
||||
|
||||
const replyEvent = parents.length > 0 ? parents[0] : undefined
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [replyText, setReplyText] = useState('')
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.currentTarget.value
|
||||
setReplyText(value)
|
||||
adjustTextareaHeight(e.currentTarget)
|
||||
}, [])
|
||||
|
||||
const handleSubmit = handleCommentSubmit(event, setCommentEvents, ndk)
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(replyText)
|
||||
if (submitted) setReplyText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
<div className='popUpMainCard'>
|
||||
<div className='popUpMainCardTop'>
|
||||
<div className='popUpMainCardTopInfo'>
|
||||
<h3>Comment replies</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='popUpMainCardTopClose'
|
||||
onClick={() => navigate('..')}
|
||||
>
|
||||
<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='popUpMainCardBottom'>
|
||||
<div className='pUMCB_PrimeComment'>
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopOther'>
|
||||
<div className='IBMSMSMBSSCL_CTO'>
|
||||
{replyEvent && (
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CTOLink'
|
||||
to={baseUrl + replyEvent.encode()}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-32 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CTOLinkIcon'
|
||||
>
|
||||
<path d='M447.1 256C447.1 273.7 433.7 288 416 288H109.3l105.4 105.4c12.5 12.5 12.5 32.75 0 45.25C208.4 444.9 200.2 448 192 448s-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25l160-160c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L109.3 224H416C433.7 224 447.1 238.3 447.1 256z'></path>
|
||||
</svg>
|
||||
</Link>
|
||||
)}
|
||||
<p className='IBMSMSMBSSCL_CTOText'>
|
||||
Reply Depth: <span>{parents.length}</span>
|
||||
</p>
|
||||
</div>
|
||||
{!isRoot && (
|
||||
<Link
|
||||
className='btn btnMain IBMSMSMBSSCL_CTOBtn'
|
||||
type='button'
|
||||
to={'..'}
|
||||
>
|
||||
Main Post
|
||||
</Link>
|
||||
)}
|
||||
</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(event.pubkey)}
|
||||
</Link>
|
||||
</div>
|
||||
{event.created_at && (
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(event.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(event.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
<CommentContent content={event.content} />
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...event.rawEvent()} />
|
||||
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<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'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{typeof profile?.lud16 !== 'undefined' &&
|
||||
profile.lud16 !== '' && <Zap {...event.rawEvent()} />}
|
||||
|
||||
{event.kind === NDKKind.GenericReply && (
|
||||
<>
|
||||
<span className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'>
|
||||
<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>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pUMCB_CommentToPrime'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box postSocialTextarea'
|
||||
placeholder='Got something to say?'
|
||||
value={replyText}
|
||||
onChange={handleChange}
|
||||
style={{ height: '0px' }}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
{/* <a className='IBMSMSMBSSCC_BottomButton'>Quote-Repost</a> */}
|
||||
<button
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
className='IBMSMSMBSSCC_BottomButton'
|
||||
>
|
||||
{isSubmitting ? 'Replying...' : 'Reply'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{commentEvents.length > 0 && (
|
||||
<>
|
||||
<h3 className='IBMSMSMBSSCL_CommentNoteRepliesTitle'>
|
||||
Replies
|
||||
</h3>
|
||||
<div className='pUMCB_RepliesToPrime'>
|
||||
{commentEvents.map((reply) => (
|
||||
<Comment key={reply.event.id} comment={reply} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
79
src/components/comment/Filter.tsx
Normal file
79
src/components/comment/Filter.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
import { AuthorFilterEnum, SortByEnum } from 'types'
|
||||
|
||||
export type FilterOptions = {
|
||||
sort: SortByEnum
|
||||
author: AuthorFilterEnum
|
||||
}
|
||||
|
||||
type FilterProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
export const Filter = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FilterProps) => {
|
||||
return (
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.author}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(AuthorFilterEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
author: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
68
src/components/comment/Reactions.tsx
Normal file
68
src/components/comment/Reactions.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { NostrEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Dots } from 'components/Spinner'
|
||||
import { useReactions } from 'hooks'
|
||||
|
||||
export const Reactions = (props: NostrEvent) => {
|
||||
const {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
handleReaction,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively
|
||||
} = useReactions({
|
||||
pubkey: props.pubkey,
|
||||
eTag: props.id!
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? likesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? disLikesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
76
src/components/comment/Zap.tsx
Normal file
76
src/components/comment/Zap.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { NostrEvent } from '@nostr-dev-kit/ndk'
|
||||
import { ZapPopUp } from 'components/Zap'
|
||||
import {
|
||||
useAppSelector,
|
||||
useNDKContext,
|
||||
useBodyScrollDisable,
|
||||
useDidMount
|
||||
} from 'hooks'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { abbreviateNumber } from 'utils'
|
||||
|
||||
export const Zap = (props: NostrEvent) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const { getTotalZapAmount } = useNDKContext()
|
||||
|
||||
useBodyScrollDisable(isOpen)
|
||||
|
||||
useDidMount(() => {
|
||||
getTotalZapAmount(
|
||||
props.pubkey,
|
||||
props.id!,
|
||||
undefined,
|
||||
userState.user?.pubkey as string
|
||||
)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(res.accumulatedZapAmount)
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt ${
|
||||
hasZapped ? 'IBMSMSMBSSCL_CAEBoltActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={props.pubkey}
|
||||
eventId={props.id}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
setTotalZapAmount={setTotalZappedAmount}
|
||||
setHasZapped={setHasZapped}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,48 +1,20 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { Dots, Spinner } from 'components/Spinner'
|
||||
import { ZapPopUp } from 'components/Zap'
|
||||
import { formatDate } from 'date-fns'
|
||||
import {
|
||||
useAppSelector,
|
||||
useBodyScrollDisable,
|
||||
useDidMount,
|
||||
useNDKContext,
|
||||
useReactions
|
||||
} from 'hooks'
|
||||
import { Spinner } from 'components/Spinner'
|
||||
import { useNDKContext } from 'hooks'
|
||||
import { useComments } from 'hooks/useComments'
|
||||
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { getProfilePageRoute } from 'routes'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
import {
|
||||
Addressable,
|
||||
AuthorFilterEnum,
|
||||
BlogPageLoaderResult,
|
||||
CommentEvent,
|
||||
CommentEventStatus,
|
||||
UserProfile
|
||||
} from 'types/index.ts'
|
||||
import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils'
|
||||
|
||||
enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest'
|
||||
}
|
||||
|
||||
enum AuthorFilterEnum {
|
||||
All_Comments = 'All Comments',
|
||||
Creator_Comments = 'Creator Comments'
|
||||
}
|
||||
|
||||
type FilterOptions = {
|
||||
sort: SortByEnum
|
||||
author: AuthorFilterEnum
|
||||
}
|
||||
ModPageLoaderResult,
|
||||
SortByEnum
|
||||
} from 'types'
|
||||
import { handleCommentSubmit } from 'utils'
|
||||
import { Filter, FilterOptions } from './Filter'
|
||||
import { CommentForm } from './CommentForm'
|
||||
import { Comment } from './Comment'
|
||||
|
||||
type Props = {
|
||||
addressable: Addressable
|
||||
@ -50,11 +22,14 @@ type Props = {
|
||||
}
|
||||
|
||||
export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
const { ndk, publish } = useNDKContext()
|
||||
const { ndk } = useNDKContext()
|
||||
const { commentEvents, setCommentEvents } = useComments(
|
||||
addressable.author,
|
||||
addressable.aTag
|
||||
)
|
||||
const { event } = useLoaderData() as
|
||||
| ModPageLoaderResult
|
||||
| BlogPageLoaderResult
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortByEnum.Latest,
|
||||
author: AuthorFilterEnum.All_Comments
|
||||
@ -73,121 +48,7 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
setCommentCount(commentEvents.length)
|
||||
}, [commentEvents, setCommentCount])
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const handleSubmit = async (content: string): Promise<boolean> => {
|
||||
if (content === '') return false
|
||||
|
||||
let pubkey: string | undefined
|
||||
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
pubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
pubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, `Could not get pubkey`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!pubkey) {
|
||||
toast.error('Could not get user pubkey')
|
||||
return false
|
||||
}
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
content: content,
|
||||
pubkey: pubkey,
|
||||
kind: kinds.ShortTextNote,
|
||||
created_at: now(),
|
||||
tags: [
|
||||
['e', addressable.id],
|
||||
['a', addressable.aTag],
|
||||
['p', addressable.author]
|
||||
]
|
||||
}
|
||||
|
||||
const signedEvent = await window.nostr
|
||||
?.signEvent(unsignedEvent)
|
||||
.then((event) => event as Event)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to sign the event!')
|
||||
log(true, LogType.Error, 'Failed to sign the event!', err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!signedEvent) return false
|
||||
|
||||
setCommentEvents((prev) => [
|
||||
{
|
||||
...signedEvent,
|
||||
status: CommentEventStatus.Publishing
|
||||
},
|
||||
...prev
|
||||
])
|
||||
|
||||
const ndkEvent = new NDKEvent(ndk, signedEvent)
|
||||
publish(ndkEvent)
|
||||
.then((publishedOnRelays) => {
|
||||
if (publishedOnRelays.length === 0) {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
return {
|
||||
...event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
} else {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
return {
|
||||
...event,
|
||||
status: CommentEventStatus.Published
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// when an event is successfully published remove the status from it after 15 seconds
|
||||
setTimeout(() => {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
delete event.status
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
}, 15000)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('An error occurred in publishing comment', err)
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((event) => {
|
||||
if (event.id === signedEvent.id) {
|
||||
return {
|
||||
...event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
const handleSubmit = handleCommentSubmit(event, setCommentEvents, ndk)
|
||||
|
||||
const handleDiscoveredClick = () => {
|
||||
setVisible(commentEvents)
|
||||
@ -203,14 +64,22 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
let filteredComments = visible
|
||||
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
|
||||
filteredComments = filteredComments.filter(
|
||||
(comment) => comment.pubkey === addressable.author
|
||||
(comment) => comment.event.pubkey === addressable.author
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortByEnum.Latest) {
|
||||
filteredComments.sort((a, b) => b.created_at - a.created_at)
|
||||
filteredComments.sort((a, b) =>
|
||||
a.event.created_at && b.event.created_at
|
||||
? b.event.created_at - a.event.created_at
|
||||
: 0
|
||||
)
|
||||
} else if (filterOptions.sort === SortByEnum.Oldest) {
|
||||
filteredComments.sort((a, b) => a.created_at - b.created_at)
|
||||
filteredComments.sort((a, b) =>
|
||||
a.event.created_at && b.event.created_at
|
||||
? a.event.created_at - b.event.created_at
|
||||
: 0
|
||||
)
|
||||
}
|
||||
|
||||
return filteredComments
|
||||
@ -241,363 +110,11 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
<div className='IBMSMSMBSSCommentsList'>
|
||||
{comments.map((event) => (
|
||||
<Comment key={event.id} {...event} />
|
||||
{comments.map((comment) => (
|
||||
<Comment key={comment.event.id} comment={comment} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type CommentFormProps = {
|
||||
handleSubmit: (content: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
const CommentForm = ({ handleSubmit }: CommentFormProps) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
|
||||
const handleComment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const submitted = await handleSubmit(commentText)
|
||||
if (submitted) setCommentText('')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMBSSCommentsCreation'>
|
||||
<div className='IBMSMSMBSSCC_Top'>
|
||||
<textarea
|
||||
className='IBMSMSMBSSCC_Top_Box'
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCC_Bottom'>
|
||||
<button
|
||||
className='btnMain'
|
||||
onClick={handleComment}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Comment'}
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FilterProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
const Filter = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FilterProps) => {
|
||||
return (
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.author}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(AuthorFilterEnum).map((item) => (
|
||||
<div
|
||||
key={`sortBy-${item}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
author: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const Comment = (props: CommentEvent) => {
|
||||
const { findMetadata } = useNDKContext()
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(() => {
|
||||
findMetadata(props.pubkey).then((res) => {
|
||||
setProfile(res)
|
||||
})
|
||||
})
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: props.pubkey
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<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(props.pubkey)}
|
||||
</Link>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
|
||||
<a className='IBMSMSMBSSCL_CADTime'>
|
||||
{formatDate(props.created_at * 1000, 'hh:mm aa')}{' '}
|
||||
</a>
|
||||
<a className='IBMSMSMBSSCL_CADDate'>
|
||||
{formatDate(props.created_at * 1000, 'dd/MM/yyyy')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentBottom'>
|
||||
{props.status && (
|
||||
<p className='IBMSMSMBSSCL_CBTextStatus'>
|
||||
<span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span>
|
||||
{props.status}
|
||||
</p>
|
||||
)}
|
||||
<p className='IBMSMSMBSSCL_CBText'>{props.content}</p>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentActions'>
|
||||
<div className='IBMSMSMBSSCL_CommentActionsInside'>
|
||||
<Reactions {...props} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<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'>0</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<Zap {...props} />
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<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'>0</p>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
|
||||
</div>
|
||||
<div
|
||||
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Reactions = (props: Event) => {
|
||||
const {
|
||||
isDataLoaded,
|
||||
likesCount,
|
||||
disLikesCount,
|
||||
handleReaction,
|
||||
hasReactedPositively,
|
||||
hasReactedNegatively
|
||||
} = useReactions({
|
||||
pubkey: props.pubkey,
|
||||
eTag: props.id
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
|
||||
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? likesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
|
||||
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
|
||||
}`}
|
||||
onClick={isDataLoaded ? () => handleReaction() : undefined}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{isDataLoaded ? disLikesCount : <Dots />}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Zap = (props: Event) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState(0)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const { getTotalZapAmount } = useNDKContext()
|
||||
|
||||
useBodyScrollDisable(isOpen)
|
||||
|
||||
useDidMount(() => {
|
||||
getTotalZapAmount(
|
||||
props.pubkey,
|
||||
props.id,
|
||||
undefined,
|
||||
userState.user?.pubkey as string
|
||||
)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(res.accumulatedZapAmount)
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt ${
|
||||
hasZapped ? 'IBMSMSMBSSCL_CAEBoltActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSSCL_CAElementIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
<p className='IBMSMSMBSSCL_CAElementText'>
|
||||
{abbreviateNumber(totalZappedAmount)}
|
||||
</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ZapPopUp
|
||||
title='Tip/Zap'
|
||||
receiver={props.pubkey}
|
||||
eventId={props.id}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
setTotalZapAmount={setTotalZappedAmount}
|
||||
setHasZapped={setHasZapped}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -123,3 +123,4 @@ export const FALLBACK_PROFILE_IMAGE =
|
||||
'https://image.nostr.build/a305f4b43f74af3c6dcda42e6a798105a56ac1e3e7b74d7bef171896b3ba7520.png'
|
||||
|
||||
export const PROFILE_BLOG_FILTER_LIMIT = 20
|
||||
export const MAX_VISIBLE_TEXT_PER_COMMENT = 500
|
||||
|
@ -13,14 +13,15 @@ import { useNDKContext } from './useNDKContext'
|
||||
|
||||
export const useComments = (
|
||||
author: string | undefined,
|
||||
aTag: string | undefined
|
||||
aTag: string | undefined,
|
||||
eTag?: string | undefined
|
||||
) => {
|
||||
const { ndk } = useNDKContext()
|
||||
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!(author && aTag)) {
|
||||
// Author and aTag are required
|
||||
if (!(author && (aTag || eTag))) {
|
||||
// Author and aTag/eTag are required
|
||||
return
|
||||
}
|
||||
|
||||
@ -48,8 +49,17 @@ export const useComments = (
|
||||
})
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.Text],
|
||||
'#a': [aTag]
|
||||
kinds: [NDKKind.Text, NDKKind.GenericReply],
|
||||
...(aTag
|
||||
? {
|
||||
'#a': [aTag]
|
||||
}
|
||||
: {}),
|
||||
...(eTag
|
||||
? {
|
||||
'#e': [eTag]
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
const relayUrls = new Set<string>()
|
||||
@ -73,21 +83,11 @@ export const useComments = (
|
||||
|
||||
subscription.on('event', (ndkEvent) => {
|
||||
setCommentEvents((prev) => {
|
||||
if (prev.find((e) => e.id === ndkEvent.id)) {
|
||||
if (prev.find((e) => e.event.id === ndkEvent.id)) {
|
||||
return [...prev]
|
||||
}
|
||||
|
||||
const commentEvent: CommentEvent = {
|
||||
kind: NDKKind.Text,
|
||||
tags: ndkEvent.tags,
|
||||
content: ndkEvent.content,
|
||||
created_at: ndkEvent.created_at!,
|
||||
pubkey: ndkEvent.pubkey,
|
||||
id: ndkEvent.id,
|
||||
sig: ndkEvent.sig!
|
||||
}
|
||||
|
||||
return [commentEvent, ...prev]
|
||||
return [{ event: ndkEvent }, ...prev]
|
||||
})
|
||||
})
|
||||
|
||||
@ -102,7 +102,7 @@ export const useComments = (
|
||||
subscription.stop()
|
||||
}
|
||||
}
|
||||
}, [aTag, author, ndk])
|
||||
}, [aTag, author, eTag, ndk])
|
||||
|
||||
return {
|
||||
commentEvents,
|
||||
|
19
src/hooks/useTextLimit.tsx
Normal file
19
src/hooks/useTextLimit.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { MAX_VISIBLE_TEXT_PER_COMMENT } from '../constants'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useTextLimit = (
|
||||
text: string,
|
||||
limit: number = MAX_VISIBLE_TEXT_PER_COMMENT
|
||||
) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const isTextOverflowing = text.length > limit
|
||||
const updated =
|
||||
isExpanded || !isTextOverflowing ? text : text.slice(0, limit) + '…'
|
||||
|
||||
return {
|
||||
text: updated,
|
||||
isTextOverflowing,
|
||||
isExpanded,
|
||||
toggle: () => setIsExpanded((prev) => !prev)
|
||||
}
|
||||
}
|
@ -22,10 +22,11 @@ import { npubToHex } from '../utils'
|
||||
import logo from '../assets/img/DEG Mods Logo With Text.svg'
|
||||
import placeholder from '../assets/img/DEG Mods Default PP.png'
|
||||
import { resetUserWot } from 'store/reducers/wot'
|
||||
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
export const Header = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { findMetadata } = useNDKContext()
|
||||
const { findMetadata, ndk } = useNDKContext()
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const revalidator = useRevalidator()
|
||||
// Track nostr-login extension modal open state
|
||||
@ -50,6 +51,7 @@ export const Header = () => {
|
||||
dispatch(setAuth(null))
|
||||
dispatch(setUser(null))
|
||||
dispatch(resetUserWot())
|
||||
ndk.signer = undefined
|
||||
} else {
|
||||
dispatch(
|
||||
setAuth({
|
||||
@ -63,6 +65,7 @@ export const Header = () => {
|
||||
pubkey: npubToHex(npub)!
|
||||
})
|
||||
)
|
||||
ndk.signer = new NDKNip07Signer()
|
||||
findMetadata(npub).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
dispatch(
|
||||
|
53
src/loaders/comment.ts
Normal file
53
src/loaders/comment.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { CommentsLoaderResult } from 'types/comments'
|
||||
import { log, LogType } from 'utils'
|
||||
|
||||
export const commentsLoader =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params }: LoaderFunctionArgs) => {
|
||||
const { nevent } = params
|
||||
|
||||
if (!nevent) {
|
||||
log(true, LogType.Error, 'Required nevent.')
|
||||
return redirect('..')
|
||||
}
|
||||
|
||||
try {
|
||||
const replyEvent = await ndkContext.ndk.fetchEvent(nevent)
|
||||
|
||||
if (!replyEvent) {
|
||||
throw new Error('We are unable to find the comment on the relays')
|
||||
}
|
||||
|
||||
const replies: NDKEvent[] = []
|
||||
let eTag: string | undefined = replyEvent.tagValue('e')
|
||||
while (eTag) {
|
||||
const prev = await ndkContext.ndk.fetchEvent({
|
||||
kinds: [NDKKind.Text, NDKKind.GenericReply],
|
||||
ids: [eTag]
|
||||
})
|
||||
if (prev) {
|
||||
replies.push(prev)
|
||||
eTag = prev.tagValue('e')
|
||||
} else {
|
||||
eTag = undefined
|
||||
}
|
||||
}
|
||||
const result: Partial<CommentsLoaderResult> = {
|
||||
event: replyEvent,
|
||||
parents: replies
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
let message = 'An error occurred in fetching comment from relays'
|
||||
log(true, LogType.Error, message, error)
|
||||
if (error instanceof Error) {
|
||||
message = error.message
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
return redirect('..')
|
||||
}
|
@ -3,7 +3,9 @@ import {
|
||||
useLoaderData,
|
||||
Link as ReactRouterLink,
|
||||
useNavigation,
|
||||
useSubmit
|
||||
useSubmit,
|
||||
Outlet,
|
||||
useParams
|
||||
} from 'react-router-dom'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ProfileSection } from 'components/ProfileSection'
|
||||
@ -30,6 +32,7 @@ const BLOG_REPORT_REASONS = [
|
||||
]
|
||||
|
||||
export const BlogPage = () => {
|
||||
const { nevent } = useParams()
|
||||
const { blog, latest, isAddedToNSFW, isBlocked, postWarning } =
|
||||
useLoaderData() as BlogPageLoaderResult
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
@ -312,6 +315,7 @@ export const BlogPage = () => {
|
||||
</>
|
||||
)}
|
||||
{!!blog?.author && <ProfileSection pubkey={blog.author} />}
|
||||
<Outlet key={nevent} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -94,6 +94,7 @@ export const blogRouteLoader =
|
||||
])
|
||||
const result: BlogPageLoaderResult = {
|
||||
blog: undefined,
|
||||
event: undefined,
|
||||
latest: [],
|
||||
isAddedToNSFW: false,
|
||||
isBlocked: false
|
||||
@ -102,6 +103,9 @@ export const blogRouteLoader =
|
||||
// Check the blog event result
|
||||
const fetchEventResult = settled[0]
|
||||
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
|
||||
// Save original event
|
||||
result.event = fetchEventResult.value
|
||||
|
||||
// Extract the blog details from the event
|
||||
result.blog = extractBlogDetails(fetchEventResult.value)
|
||||
} else if (fetchEventResult.status === 'rejected') {
|
||||
|
@ -2,6 +2,7 @@ import FsLightbox from 'fslightbox-react'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Outlet,
|
||||
Link as ReactRouterLink,
|
||||
useLoaderData,
|
||||
useNavigation,
|
||||
@ -66,7 +67,7 @@ export const ModPage = () => {
|
||||
const { mod, postWarning } = useLoaderData() as ModPageLoaderResult
|
||||
|
||||
// We can get author right away from naddr, no need to wait for mod data
|
||||
const { naddr } = useParams()
|
||||
const { naddr, nevent } = useParams()
|
||||
let author = mod?.author
|
||||
if (naddr && !author) {
|
||||
try {
|
||||
@ -143,6 +144,7 @@ export const ModPage = () => {
|
||||
{typeof author !== 'undefined' && (
|
||||
<ProfileSection pubkey={author} />
|
||||
)}
|
||||
<Outlet key={nevent} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -103,6 +103,7 @@ export const modRouteLoader =
|
||||
|
||||
const result: ModPageLoaderResult = {
|
||||
mod: undefined,
|
||||
event: undefined,
|
||||
latest: [],
|
||||
isAddedToNSFW: false,
|
||||
isBlocked: false,
|
||||
@ -112,6 +113,9 @@ export const modRouteLoader =
|
||||
// Check the mod event result
|
||||
const fetchEventResult = settled[0]
|
||||
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
|
||||
// Save original event
|
||||
result.event = fetchEventResult.value
|
||||
|
||||
// Extract the mod data from the event
|
||||
result.mod = extractModData(fetchEventResult.value)
|
||||
} else if (fetchEventResult.status === 'rejected') {
|
||||
|
@ -30,17 +30,19 @@ import { blogRouteAction } from '../pages/blog/action'
|
||||
import { reportRouteAction } from '../actions/report'
|
||||
import { BackupPage } from 'pages/backup'
|
||||
import { SupportersPage } from 'pages/supporters'
|
||||
import { commentsLoader } from 'loaders/comment'
|
||||
import { CommentsPopup } from 'components/comment/CommentsPopup'
|
||||
|
||||
export const appRoutes = {
|
||||
home: '/',
|
||||
games: '/games',
|
||||
game: '/game/:name',
|
||||
mods: '/mods',
|
||||
mod: '/mod/:naddr',
|
||||
mod: '/mod/:naddr/',
|
||||
modReport_actionOnly: '/mod/:naddr/report',
|
||||
about: '/about',
|
||||
blogs: '/blog',
|
||||
blog: '/blog/:naddr',
|
||||
blog: '/blog/:naddr/',
|
||||
blogEdit: '/blog/:naddr/edit',
|
||||
blogReport_actionOnly: '/blog/:naddr/report',
|
||||
submitMod: '/submit-mod',
|
||||
@ -98,6 +100,13 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
{
|
||||
path: appRoutes.mod,
|
||||
element: <ModPage />,
|
||||
children: [
|
||||
{
|
||||
path: ':nevent',
|
||||
element: <CommentsPopup />,
|
||||
loader: commentsLoader(context)
|
||||
}
|
||||
],
|
||||
loader: modRouteLoader(context),
|
||||
action: modRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
@ -118,6 +127,13 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
{
|
||||
path: appRoutes.blog,
|
||||
element: <BlogPage />,
|
||||
children: [
|
||||
{
|
||||
path: ':nevent',
|
||||
element: <CommentsPopup />,
|
||||
loader: commentsLoader(context)
|
||||
}
|
||||
],
|
||||
loader: blogRouteLoader(context),
|
||||
action: blogRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
|
@ -475,6 +475,8 @@ hover {
|
||||
}
|
||||
|
||||
.IBMSMSMBSSCL_CommentNoteRepliesTitle {
|
||||
width: 100%;
|
||||
margin: 0 0 15px 0;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { SortBy, NSFWFilter, ModeratedFilter } from './modsFilter'
|
||||
|
||||
export interface BlogForm {
|
||||
@ -36,6 +37,7 @@ export interface BlogCardDetails extends BlogDetails {
|
||||
|
||||
export interface BlogPageLoaderResult {
|
||||
blog: Partial<BlogDetails> | undefined
|
||||
event: NDKEvent | undefined
|
||||
latest: Partial<BlogDetails>[]
|
||||
isAddedToNSFW: boolean
|
||||
isBlocked: boolean
|
||||
|
16
src/types/comments.ts
Normal file
16
src/types/comments.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
export interface CommentsLoaderResult {
|
||||
event: NDKEvent
|
||||
parents: NDKEvent[]
|
||||
}
|
||||
|
||||
export enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest'
|
||||
}
|
||||
|
||||
export enum AuthorFilterEnum {
|
||||
All_Comments = 'All Comments',
|
||||
Creator_Comments = 'Creator Comments'
|
||||
}
|
@ -7,3 +7,4 @@ export * from './blog'
|
||||
export * from './category'
|
||||
export * from './popup'
|
||||
export * from './errors'
|
||||
export * from './comments'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Event } from 'nostr-tools'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { BlogDetails } from 'types'
|
||||
|
||||
export enum CommentEventStatus {
|
||||
@ -7,7 +7,8 @@ export enum CommentEventStatus {
|
||||
Failed = 'Failed to publish comment.'
|
||||
}
|
||||
|
||||
export interface CommentEvent extends Event {
|
||||
export interface CommentEvent {
|
||||
event: NDKEvent
|
||||
status?: CommentEventStatus
|
||||
}
|
||||
|
||||
@ -85,6 +86,7 @@ export interface MuteLists {
|
||||
|
||||
export interface ModPageLoaderResult {
|
||||
mod: ModDetails | undefined
|
||||
event: NDKEvent | undefined
|
||||
latest: Partial<BlogDetails>[]
|
||||
isAddedToNSFW: boolean
|
||||
isBlocked: boolean
|
||||
|
91
src/utils/comments.ts
Normal file
91
src/utils/comments.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import NDK, { NDKEvent, NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
import { toast } from 'react-toastify'
|
||||
import { CommentEvent, CommentEventStatus } from 'types'
|
||||
import { log, LogType } from './utils'
|
||||
|
||||
export function handleCommentSubmit(
|
||||
event: NDKEvent | undefined,
|
||||
setCommentEvents: React.Dispatch<React.SetStateAction<CommentEvent[]>>,
|
||||
ndk: NDK
|
||||
) {
|
||||
return async (content: string): Promise<boolean> => {
|
||||
if (content === '') return false
|
||||
|
||||
// NDKEvent required
|
||||
if (!event) return false
|
||||
|
||||
let id: string | undefined
|
||||
try {
|
||||
const reply = event.reply()
|
||||
reply.content = content.trim()
|
||||
|
||||
setCommentEvents((prev) => [
|
||||
{
|
||||
event: reply,
|
||||
status: CommentEventStatus.Publishing
|
||||
},
|
||||
...prev
|
||||
])
|
||||
if (!ndk.signer) {
|
||||
ndk.signer = new NDKNip07Signer()
|
||||
}
|
||||
await reply.sign(ndk.signer)
|
||||
id = reply.id
|
||||
const relaySet = await reply.publish()
|
||||
if (relaySet.size) {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((ce) => {
|
||||
if (ce.event.id === reply.id) {
|
||||
return {
|
||||
event: ce.event,
|
||||
status: CommentEventStatus.Published
|
||||
}
|
||||
}
|
||||
return ce
|
||||
})
|
||||
)
|
||||
// when an event is successfully published remove the status from it after 15 seconds
|
||||
setTimeout(() => {
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((ce) => {
|
||||
if (ce.event.id === reply.id) {
|
||||
delete ce.status
|
||||
}
|
||||
|
||||
return ce
|
||||
})
|
||||
)
|
||||
}, 15000)
|
||||
} else {
|
||||
log(true, LogType.Error, 'Publishing reply failed.')
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((ce) => {
|
||||
if (ce.event.id === reply.id) {
|
||||
return {
|
||||
event: ce.event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
return ce
|
||||
})
|
||||
)
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
toast.error('An error occurred in publishing reply.')
|
||||
log(true, LogType.Error, 'An error occurred in publishing reply.', error)
|
||||
setCommentEvents((prev) =>
|
||||
prev.map((ce) => {
|
||||
if (ce.event.id === id) {
|
||||
return {
|
||||
event: ce.event,
|
||||
status: CommentEventStatus.Failed
|
||||
}
|
||||
}
|
||||
return ce
|
||||
})
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -9,3 +9,4 @@ export * from './consts'
|
||||
export * from './blog'
|
||||
export * from './curationSets'
|
||||
export * from './category'
|
||||
export * from './comments'
|
||||
|
@ -193,3 +193,8 @@ export function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
|
||||
}
|
||||
return storedValue
|
||||
}
|
||||
|
||||
export function adjustTextareaHeight(textarea: HTMLTextAreaElement) {
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = `${textarea.scrollHeight}px`
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user