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