index.tsx raw
1 import NoteList, { TNoteListRef } from '@/components/NoteList'
2 import PostEditor from '@/components/PostEditor'
3 import Tabs from '@/components/Tabs'
4 import { isTouchDevice } from '@/lib/utils'
5 import { useCompose } from '@/providers/ComposeProvider'
6 import { useKindFilter } from '@/providers/KindFilterProvider'
7 import { useUserTrust } from '@/providers/UserTrustProvider'
8 import storage, { dispatchSettingsChanged } from '@/services/local-storage.service'
9 import { TFeedSubRequest, TNoteListMode } from '@/types'
10 import { useMemo, useRef, useState } from 'react'
11 import KindFilter from '../KindFilter'
12 import { RefreshButton } from '../RefreshButton'
13
14 export default function NormalFeed({
15 subRequests,
16 areAlgoRelays = false,
17 isMainFeed = false,
18 showRelayCloseReason = false,
19 enableSocialGraphFilter = false,
20 onRefresh,
21 onInitialLoad
22 }: {
23 subRequests: TFeedSubRequest[]
24 areAlgoRelays?: boolean
25 isMainFeed?: boolean
26 showRelayCloseReason?: boolean
27 enableSocialGraphFilter?: boolean
28 onRefresh?: () => void
29 onInitialLoad?: () => void
30 }) {
31 const { hideUntrustedNotes } = useUserTrust()
32 const { showKinds } = useKindFilter()
33 const { composeOpen, closeCompose } = useCompose()
34 const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
35 const [listMode, setListMode] = useState<TNoteListMode>(() => {
36 const stored = storage.getNoteListMode() as string
37 // If stored mode was 'posts' or '24h' (legacy), use 'postsAndReplies' instead
38 return stored === 'posts' || stored === '24h' ? 'postsAndReplies' : stored as TNoteListMode
39 })
40 const supportTouch = useMemo(() => isTouchDevice(), [])
41 const noteListRef = useRef<TNoteListRef>(null)
42 const topRef = useRef<HTMLDivElement>(null)
43 const showKindsFilter = useMemo(() => {
44 return subRequests.every((req) => !req.filter.kinds?.length)
45 }, [subRequests])
46
47 const handleListModeChange = (mode: TNoteListMode) => {
48 setListMode(mode)
49 if (isMainFeed) {
50 storage.setNoteListMode(mode)
51 dispatchSettingsChanged()
52 }
53 topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
54 }
55
56 const handleShowKindsChange = (newShowKinds: number[]) => {
57 setTemporaryShowKinds(newShowKinds)
58 noteListRef.current?.scrollToTop()
59 }
60
61 return (
62 <>
63 <Tabs
64 value={listMode}
65 tabs={[
66 { value: 'postsAndReplies', label: 'Feed' }
67 ]}
68 onTabChange={(listMode) => {
69 handleListModeChange(listMode as TNoteListMode)
70 }}
71 options={
72 <>
73 {!supportTouch && (
74 <RefreshButton
75 onClick={() => {
76 if (onRefresh) {
77 onRefresh()
78 return
79 }
80 noteListRef.current?.refresh()
81 }}
82 />
83 )}
84 {showKindsFilter && (
85 <KindFilter
86 showKinds={temporaryShowKinds}
87 onShowKindsChange={handleShowKindsChange}
88 showSocialGraphFilter={enableSocialGraphFilter}
89 />
90 )}
91 </>
92 }
93 />
94 {composeOpen && (
95 <PostEditor
96 inline
97 open={composeOpen}
98 setOpen={(v) => { if (!v) closeCompose() }}
99 />
100 )}
101 <div ref={topRef} className="scroll-mt-[calc(6rem+1px)]" />
102 <NoteList
103 ref={noteListRef}
104 showKinds={temporaryShowKinds}
105 subRequests={subRequests}
106 hideReplies={listMode === 'posts'}
107 hideUntrustedNotes={hideUntrustedNotes}
108 areAlgoRelays={areAlgoRelays}
109 showRelayCloseReason={showRelayCloseReason}
110 applySocialGraphFilter={enableSocialGraphFilter}
111 onInitialLoad={onInitialLoad}
112 />
113 </>
114 )
115 }
116