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