ProfileFeed.tsx raw
1 import KindFilter from '@/components/KindFilter'
2 import NoteList, { TNoteListRef } from '@/components/NoteList'
3 import Tabs from '@/components/Tabs'
4 import { MAX_PINNED_NOTES } from '@/constants'
5 import { generateBech32IdFromETag } from '@/lib/tag'
6 import { isTouchDevice } from '@/lib/utils'
7 import { useKindFilter } from '@/providers/KindFilterProvider'
8 import { useNostr } from '@/providers/NostrProvider'
9 import client from '@/services/client.service'
10 import storage from '@/services/local-storage.service'
11 import relayInfoService from '@/services/relay-info.service'
12 import { TFeedSubRequest, TNoteListMode } from '@/types'
13 import { NostrEvent } from 'nostr-tools'
14 import { useEffect, useMemo, useRef, useState } from 'react'
15 import { RefreshButton } from '../RefreshButton'
16
17 export default function ProfileFeed({
18 pubkey,
19 topSpace = 0,
20 search = ''
21 }: {
22 pubkey: string
23 topSpace?: number
24 search?: string
25 }) {
26 const { pubkey: myPubkey, pinListEvent: myPinListEvent } = useNostr()
27 const { showKinds } = useKindFilter()
28 const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
29 const [listMode, setListMode] = useState<TNoteListMode>(() => {
30 const mode = storage.getNoteListMode() as string
31 // Migrate legacy modes
32 if (mode === '24h' || mode === 'posts') {
33 return 'postsAndReplies'
34 }
35 return mode as TNoteListMode
36 })
37 const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
38 const [pinnedEventIds, setPinnedEventIds] = useState<string[]>([])
39 const tabs = useMemo(() => {
40 const _tabs = [
41 { value: 'posts', label: 'Notes' },
42 { value: 'postsAndReplies', label: 'Replies' }
43 ]
44
45 if (myPubkey && myPubkey !== pubkey) {
46 _tabs.push({ value: 'you', label: 'YouTabName' })
47 }
48
49 return _tabs
50 }, [myPubkey, pubkey])
51 const supportTouch = useMemo(() => isTouchDevice(), [])
52 const noteListRef = useRef<TNoteListRef>(null)
53
54 useEffect(() => {
55 const initPinnedEventIds = async () => {
56 let evt: NostrEvent | null = null
57 if (pubkey === myPubkey) {
58 evt = myPinListEvent
59 } else {
60 evt = await client.fetchPinListEvent(pubkey)
61 }
62 const hexIdSet = new Set<string>()
63 const ids =
64 (evt?.tags
65 .filter((tag) => tag[0] === 'e')
66 .reverse()
67 .slice(0, MAX_PINNED_NOTES)
68 .map((tag) => {
69 const [, hexId, relay, _pubkey] = tag
70 if (!hexId || hexIdSet.has(hexId) || (_pubkey && _pubkey !== pubkey)) {
71 return undefined
72 }
73
74 const id = generateBech32IdFromETag(['e', hexId, relay ?? '', pubkey])
75 if (id) {
76 hexIdSet.add(hexId)
77 }
78 return id
79 })
80 .filter(Boolean) as string[]) ?? []
81 setPinnedEventIds(ids)
82 }
83 initPinnedEventIds()
84 }, [pubkey, myPubkey, myPinListEvent])
85
86 useEffect(() => {
87 const init = async () => {
88 if (listMode === 'you') {
89 if (!myPubkey) {
90 setSubRequests([])
91 return
92 }
93
94 const [relayList, myRelayList] = await Promise.all([
95 client.fetchRelayList(pubkey),
96 client.fetchRelayList(myPubkey)
97 ])
98
99 setSubRequests([
100 {
101 urls: myRelayList.write.concat(client.currentRelays).slice(0, 5),
102 filter: {
103 authors: [myPubkey],
104 '#p': [pubkey]
105 }
106 },
107 {
108 urls: relayList.write.concat(client.currentRelays).slice(0, 5),
109 filter: {
110 authors: [pubkey],
111 '#p': [myPubkey]
112 }
113 }
114 ])
115 return
116 }
117
118 const relayList = await client.fetchRelayList(pubkey)
119
120 if (search) {
121 const writeRelays = relayList.write.slice(0, 8)
122 const relayInfos = await relayInfoService.getRelayInfos(writeRelays)
123 const searchableRelays = writeRelays.filter((_, index) =>
124 relayInfos[index]?.supported_nips?.includes(50)
125 )
126 setSubRequests([
127 {
128 urls: searchableRelays.concat(storage.getSearchRelays()).slice(0, 8),
129 filter: { authors: [pubkey], search }
130 }
131 ])
132 } else {
133 setSubRequests([
134 {
135 urls: relayList.write.concat(client.currentRelays).slice(0, 8),
136 filter: {
137 authors: [pubkey]
138 }
139 }
140 ])
141 }
142 }
143 init()
144 }, [pubkey, listMode, search])
145
146 const handleListModeChange = (mode: TNoteListMode) => {
147 setListMode(mode)
148 noteListRef.current?.scrollToTop('smooth')
149 }
150
151 const handleShowKindsChange = (newShowKinds: number[]) => {
152 setTemporaryShowKinds(newShowKinds)
153 noteListRef.current?.scrollToTop('instant')
154 }
155
156 return (
157 <>
158 <Tabs
159 value={listMode}
160 tabs={tabs}
161 onTabChange={(listMode) => {
162 handleListModeChange(listMode as TNoteListMode)
163 }}
164 threshold={Math.max(800, topSpace)}
165 options={
166 <>
167 {!supportTouch && <RefreshButton onClick={() => noteListRef.current?.refresh()} />}
168 <KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
169 </>
170 }
171 />
172 <NoteList
173 ref={noteListRef}
174 subRequests={subRequests}
175 showKinds={temporaryShowKinds}
176 hideReplies={listMode === 'posts'}
177 filterMutedNotes={false}
178 pinnedEventIds={listMode === 'you' || !!search ? [] : pinnedEventIds}
179 showNewNotesDirectly={myPubkey === pubkey}
180 />
181 </>
182 )
183 }
184