Browse Source

feat: improve reply experience

imwald
codytseng 1 year ago
parent
commit
04dd682e0d
  1. 53
      src/renderer/src/components/ReplyNoteList/index.tsx
  2. 7
      src/renderer/src/services/client.service.ts

53
src/renderer/src/components/ReplyNoteList/index.tsx

@ -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}

7
src/renderer/src/services/client.service.ts

@ -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…
Cancel
Save