index.tsx raw
1 import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'
2 import { useStuff } from '@/hooks/useStuff'
3 import { useAllDescendantThreads } from '@/hooks/useThread'
4 import { getEventKey, isMentioningMutedUsers } from '@/lib/event'
5 import { useContentPolicy } from '@/providers/ContentPolicyProvider'
6 import { useMuteList } from '@/providers/MuteListProvider'
7 import { useUserTrust } from '@/providers/UserTrustProvider'
8 import threadService from '@/services/thread.service'
9 import { Event as NEvent } from 'nostr-tools'
10 import { useCallback, useEffect, useMemo, useState } from 'react'
11 import { useTranslation } from 'react-i18next'
12 import { LoadingBar } from '../LoadingBar'
13 import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote'
14 import SubReplies from './SubReplies'
15
16 const LIMIT = 100
17 const SHOW_COUNT = 10
18
19 export default function ReplyNoteList({ stuff, navIndexOffset = 0 }: { stuff: NEvent | string; navIndexOffset?: number }) {
20 const { t } = useTranslation()
21 const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
22 const { mutePubkeySet } = useMuteList()
23 const { hideContentMentioningMutedUsers } = useContentPolicy()
24 const { stuffKey } = useStuff(stuff)
25 const allThreads = useAllDescendantThreads(stuffKey)
26 const [initialLoading, setInitialLoading] = useState(false)
27
28 const replies = useMemo(() => {
29 const replyKeySet = new Set<string>()
30 const thread = allThreads.get(stuffKey) || []
31 const replyEvents = thread.filter((evt) => {
32 const key = getEventKey(evt)
33 if (replyKeySet.has(key)) return false
34 if (mutePubkeySet.has(evt.pubkey)) return false
35 if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) {
36 return false
37 }
38 if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) {
39 const replyKey = getEventKey(evt)
40 const repliesForThisReply = allThreads.get(replyKey)
41 // If the reply is not trusted and there are no trusted replies for this reply, skip rendering
42 if (
43 !repliesForThisReply ||
44 repliesForThisReply.every((evt) => !isUserTrusted(evt.pubkey))
45 ) {
46 return false
47 }
48 }
49
50 replyKeySet.add(key)
51 return true
52 })
53 return replyEvents.sort((a, b) => b.created_at - a.created_at)
54 }, [
55 stuffKey,
56 allThreads,
57 mutePubkeySet,
58 hideContentMentioningMutedUsers,
59 hideUntrustedInteractions,
60 isUserTrusted
61 ])
62
63 // Initial subscription
64 useEffect(() => {
65 const loadInitial = async () => {
66 setInitialLoading(true)
67 await threadService.subscribe(stuff, LIMIT)
68 setInitialLoading(false)
69 }
70
71 loadInitial()
72
73 return () => {
74 threadService.unsubscribe(stuff)
75 }
76 }, [stuff])
77
78 const handleLoadMore = useCallback(async () => {
79 return await threadService.loadMore(stuff, LIMIT)
80 }, [stuff])
81
82 const { visibleItems, loading, shouldShowLoadingIndicator, bottomRef } = useInfiniteScroll({
83 items: replies,
84 showCount: SHOW_COUNT,
85 onLoadMore: handleLoadMore,
86 initialLoading
87 })
88
89 return (
90 <div className="min-h-[80vh]">
91 {(loading || initialLoading) && <LoadingBar />}
92 <div>
93 {visibleItems.map((reply, index) => (
94 <Item key={reply.id} reply={reply} navIndex={navIndexOffset + index} />
95 ))}
96 </div>
97 <div ref={bottomRef} />
98 {shouldShowLoadingIndicator ? (
99 <ReplyNoteSkeleton />
100 ) : (
101 <div className="text-sm mt-2 mb-3 text-center text-muted-foreground">
102 {replies.length > 0 ? t('no more replies') : t('no replies')}
103 </div>
104 )}
105 </div>
106 )
107 }
108
109 // Use larger gaps between items to leave room for sub-replies
110 const NAV_INDEX_MULTIPLIER = 100
111
112 function Item({ reply, navIndex }: { reply: NEvent; navIndex: number }) {
113 const key = useMemo(() => getEventKey(reply), [reply])
114 const baseNavIndex = navIndex * NAV_INDEX_MULTIPLIER
115
116 return (
117 <div className="relative border-b">
118 <ReplyNote event={reply} navColumn={2} navIndex={baseNavIndex} />
119 <SubReplies parentKey={key} revealerNavIndex={baseNavIndex + 1} subReplyNavIndexStart={baseNavIndex + 2} />
120 </div>
121 )
122 }
123