fix: replies
This commit is contained in:
parent
a6c2decfe3
commit
304bbe4f01
13 changed files with 178 additions and 120 deletions
|
|
@ -1,10 +1,9 @@
|
|||
import { Separator } from '@/components/ui/separator'
|
||||
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||
import { isCommentEvent, isProtectedEvent } from '@/lib/event'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
import { generateEventId, tagNameEquals } from '@/lib/tag'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event as NEvent } from 'nostr-tools'
|
||||
|
|
@ -31,7 +30,6 @@ export default function Nip22ReplyNoteList({
|
|||
>({})
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [highlightReplyId, setHighlightReplyId] = useState<string | undefined>(undefined)
|
||||
const { updateNoteReplyCount } = useNoteStats()
|
||||
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
|
@ -111,8 +109,6 @@ export default function Nip22ReplyNoteList({
|
|||
}, [event])
|
||||
|
||||
useEffect(() => {
|
||||
updateNoteReplyCount(event.id, replies.length)
|
||||
|
||||
const replyMap: Record<string, { event: NEvent; level: number; parent?: NEvent } | undefined> =
|
||||
{}
|
||||
for (const reply of replies) {
|
||||
|
|
@ -128,7 +124,7 @@ export default function Nip22ReplyNoteList({
|
|||
continue
|
||||
}
|
||||
setReplyMap(replyMap)
|
||||
}, [replies, event.id, updateNoteReplyCount])
|
||||
}, [replies, event.id])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loading || !until || !timelineKey) return
|
||||
|
|
@ -192,8 +188,8 @@ export default function Nip22ReplyNoteList({
|
|||
<div ref={(el) => (replyRefs.current[reply.id] = el)} key={reply.id}>
|
||||
<ReplyNote
|
||||
event={reply}
|
||||
parentEvent={info?.parent}
|
||||
onClickParent={highlightReply}
|
||||
parentEventId={info?.parent ? generateEventId(info.parent) : undefined}
|
||||
onClickParent={() => info?.parent?.id && highlightReply(info?.parent?.id)}
|
||||
highlight={highlightReplyId === reply.id}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { ExtendedKind } from '@/constants'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import { extractImageInfosFromEventTags, getParentEventId, getUsingClient } from '@/lib/event'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { Event } from 'nostr-tools'
|
||||
|
|
@ -30,7 +29,6 @@ export default function Note({
|
|||
[event, hideParentNotePreview]
|
||||
)
|
||||
const imageInfos = useMemo(() => extractImageInfosFromEventTags(event), [event])
|
||||
const { event: parentEvent, isFetching } = useFetchEvent(parentEventId)
|
||||
const usingClient = useMemo(() => getUsingClient(event), [event])
|
||||
|
||||
return (
|
||||
|
|
@ -60,8 +58,7 @@ export default function Note({
|
|||
</div>
|
||||
{parentEventId && (
|
||||
<ParentNotePreview
|
||||
event={parentEvent}
|
||||
isFetching={isFetching}
|
||||
eventId={parentEventId}
|
||||
className="mt-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import { useReply } from '@/providers/ReplyProvider'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
|
@ -7,19 +7,13 @@ import { useTranslation } from 'react-i18next'
|
|||
import PostEditor from '../PostEditor'
|
||||
import { formatCount } from './utils'
|
||||
|
||||
export default function ReplyButton({
|
||||
event,
|
||||
variant = 'note'
|
||||
}: {
|
||||
event: Event
|
||||
variant?: 'note' | 'reply'
|
||||
}) {
|
||||
export default function ReplyButton({ event }: { event: Event }) {
|
||||
const { t } = useTranslation()
|
||||
const { checkLogin } = useNostr()
|
||||
const { noteStatsMap } = useNoteStats()
|
||||
const { replyCount } = useMemo(
|
||||
() => (variant === 'reply' ? {} : (noteStatsMap.get(event.id) ?? {})),
|
||||
[noteStatsMap, event.id, variant]
|
||||
const { repliesMap } = useReply()
|
||||
const replyCount = useMemo(
|
||||
() => repliesMap.get(event.id)?.events.length || 0,
|
||||
[repliesMap, event.id]
|
||||
)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
|
|
@ -36,9 +30,7 @@ export default function ReplyButton({
|
|||
title={t('Reply')}
|
||||
>
|
||||
<MessageCircle />
|
||||
{variant !== 'reply' && !!replyCount && (
|
||||
<div className="text-sm">{formatCount(replyCount)}</div>
|
||||
)}
|
||||
{!!replyCount && <div className="text-sm">{formatCount(replyCount)}</div>}
|
||||
</button>
|
||||
<PostEditor parentEvent={event} open={open} setOpen={setOpen} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ export default function NoteStats({
|
|||
event,
|
||||
className,
|
||||
classNames,
|
||||
fetchIfNotExisting = false,
|
||||
variant = 'note'
|
||||
fetchIfNotExisting = false
|
||||
}: {
|
||||
event: Event
|
||||
className?: string
|
||||
|
|
@ -25,7 +24,6 @@ export default function NoteStats({
|
|||
buttonBar?: string
|
||||
}
|
||||
fetchIfNotExisting?: boolean
|
||||
variant?: 'note' | 'reply'
|
||||
}) {
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { fetchNoteStats } = useNoteStats()
|
||||
|
|
@ -50,7 +48,7 @@ export default function NoteStats({
|
|||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ReplyButton event={event} variant={variant} />
|
||||
<ReplyButton event={event} />
|
||||
<RepostButton event={event} />
|
||||
<LikeButton event={event} />
|
||||
<ZapButton event={event} />
|
||||
|
|
@ -70,7 +68,7 @@ export default function NoteStats({
|
|||
className={cn('flex items-center', loading ? 'animate-pulse' : '')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ReplyButton event={event} variant={variant} />
|
||||
<ReplyButton event={event} />
|
||||
<RepostButton event={event} />
|
||||
<LikeButton event={event} />
|
||||
<ZapButton event={event} />
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ContentPreview from '../ContentPreview'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
|
||||
export default function ParentNotePreview({
|
||||
event,
|
||||
isFetching = false,
|
||||
eventId,
|
||||
className,
|
||||
onClick
|
||||
}: {
|
||||
event?: Event
|
||||
isFetching?: boolean
|
||||
eventId: string
|
||||
className?: string
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { mutePubkeys } = useMuteList()
|
||||
const { event, isFetching } = useFetchEvent(eventId)
|
||||
const isMuted = useMemo(
|
||||
() => (event ? mutePubkeys.includes(event.pubkey) : false),
|
||||
[mutePubkeys, event]
|
||||
|
|
@ -43,6 +42,20 @@ export default function ParentNotePreview({
|
|||
)
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-fit max-w-full text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0">{t('reply to')}</div>
|
||||
<div>{`[${t('Not found the note')}]`}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
|
@ -10,18 +12,16 @@ import NoteStats from '../NoteStats'
|
|||
import ParentNotePreview from '../ParentNotePreview'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
import Username from '../Username'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { toNote } from '@/lib/link'
|
||||
|
||||
export default function ReplyNote({
|
||||
event,
|
||||
parentEvent,
|
||||
parentEventId,
|
||||
onClickParent = () => {},
|
||||
highlight = false
|
||||
}: {
|
||||
event: Event
|
||||
parentEvent?: Event
|
||||
onClickParent?: (eventId: string) => void
|
||||
parentEventId?: string
|
||||
onClickParent?: () => void
|
||||
highlight?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -53,20 +53,20 @@ export default function ReplyNote({
|
|||
</div>
|
||||
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
|
||||
</div>
|
||||
{parentEvent && (
|
||||
{parentEventId && (
|
||||
<ParentNotePreview
|
||||
className="mt-2"
|
||||
event={parentEvent}
|
||||
eventId={parentEventId}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickParent(parentEvent.id)
|
||||
onClickParent()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{show ? (
|
||||
<>
|
||||
<Content className="mt-2" event={event} />
|
||||
<NoteStats className="mt-2" event={event} variant="reply" />
|
||||
<NoteStats className="mt-2" event={event} />
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { Separator } from '@/components/ui/separator'
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import {
|
||||
getParentEventHexId,
|
||||
getParentEventTag,
|
||||
getRootEventHexId,
|
||||
getRootEventTag,
|
||||
isReplyNoteEvent
|
||||
} from '@/lib/event'
|
||||
import { generateEventIdFromETag } from '@/lib/tag'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||
import { useReply } from '@/providers/ReplyProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Event as NEvent, kinds } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReplyNote from '../ReplyNote'
|
||||
|
||||
const LIMIT = 100
|
||||
const SHOW_COUNT = 10
|
||||
|
||||
export default function ReplyNoteList({
|
||||
index,
|
||||
|
|
@ -29,16 +30,13 @@ export default function ReplyNoteList({
|
|||
const { t } = useTranslation()
|
||||
const { currentIndex } = useSecondaryPage()
|
||||
const [rootInfo, setRootInfo] = useState<{ id: string; pubkey: string } | undefined>(undefined)
|
||||
const { repliesMap, addReplies } = useReply()
|
||||
const replies = useMemo(() => repliesMap.get(event.id)?.events || [], [event.id, repliesMap])
|
||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||
const [until, setUntil] = useState<number | undefined>(undefined)
|
||||
const [events, setEvents] = useState<NEvent[]>([])
|
||||
const [replies, setReplies] = useState<NEvent[]>([])
|
||||
const [replyMap, setReplyMap] = useState<
|
||||
Map<string, { event: NEvent; level: number; parent?: NEvent } | undefined>
|
||||
>(new Map())
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||
const [highlightReplyId, setHighlightReplyId] = useState<string | undefined>(undefined)
|
||||
const { updateNoteReplyCount } = useNoteStats()
|
||||
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
|
@ -66,10 +64,7 @@ export default function ReplyNoteList({
|
|||
}, [event])
|
||||
|
||||
const onNewReply = useCallback((evt: NEvent) => {
|
||||
setEvents((pre) => {
|
||||
if (pre.some((reply) => reply.id === evt.id)) return pre
|
||||
return [...pre, evt]
|
||||
})
|
||||
addReplies([evt])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -114,7 +109,7 @@ export default function ReplyNoteList({
|
|||
{
|
||||
onEvents: (evts, eosed) => {
|
||||
if (evts.length > 0) {
|
||||
setEvents(evts.filter((evt) => isReplyNoteEvent(evt)).reverse())
|
||||
addReplies(evts.filter((evt) => isReplyNoteEvent(evt)))
|
||||
}
|
||||
if (eosed) {
|
||||
setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined)
|
||||
|
|
@ -123,7 +118,7 @@ export default function ReplyNoteList({
|
|||
},
|
||||
onNew: (evt) => {
|
||||
if (!isReplyNoteEvent(evt)) return
|
||||
onNewReply(evt)
|
||||
addReplies([evt])
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -142,44 +137,45 @@ export default function ReplyNoteList({
|
|||
}, [rootInfo, currentIndex, index, onNewReply])
|
||||
|
||||
useEffect(() => {
|
||||
const replies: NEvent[] = []
|
||||
const replyMap: Map<string, { event: NEvent; level: number; parent?: NEvent } | undefined> =
|
||||
new Map()
|
||||
const rootEventId = getRootEventHexId(event) ?? event.id
|
||||
const isRootEvent = rootEventId === event.id
|
||||
for (const evt of events) {
|
||||
const parentEventId = getParentEventHexId(evt)
|
||||
if (parentEventId) {
|
||||
const parentReplyInfo = replyMap.get(parentEventId)
|
||||
if (!parentReplyInfo && parentEventId !== event.id) continue
|
||||
|
||||
const level = parentReplyInfo ? parentReplyInfo.level + 1 : 1
|
||||
replies.push(evt)
|
||||
replyMap.set(evt.id, { event: evt, level, parent: parentReplyInfo?.event })
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isRootEvent) continue
|
||||
|
||||
replies.push(evt)
|
||||
replyMap.set(evt.id, { event: evt, level: 1 })
|
||||
}
|
||||
setReplyMap(replyMap)
|
||||
setReplies(replies)
|
||||
updateNoteReplyCount(event.id, replies.length)
|
||||
if (replies.length === 0) {
|
||||
loadMore()
|
||||
}
|
||||
}, [events, event, updateNoteReplyCount])
|
||||
}, [replies])
|
||||
|
||||
useEffect(() => {
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: '10px',
|
||||
threshold: 0.1
|
||||
}
|
||||
|
||||
const observerInstance = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && showCount < replies.length) {
|
||||
setShowCount((prev) => prev + SHOW_COUNT)
|
||||
}
|
||||
}, options)
|
||||
|
||||
const currentBottomRef = bottomRef.current
|
||||
|
||||
if (currentBottomRef) {
|
||||
observerInstance.observe(currentBottomRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observerInstance && currentBottomRef) {
|
||||
observerInstance.unobserve(currentBottomRef)
|
||||
}
|
||||
}
|
||||
}, [replies, showCount])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loading || !until || !timelineKey) return
|
||||
|
||||
setLoading(true)
|
||||
const events = await client.loadMoreTimeline(timelineKey, until, LIMIT)
|
||||
const olderEvents = events.filter((evt) => isReplyNoteEvent(evt)).reverse()
|
||||
const olderEvents = events.filter((evt) => isReplyNoteEvent(evt))
|
||||
if (olderEvents.length > 0) {
|
||||
setEvents((pre) => [...olderEvents, ...pre])
|
||||
addReplies(olderEvents)
|
||||
}
|
||||
setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined)
|
||||
setLoading(false)
|
||||
|
|
@ -210,14 +206,16 @@ export default function ReplyNoteList({
|
|||
)}
|
||||
{replies.length > 0 && (loading || until) && <Separator className="mt-2" />}
|
||||
<div className={className}>
|
||||
{replies.map((reply) => {
|
||||
const info = replyMap.get(reply.id)
|
||||
{replies.slice(0, showCount).map((reply) => {
|
||||
const parentEventTag = getParentEventTag(reply)
|
||||
const parentEventOriginalId = parentEventTag?.[1]
|
||||
const parentEventId = parentEventTag ? generateEventIdFromETag(parentEventTag) : undefined
|
||||
return (
|
||||
<div ref={(el) => (replyRefs.current[reply.id] = el)} key={reply.id}>
|
||||
<ReplyNote
|
||||
event={reply}
|
||||
parentEvent={info?.parent}
|
||||
onClickParent={highlightReply}
|
||||
parentEventId={event.id !== parentEventOriginalId ? parentEventId : undefined}
|
||||
onClickParent={() => parentEventOriginalId && highlightReply(parentEventOriginalId)}
|
||||
highlight={highlightReplyId === reply.id}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue