feat: improve reply experience
This commit is contained in:
parent
56d84ae18d
commit
04dd682e0d
2 changed files with 44 additions and 16 deletions
|
|
@ -6,27 +6,45 @@ import { useNostr } from '@renderer/providers/NostrProvider'
|
||||||
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
||||||
import client from '@renderer/services/client.service'
|
import client from '@renderer/services/client.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event as NEvent, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReplyNote from '../ReplyNote'
|
import ReplyNote from '../ReplyNote'
|
||||||
|
|
||||||
const LIMIT = 100
|
const LIMIT = 100
|
||||||
|
|
||||||
export default function ReplyNoteList({ event, className }: { event: Event; className?: string }) {
|
export default function ReplyNoteList({ event, className }: { event: NEvent; className?: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isReady, pubkey } = useNostr()
|
const { isReady, pubkey } = useNostr()
|
||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
const [until, setUntil] = useState<number | undefined>(() => dayjs().unix())
|
const [until, setUntil] = useState<number | undefined>(() => dayjs().unix())
|
||||||
const [replies, setReplies] = useState<Event[]>([])
|
const [replies, setReplies] = useState<NEvent[]>([])
|
||||||
const [replyMap, setReplyMap] = useState<
|
const [replyMap, setReplyMap] = useState<
|
||||||
Record<string, { event: Event; level: number; parent?: Event } | undefined>
|
Record<string, { event: NEvent; level: number; parent?: NEvent } | undefined>
|
||||||
>({})
|
>({})
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [highlightReplyId, setHighlightReplyId] = useState<string | undefined>(undefined)
|
const [highlightReplyId, setHighlightReplyId] = useState<string | undefined>(undefined)
|
||||||
const { updateNoteReplyCount } = useNoteStats()
|
const { updateNoteReplyCount } = useNoteStats()
|
||||||
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEventPublished = (data: Event) => {
|
||||||
|
const customEvent = data as CustomEvent<NEvent>
|
||||||
|
const evt = customEvent.detail
|
||||||
|
if (
|
||||||
|
isReplyNoteEvent(evt) &&
|
||||||
|
evt.tags.some(([tagName, tagValue]) => tagName === 'e' && tagValue === event.id)
|
||||||
|
) {
|
||||||
|
onNewReply(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.addEventListener('eventPublished', handleEventPublished)
|
||||||
|
return () => {
|
||||||
|
client.removeEventListener('eventPublished', handleEventPublished)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReady || loading) return
|
if (!isReady || loading) return
|
||||||
|
|
||||||
|
|
@ -53,11 +71,7 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||||
},
|
},
|
||||||
onNew: (evt) => {
|
onNew: (evt) => {
|
||||||
if (!isReplyNoteEvent(evt)) return
|
if (!isReplyNoteEvent(evt)) return
|
||||||
|
onNewReply(evt)
|
||||||
setReplies((pre) => [...pre, evt])
|
|
||||||
if (evt.pubkey === pubkey) {
|
|
||||||
highlightReply(evt.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -78,7 +92,8 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateNoteReplyCount(event.id, replies.length)
|
updateNoteReplyCount(event.id, replies.length)
|
||||||
|
|
||||||
const replyMap: Record<string, { event: Event; level: number; parent?: Event } | undefined> = {}
|
const replyMap: Record<string, { event: NEvent; level: number; parent?: NEvent } | undefined> =
|
||||||
|
{}
|
||||||
for (const reply of replies) {
|
for (const reply of replies) {
|
||||||
const parentReplyTag = reply.tags.find(isReplyETag)
|
const parentReplyTag = reply.tags.find(isReplyETag)
|
||||||
if (parentReplyTag) {
|
if (parentReplyTag) {
|
||||||
|
|
@ -95,7 +110,7 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||||
}
|
}
|
||||||
|
|
||||||
let level = 0
|
let level = 0
|
||||||
let parent: Event | undefined
|
let parent: NEvent | undefined
|
||||||
for (const [tagName, tagValue] of reply.tags) {
|
for (const [tagName, tagValue] of reply.tags) {
|
||||||
if (tagName === 'e') {
|
if (tagName === 'e') {
|
||||||
const info = replyMap[tagValue]
|
const info = replyMap[tagValue]
|
||||||
|
|
@ -123,10 +138,20 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onNewReply = (evt: NEvent) => {
|
||||||
|
if (replies.some((reply) => reply.id === evt.id)) return
|
||||||
|
setReplies((pre) => [...pre, evt])
|
||||||
|
if (evt.pubkey === pubkey) {
|
||||||
|
setTimeout(() => {
|
||||||
|
highlightReply(evt.id)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const highlightReply = (eventId: string) => {
|
const highlightReply = (eventId: string) => {
|
||||||
const ref = replyRefs.current[eventId]
|
const ref = replyRefs.current[eventId]
|
||||||
if (ref) {
|
if (ref) {
|
||||||
ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
ref.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
}
|
}
|
||||||
setHighlightReplyId(eventId)
|
setHighlightReplyId(eventId)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -144,10 +169,10 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||||
</div>
|
</div>
|
||||||
{replies.length > 0 && (loading || until) && <Separator className="my-2" />}
|
{replies.length > 0 && (loading || until) && <Separator className="my-2" />}
|
||||||
<div className={cn('mb-4', className)}>
|
<div className={cn('mb-4', className)}>
|
||||||
{replies.map((reply, index) => {
|
{replies.map((reply) => {
|
||||||
const info = replyMap[reply.id]
|
const info = replyMap[reply.id]
|
||||||
return (
|
return (
|
||||||
<div ref={(el) => (replyRefs.current[reply.id] = el)} key={index}>
|
<div ref={(el) => (replyRefs.current[reply.id] = el)} key={reply.id}>
|
||||||
<ReplyNote
|
<ReplyNote
|
||||||
event={reply}
|
event={reply}
|
||||||
parentEvent={info?.parent}
|
parentEvent={info?.parent}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const BIG_RELAY_URLS = [
|
||||||
|
|
||||||
type TTimelineRef = [string, number]
|
type TTimelineRef = [string, number]
|
||||||
|
|
||||||
class ClientService {
|
class ClientService extends EventTarget {
|
||||||
static instance: ClientService
|
static instance: ClientService
|
||||||
|
|
||||||
private defaultRelayUrls: string[] = BIG_RELAY_URLS
|
private defaultRelayUrls: string[] = BIG_RELAY_URLS
|
||||||
|
|
@ -84,6 +84,7 @@ class ClientService {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!ClientService.instance) {
|
if (!ClientService.instance) {
|
||||||
|
super()
|
||||||
ClientService.instance = this
|
ClientService.instance = this
|
||||||
}
|
}
|
||||||
return ClientService.instance
|
return ClientService.instance
|
||||||
|
|
@ -102,7 +103,9 @@ class ClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async publishEvent(relayUrls: string[], event: NEvent) {
|
async publishEvent(relayUrls: string[], event: NEvent) {
|
||||||
return await Promise.any(this.pool.publish(relayUrls, event))
|
const result = await Promise.any(this.pool.publish(relayUrls, event))
|
||||||
|
this.dispatchEvent(new CustomEvent('eventPublished', { detail: event }))
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateTimelineKey(urls: string[], filter: Filter): Promise<string> {
|
private async generateTimelineKey(urls: string[], filter: Filter): Promise<string> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue