Bluesky app fork with some witchin' additions 馃挮
at main 238 lines 6.5 kB view raw
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}