forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {Pressable, View} from 'react-native'
2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
4import {useNavigation, useNavigationState} from '@react-navigation/native'
5
6import {getCurrentRoute} from '#/lib/routes/helpers'
7import {type NavigationProp} from '#/lib/routes/types'
8import {logger} from '#/logger'
9import {emitSoftReset} from '#/state/events'
10import {
11 type SavedFeedSourceInfo,
12 usePinnedFeedsInfos,
13} from '#/state/queries/feed'
14import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
15import {UserAvatar} from '#/view/com/util/UserAvatar'
16import {atoms as a, useTheme, web} from '#/alf'
17import {useInteractionState} from '#/components/hooks/useInteractionState'
18import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
19import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
20import {Link} from '#/components/Link'
21import {Text} from '#/components/Typography'
22
23export function DesktopFeeds() {
24 const t = useTheme()
25 const {_} = useLingui()
26 const {data: pinnedFeedInfos, error, isLoading} = usePinnedFeedsInfos()
27 const selectedFeed = useSelectedFeed()
28 const setSelectedFeed = useSetSelectedFeed()
29 const navigation = useNavigation<NavigationProp>()
30 const route = useNavigationState(state => {
31 if (!state) {
32 return {name: 'Home'}
33 }
34 return getCurrentRoute(state)
35 })
36
37 if (isLoading) {
38 return (
39 <View style={[{gap: 10}]}>
40 {Array(5)
41 .fill(0)
42 .map((_, i) => (
43 <View
44 key={i}
45 style={[
46 a.rounded_sm,
47 t.atoms.bg_contrast_25,
48 {
49 height: 16,
50 width: i % 2 === 0 ? '60%' : '80%',
51 },
52 ]}
53 />
54 ))}
55 </View>
56 )
57 }
58
59 if (error || !pinnedFeedInfos) {
60 return null
61 }
62
63 return (
64 <View
65 style={[
66 a.flex_1,
67 web({
68 gap: 2,
69 /*
70 * Small padding prevents overflow prior to actually overflowing the
71 * height of the screen with lots of feeds.
72 */
73 paddingTop: 2,
74 overflowY: 'auto',
75 }),
76 ]}>
77 {pinnedFeedInfos.map((feedInfo, index) => {
78 const feed = feedInfo.feedDescriptor
79 const current =
80 route.name === 'Home' &&
81 (selectedFeed ? feed === selectedFeed : index === 0)
82
83 return (
84 <FeedItem
85 key={feedInfo.uri}
86 feedInfo={feedInfo}
87 current={current}
88 onPress={() => {
89 logger.metric(
90 'desktopFeeds:feed:click',
91 {
92 feedUri: feedInfo.uri,
93 feedDescriptor: feed,
94 },
95 {statsig: false},
96 )
97 setSelectedFeed(feed)
98 navigation.navigate('Home')
99 if (route.name === 'Home' && feed === selectedFeed) {
100 emitSoftReset()
101 }
102 }}
103 />
104 )
105 })}
106
107 <Link
108 to="/feeds"
109 label={_(msg`More feeds`)}
110 style={[
111 a.flex_row,
112 a.align_center,
113 a.gap_sm,
114 a.self_start,
115 a.rounded_sm,
116 {paddingVertical: 6, paddingHorizontal: 8},
117 route.name === 'Feeds' && {backgroundColor: t.palette.primary_50},
118 ]}>
119 {({hovered}) => {
120 const isActive = route.name === 'Feeds'
121 return (
122 <>
123 <View
124 style={[
125 a.align_center,
126 a.justify_center,
127 a.rounded_xs,
128 isActive
129 ? {backgroundColor: t.palette.primary_100}
130 : t.atoms.bg_contrast_50,
131 {
132 width: 20,
133 height: 20,
134 },
135 ]}>
136 <Plus
137 style={{width: 16, height: 16}}
138 fill={
139 isActive || hovered
140 ? t.atoms.text.color
141 : t.atoms.text_contrast_medium.color
142 }
143 />
144 </View>
145 <Text
146 style={[
147 a.text_md,
148 a.leading_snug,
149 isActive
150 ? [t.atoms.text, a.font_semi_bold]
151 : hovered
152 ? t.atoms.text
153 : t.atoms.text_contrast_medium,
154 ]}
155 numberOfLines={1}>
156 {_(msg`More feeds`)}
157 </Text>
158 </>
159 )
160 }}
161 </Link>
162 </View>
163 )
164}
165
166function FeedItem({
167 feedInfo,
168 current,
169 onPress,
170}: {
171 feedInfo: SavedFeedSourceInfo
172 current: boolean
173 onPress: () => void
174}) {
175 const t = useTheme()
176 const {_} = useLingui()
177 const {
178 state: hovered,
179 onIn: onHoverIn,
180 onOut: onHoverOut,
181 } = useInteractionState()
182 const isFollowing = feedInfo.feedDescriptor === 'following'
183
184 return (
185 <Pressable
186 accessibilityRole="link"
187 accessibilityLabel={feedInfo.displayName}
188 accessibilityHint={_(msg`Opens ${feedInfo.displayName} feed`)}
189 onPress={onPress}
190 onHoverIn={onHoverIn}
191 onHoverOut={onHoverOut}
192 style={[
193 a.flex_row,
194 a.align_center,
195 a.gap_sm,
196 a.self_start,
197 a.rounded_sm,
198 {paddingVertical: 6, paddingHorizontal: 8},
199 current && {backgroundColor: t.palette.primary_50},
200 ]}>
201 {isFollowing ? (
202 <View
203 style={[
204 a.align_center,
205 a.justify_center,
206 a.rounded_xs,
207 {
208 width: 20,
209 height: 20,
210 backgroundColor: t.palette.primary_500,
211 },
212 ]}>
213 <FilterTimeline
214 style={{width: 14, height: 14}}
215 fill={t.palette.white}
216 />
217 </View>
218 ) : (
219 <UserAvatar
220 type={feedInfo.type === 'list' ? 'list' : 'algo'}
221 size={20}
222 avatar={feedInfo.avatar}
223 noBorder
224 />
225 )}
226 <Text
227 style={[
228 a.text_md,
229 a.leading_snug,
230 current
231 ? [t.atoms.text, a.font_semi_bold]
232 : hovered
233 ? t.atoms.text
234 : t.atoms.text_contrast_medium,
235 ]}
236 numberOfLines={1}>
237 {feedInfo.displayName}
238 </Text>
239 </Pressable>
240 )
241}