Fix adv.comments issues #209

Merged
enes merged 7 commits from fixes/adv-comments-30-1-25 into staging 2025-01-30 13:43:13 +00:00
10 changed files with 193 additions and 66 deletions

View File

@ -99,7 +99,7 @@ export const Comment = ({ comment }: CommentProps) => {
<div className='IBMSMSMBSSCL_CommentActions'>
<div className='IBMSMSMBSSCL_CommentActionsInside'>
<Reactions {...comment.event.rawEvent()} />
<div
{/* <div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
style={{ cursor: 'not-allowed' }}
>
@ -117,7 +117,7 @@ export const Comment = ({ comment }: CommentProps) => {
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
</div> */}
{typeof profile?.lud16 !== 'undefined' && profile.lud16 !== '' && (
<Zap {...comment.event.rawEvent()} />
)}

View File

@ -1,5 +1,5 @@
import { formatDate } from 'date-fns'
import { useBodyScrollDisable, useNDKContext } from 'hooks'
import { useBodyScrollDisable, useNDKContext, useReplies } from 'hooks'
import { nip19 } from 'nostr-tools'
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
import {
@ -10,7 +10,7 @@ import {
useParams
} from 'react-router-dom'
import { getBlogPageRoute, getModPageRoute, getProfilePageRoute } from 'routes'
import { UserProfile } from 'types'
import { CommentEvent, UserProfile } from 'types'
import { CommentsLoaderResult } from 'types/comments'
import { adjustTextareaHeight, handleCommentSubmit, hexToNpub } from 'utils'
import { Reactions } from './Reactions'
@ -19,6 +19,7 @@ import { NDKKind } from '@nostr-dev-kit/ndk'
import { Comment } from './Comment'
import { useComments } from 'hooks/useComments'
import { CommentContent } from './CommentContent'
import { Dots } from 'components/Spinner'
export const CommentsPopup = () => {
const { naddr } = useParams()
@ -35,7 +36,13 @@ export const CommentsPopup = () => {
: undefined
: undefined
const { event, parents } = useLoaderData() as CommentsLoaderResult
const { event } = useLoaderData() as CommentsLoaderResult
const {
size,
parent: replyEvent,
isComplete,
root: rootEvent
} = useReplies(event.tagValue('e'))
const isRoot = event.tagValue('a') === event.tagValue('A')
const [profile, setProfile] = useState<UserProfile>()
const { commentEvents, setCommentEvents } = useComments(
@ -56,7 +63,6 @@ export const CommentsPopup = () => {
[event.pubkey]
)
const replyEvent = parents.length > 0 ? parents[0] : undefined
const navigate = useNavigate()
const [isSubmitting, setIsSubmitting] = useState(false)
@ -68,7 +74,30 @@ export const CommentsPopup = () => {
adjustTextareaHeight(e.currentTarget)
}, [])
const handleSubmit = handleCommentSubmit(event, setCommentEvents, ndk)
const [visible, setVisible] = useState<CommentEvent[]>([])
const discoveredCount = commentEvents.length - visible.length
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// Initial loading to indicate comments fetching (stop after 5 seconds)
const t = window.setTimeout(() => setIsLoading(false), 5000)
return () => {
window.clearTimeout(t)
}
}, [])
useEffect(() => {
if (isLoading) {
setVisible(commentEvents)
}
}, [commentEvents, isLoading])
const handleDiscoveredClick = () => {
setVisible(commentEvents)
}
const handleSubmit = handleCommentSubmit(
event,
setCommentEvents,
setVisible,
ndk
)
const handleComment = async () => {
setIsSubmitting(true)
@ -110,6 +139,9 @@ export const CommentsPopup = () => {
<div className='IBMSMSMBSSCL_CTO'>
{replyEvent && (
<Link
style={{
...(!isComplete ? { pointerEvents: 'none' } : {})
}}
className='IBMSMSMBSSCL_CTOLink'
to={baseUrl + replyEvent.encode()}
>
@ -126,16 +158,20 @@ export const CommentsPopup = () => {
</Link>
)}
<p className='IBMSMSMBSSCL_CTOText'>
Reply Depth:&nbsp;<span>{parents.length}</span>
Reply Depth:&nbsp;<span>{size}</span>
{!isComplete && <Dots />}
</p>
</div>
{!isRoot && (
{!isRoot && rootEvent && (
<Link
style={{
...(!isComplete ? { pointerEvents: 'none' } : {})
}}
className='btn btnMain IBMSMSMBSSCL_CTOBtn'
type='button'
to={'..'}
to={baseUrl + rootEvent.encode()}
>
Main Post
Main Post {!isComplete && <Dots />}
</Link>
)}
</div>
@ -185,7 +221,7 @@ export const CommentsPopup = () => {
<div className='IBMSMSMBSSCL_CommentActionsInside'>
<Reactions {...event.rawEvent()} />
<div
{/* <div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
style={{ cursor: 'not-allowed' }}
>
@ -203,7 +239,7 @@ export const CommentsPopup = () => {
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
</div> */}
{typeof profile?.lud16 !== 'undefined' &&
profile.lud16 !== '' && <Zap {...event.rawEvent()} />}
@ -262,6 +298,26 @@ export const CommentsPopup = () => {
<>
<h3 className='IBMSMSMBSSCL_CommentNoteRepliesTitle'>
Replies
<button
type='button'
className='btnMain IBMSMSMBSSCL_CommentNoteRepliesTitleBtn'
onClick={
discoveredCount ? handleDiscoveredClick : undefined
}
>
<span>
{isLoading ? (
<>
Discovering replies
<Dots />
</>
) : discoveredCount ? (
<>Load {discoveredCount} discovered replies</>
) : (
<>No new replies</>
)}
</span>
</button>
</h3>
<div className='pUMCB_RepliesToPrime'>
{commentEvents.map((reply) => (

View File

@ -1,4 +1,4 @@
import { Spinner } from 'components/Spinner'
import { Dots } from 'components/Spinner'
import { useNDKContext } from 'hooks'
import { useComments } from 'hooks/useComments'
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'
@ -48,12 +48,16 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
setCommentCount(commentEvents.length)
}, [commentEvents, setCommentCount])
const handleSubmit = handleCommentSubmit(event, setCommentEvents, ndk)
const handleDiscoveredClick = () => {
setVisible(commentEvents)
}
const [visible, setVisible] = useState<CommentEvent[]>([])
const handleSubmit = handleCommentSubmit(
event,
setCommentEvents,
setVisible,
ndk
)
useEffect(() => {
if (isLoading) {
setVisible(commentEvents)
@ -93,17 +97,24 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
{/* Hide comment form if aTag is missing */}
{!!addressable.aTag && <CommentForm handleSubmit={handleSubmit} />}
<div>
{isLoading ? (
<Spinner />
) : (
<button
type='button'
className='btnMain'
onClick={discoveredCount ? handleDiscoveredClick : undefined}
>
<span>Load {discoveredCount} discovered comments</span>
</button>
<span>
{isLoading ? (
<>
Discovering comments
<Dots />
</>
) : discoveredCount ? (
<>Load {discoveredCount} discovered comments</>
) : (
<>No new comments</>
)}
</span>
</button>
</div>
<Filter
filterOptions={filterOptions}

View File

@ -11,3 +11,4 @@ export * from './useScrollDisable'
export * from './useLocalStorage'
export * from './useSessionStorage'
export * from './useLocalCache'
export * from './useReplies'

53
src/hooks/useReplies.tsx Normal file
View File

@ -0,0 +1,53 @@
import {
NDKEvent,
NDKKind,
NDKSubscriptionCacheUsage
} from '@nostr-dev-kit/ndk'
import { useState } from 'react'
import { useNDKContext } from './useNDKContext'
import { useDidMount } from './useDidMount'
export const useReplies = (eTag: string | undefined) => {
const { ndk } = useNDKContext()
const [replies, setReplies] = useState<NDKEvent[]>([])
const [isComplete, setIsComplete] = useState(false)
useDidMount(async () => {
if (!eTag) {
setIsComplete(true)
return
}
let eDepth: string | undefined = eTag
while (eDepth) {
const previousReply = await ndk.fetchEvent(
{
kinds: [NDKKind.Text, NDKKind.GenericReply],
ids: [eDepth]
},
{
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
}
)
if (previousReply) {
setReplies((p) => {
if (p.findIndex((p) => p.id === previousReply.id) === -1) {
p.push(previousReply)
}
return p
})
eDepth = previousReply.tagValue('e')
} else {
eDepth = undefined
}
}
setIsComplete(true)
})
return {
size: replies.length,
isComplete,
parent: replies.length > 0 ? replies[0] : undefined,
root: isComplete ? replies[replies.length - 1] : undefined
}
}

View File

@ -1,4 +1,3 @@
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'
@ -21,23 +20,8 @@ export const commentsLoader =
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
event: replyEvent
}
return result
} catch (error) {

View File

@ -59,6 +59,7 @@
}
.IBMSMSMBSSCL_CBText {
white-space: pre-line;
}
.IBMSMSMBSSCL_CBTextStatus {
@ -478,6 +479,13 @@ hover {
width: 100%;
margin: 0 0 15px 0;
color: rgba(255, 255, 255, 0.5);
display: flex;
flex-direction: row;
justify-content: space-between;
}
.IBMSMSMBSSCL_CommentNoteRepliesTitleBtn {
font-size: 16px;
}
.IBMSMSMBSSCL_CAElementLoadWrapper {

View File

@ -2,7 +2,6 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'
export interface CommentsLoaderResult {
event: NDKEvent
parents: NDKEvent[]
}
export enum SortByEnum {

View File

@ -6,6 +6,7 @@ import { log, LogType } from './utils'
export function handleCommentSubmit(
event: NDKEvent | undefined,
setCommentEvents: React.Dispatch<React.SetStateAction<CommentEvent[]>>,
setVisible: React.Dispatch<React.SetStateAction<CommentEvent[]>>,
ndk: NDK
) {
return async (content: string): Promise<boolean> => {
@ -19,13 +20,18 @@ export function handleCommentSubmit(
const reply = event.reply()
reply.content = content.trim()
setCommentEvents((prev) => [
setCommentEvents((prev) => {
const newCommentEvents = [
{
event: reply,
status: CommentEventStatus.Publishing
},
...prev
])
]
setVisible(newCommentEvents)
return newCommentEvents
})
if (!ndk.signer) {
ndk.signer = new NDKNip07Signer()
}
@ -33,8 +39,8 @@ export function handleCommentSubmit(
id = reply.id
const relaySet = await reply.publish()
if (relaySet.size) {
setCommentEvents((prev) =>
prev.map((ce) => {
setCommentEvents((prev) => {
const newCommentEvents = prev.map((ce) => {
if (ce.event.id === reply.id) {
return {
event: ce.event,
@ -43,23 +49,28 @@ export function handleCommentSubmit(
}
return ce
})
)
setVisible(newCommentEvents)
return newCommentEvents
})
// when an event is successfully published remove the status from it after 15 seconds
setTimeout(() => {
setCommentEvents((prev) =>
prev.map((ce) => {
setCommentEvents((prev) => {
const newCommentEvents = prev.map((ce) => {
if (ce.event.id === reply.id) {
delete ce.status
}
return ce
})
)
setVisible(newCommentEvents)
return newCommentEvents
})
}, 15000)
return true
} else {
log(true, LogType.Error, 'Publishing reply failed.')
setCommentEvents((prev) =>
prev.map((ce) => {
setCommentEvents((prev) => {
const newCommentEvents = prev.map((ce) => {
if (ce.event.id === reply.id) {
return {
event: ce.event,
@ -68,14 +79,16 @@ export function handleCommentSubmit(
}
return ce
})
)
}
setVisible(newCommentEvents)
return newCommentEvents
})
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) => {
setCommentEvents((prev) => {
const newCommentEvents = prev.map((ce) => {
if (ce.event.id === id) {
return {
event: ce.event,
@ -84,7 +97,9 @@ export function handleCommentSubmit(
}
return ce
})
)
setVisible(newCommentEvents)
return newCommentEvents
})
return false
}
}