Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 437 lines 16 kB view raw
1import {type JSX, useCallback} from 'react' 2import {type GestureResponderEvent, View} from 'react-native' 3import Animated from 'react-native-reanimated' 4import {useSafeAreaInsets} from 'react-native-safe-area-context' 5import {msg, plural, Trans} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7import {type BottomTabBarProps} from '@react-navigation/bottom-tabs' 8import {StackActions} from '@react-navigation/native' 9 10import {useActorStatus} from '#/lib/actor-status' 11import {PressableScale} from '#/lib/custom-animations/PressableScale' 12import {BOTTOM_BAR_AVI} from '#/lib/demo' 13import {useHaptics} from '#/lib/haptics' 14import {useDedupe} from '#/lib/hooks/useDedupe' 15import {useHideBottomBarBorder} from '#/lib/hooks/useHideBottomBarBorder' 16import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' 17import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' 18import {usePalette} from '#/lib/hooks/usePalette' 19import {clamp} from '#/lib/numbers' 20import {getTabState, TabState} from '#/lib/routes/helpers' 21import {emitSoftReset} from '#/state/events' 22import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars' 23import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 24import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' 25import {useUnreadNotifications} from '#/state/queries/notifications/unread' 26import {useProfileQuery} from '#/state/queries/profile' 27import {useSession} from '#/state/session' 28import {useLoggedOutViewControls} from '#/state/shell/logged-out' 29import {useShellLayout} from '#/state/shell/shell-layout' 30import {useCloseAllActiveElements} from '#/state/util' 31import {UserAvatar} from '#/view/com/util/UserAvatar' 32import {Logo} from '#/view/icons/Logo' 33import {Logotype} from '#/view/icons/Logotype' 34import {atoms as a, useTheme} from '#/alf' 35import {Button, ButtonText} from '#/components/Button' 36import {useDialogControl} from '#/components/Dialog' 37import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 38import { 39 Bell_Filled_Corner0_Rounded as BellFilled, 40 Bell_Stroke2_Corner0_Rounded as Bell, 41} from '#/components/icons/Bell' 42import { 43 HomeOpen_Filled_Corner0_Rounded as HomeFilled, 44 HomeOpen_Stoke2_Corner0_Rounded as Home, 45} from '#/components/icons/HomeOpen' 46import { 47 MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled, 48 MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlass, 49} from '#/components/icons/MagnifyingGlass' 50import { 51 Message_Stroke2_Corner0_Rounded as Message, 52 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, 53} from '#/components/icons/Message' 54import {Text} from '#/components/Typography' 55import {useDemoMode} from '#/storage/hooks/demo-mode' 56import {styles} from './BottomBarStyles' 57 58type TabOptions = 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile' 59 60export function BottomBar({navigation}: BottomTabBarProps) { 61 const {hasSession, currentAccount} = useSession() 62 const pal = usePalette('default') 63 const {_} = useLingui() 64 const safeAreaInsets = useSafeAreaInsets() 65 const {footerHeight} = useShellLayout() 66 const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} = 67 useNavigationTabState() 68 const numUnreadNotifications = useUnreadNotifications() 69 const numUnreadMessages = useUnreadMessageCount() 70 const footerMinimalShellTransform = useMinimalShellFooterTransform() 71 const {data: profile} = useProfileQuery({did: currentAccount?.did}) 72 const {requestSwitchToAccount} = useLoggedOutViewControls() 73 const closeAllActiveElements = useCloseAllActiveElements() 74 const dedupe = useDedupe() 75 const accountSwitchControl = useDialogControl() 76 const playHaptic = useHaptics() 77 const hideBorder = useHideBottomBarBorder() 78 const iconWidth = 28 79 80 const showSignIn = useCallback(() => { 81 closeAllActiveElements() 82 requestSwitchToAccount({requestedAccount: 'none'}) 83 }, [requestSwitchToAccount, closeAllActiveElements]) 84 85 const showCreateAccount = useCallback(() => { 86 closeAllActiveElements() 87 requestSwitchToAccount({requestedAccount: 'new'}) 88 // setShowLoggedOut(true) 89 }, [requestSwitchToAccount, closeAllActiveElements]) 90 91 const onPressTab = useCallback( 92 (tab: TabOptions) => { 93 const state = navigation.getState() 94 const tabState = getTabState(state, tab) 95 if (tabState === TabState.InsideAtRoot) { 96 emitSoftReset() 97 } else if (tabState === TabState.Inside) { 98 // find the correct navigator in which to pop-to-top 99 const target = state.routes.find(route => route.name === `${tab}Tab`) 100 ?.state?.key 101 dedupe(() => { 102 if (target) { 103 // if we found it, trigger pop-to-top 104 navigation.dispatch({ 105 ...StackActions.popToTop(), 106 target, 107 }) 108 } else { 109 // fallback: reset navigation 110 navigation.reset({ 111 index: 0, 112 routes: [{name: `${tab}Tab`}], 113 }) 114 } 115 }) 116 } else { 117 dedupe(() => navigation.navigate(`${tab}Tab`)) 118 } 119 }, 120 [navigation, dedupe], 121 ) 122 const onPressHome = useCallback(() => onPressTab('Home'), [onPressTab]) 123 const onPressSearch = useCallback(() => onPressTab('Search'), [onPressTab]) 124 const onPressNotifications = useCallback( 125 () => onPressTab('Notifications'), 126 [onPressTab], 127 ) 128 const onPressProfile = useCallback(() => { 129 onPressTab('MyProfile') 130 }, [onPressTab]) 131 const onPressMessages = useCallback(() => { 132 onPressTab('Messages') 133 }, [onPressTab]) 134 135 const onLongPressProfile = useCallback(() => { 136 playHaptic() 137 accountSwitchControl.open() 138 }, [accountSwitchControl, playHaptic]) 139 140 const [demoMode] = useDemoMode() 141 const {isActive: live} = useActorStatus(profile) 142 143 const enableSquareAvatars = useEnableSquareAvatars() 144 145 return ( 146 <> 147 <SwitchAccountDialog control={accountSwitchControl} /> 148 149 <Animated.View 150 style={[ 151 styles.bottomBar, 152 pal.view, 153 hideBorder ? {borderColor: pal.view.backgroundColor} : pal.border, 154 {paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)}, 155 footerMinimalShellTransform, 156 ]} 157 onLayout={e => { 158 footerHeight.set(e.nativeEvent.layout.height) 159 }}> 160 {hasSession ? ( 161 <> 162 <Btn 163 testID="bottomBarHomeBtn" 164 icon={ 165 isAtHome ? ( 166 <HomeFilled 167 width={iconWidth + 1} 168 style={[styles.ctrlIcon, pal.text, styles.homeIcon]} 169 /> 170 ) : ( 171 <Home 172 width={iconWidth + 1} 173 style={[styles.ctrlIcon, pal.text, styles.homeIcon]} 174 /> 175 ) 176 } 177 onPress={onPressHome} 178 accessibilityRole="tab" 179 accessibilityLabel={_(msg`Home`)} 180 accessibilityHint="" 181 /> 182 <Btn 183 icon={ 184 isAtSearch ? ( 185 <MagnifyingGlassFilled 186 width={iconWidth + 2} 187 style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 188 /> 189 ) : ( 190 <MagnifyingGlass 191 testID="bottomBarSearchBtn" 192 width={iconWidth + 2} 193 style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 194 /> 195 ) 196 } 197 onPress={onPressSearch} 198 accessibilityRole="search" 199 accessibilityLabel={_(msg`Search`)} 200 accessibilityHint="" 201 /> 202 <Btn 203 testID="bottomBarMessagesBtn" 204 icon={ 205 isAtMessages ? ( 206 <MessageFilled 207 width={iconWidth - 1} 208 style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} 209 /> 210 ) : ( 211 <Message 212 width={iconWidth - 1} 213 style={[styles.ctrlIcon, pal.text, styles.feedsIcon]} 214 /> 215 ) 216 } 217 onPress={onPressMessages} 218 notificationCount={numUnreadMessages.numUnread} 219 hasNew={numUnreadMessages.hasNew} 220 accessible={true} 221 accessibilityRole="tab" 222 accessibilityLabel={_(msg`Chat`)} 223 accessibilityHint={ 224 numUnreadMessages.count > 0 225 ? _( 226 msg`${plural(numUnreadMessages.numUnread ?? 0, { 227 one: '# unread item', 228 other: '# unread items', 229 })}` || '', 230 ) 231 : '' 232 } 233 /> 234 <Btn 235 testID="bottomBarNotificationsBtn" 236 icon={ 237 isAtNotifications ? ( 238 <BellFilled 239 width={iconWidth} 240 style={[styles.ctrlIcon, pal.text, styles.bellIcon]} 241 /> 242 ) : ( 243 <Bell 244 width={iconWidth} 245 style={[styles.ctrlIcon, pal.text, styles.bellIcon]} 246 /> 247 ) 248 } 249 onPress={onPressNotifications} 250 notificationCount={numUnreadNotifications} 251 accessible={true} 252 accessibilityRole="tab" 253 accessibilityLabel={_(msg`Notifications`)} 254 accessibilityHint={ 255 numUnreadNotifications === '' 256 ? '' 257 : _( 258 msg`${plural(numUnreadNotifications ?? 0, { 259 one: '# unread item', 260 other: '# unread items', 261 })}` || '', 262 ) 263 } 264 /> 265 <Btn 266 testID="bottomBarProfileBtn" 267 icon={ 268 <View style={styles.ctrlIconSizingWrapper}> 269 {isAtMyProfile ? ( 270 <View 271 style={[ 272 styles.ctrlIcon, 273 pal.text, 274 styles.profileIcon, 275 enableSquareAvatars 276 ? styles.onProfileSquare 277 : styles.onProfile, 278 { 279 borderColor: pal.text.color, 280 borderWidth: live ? 0 : enableSquareAvatars ? 1.5 : 1, 281 }, 282 ]}> 283 <UserAvatar 284 avatar={demoMode ? BOTTOM_BAR_AVI : profile?.avatar} 285 size={iconWidth - 2} 286 // See https://github.com/bluesky-social/social-app/pull/1801: 287 usePlainRNImage={true} 288 type={profile?.associated?.labeler ? 'labeler' : 'user'} 289 live={live} 290 hideLiveBadge 291 /> 292 </View> 293 ) : ( 294 <View 295 style={[ 296 styles.ctrlIcon, 297 pal.text, 298 styles.profileIcon, 299 { 300 borderWidth: live ? 0 : 1, 301 }, 302 ]}> 303 <UserAvatar 304 avatar={demoMode ? BOTTOM_BAR_AVI : profile?.avatar} 305 size={iconWidth - 2} 306 // See https://github.com/bluesky-social/social-app/pull/1801: 307 usePlainRNImage={true} 308 type={profile?.associated?.labeler ? 'labeler' : 'user'} 309 live={live} 310 hideLiveBadge 311 /> 312 </View> 313 )} 314 </View> 315 } 316 onPress={onPressProfile} 317 onLongPress={onLongPressProfile} 318 accessibilityRole="tab" 319 accessibilityLabel={_(msg`Profile`)} 320 accessibilityHint="" 321 /> 322 </> 323 ) : ( 324 <> 325 <View 326 style={{ 327 width: '100%', 328 flexDirection: 'row', 329 alignItems: 'center', 330 justifyContent: 'space-between', 331 paddingTop: 14, 332 paddingBottom: 2, 333 paddingLeft: 14, 334 paddingRight: 6, 335 gap: 8, 336 }}> 337 <View 338 style={{flexDirection: 'row', alignItems: 'center', gap: 8}}> 339 <Logo width={28} /> 340 <View style={{paddingTop: 4}}> 341 <Logotype width={80} fill={pal.text.color} /> 342 </View> 343 </View> 344 345 <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 346 <Button 347 onPress={showCreateAccount} 348 label={_(msg`Create account`)} 349 size="small" 350 variant="solid" 351 color="primary"> 352 <ButtonText> 353 <Trans>Create account</Trans> 354 </ButtonText> 355 </Button> 356 <Button 357 onPress={showSignIn} 358 label={_(msg`Sign in`)} 359 size="small" 360 variant="solid" 361 color="secondary"> 362 <ButtonText> 363 <Trans>Sign in</Trans> 364 </ButtonText> 365 </Button> 366 </View> 367 </View> 368 </> 369 )} 370 </Animated.View> 371 </> 372 ) 373} 374 375interface BtnProps 376 extends Pick< 377 React.ComponentProps<typeof PressableScale>, 378 | 'accessible' 379 | 'accessibilityRole' 380 | 'accessibilityHint' 381 | 'accessibilityLabel' 382 > { 383 testID?: string 384 icon: JSX.Element 385 notificationCount?: string 386 hasNew?: boolean 387 onPress?: (event: GestureResponderEvent) => void 388 onLongPress?: (event: GestureResponderEvent) => void 389} 390 391function Btn({ 392 testID, 393 icon, 394 hasNew, 395 notificationCount, 396 onPress, 397 onLongPress, 398 accessible, 399 accessibilityHint, 400 accessibilityLabel, 401}: BtnProps) { 402 const enableSquareButtons = useEnableSquareButtons() 403 const t = useTheme() 404 405 return ( 406 <PressableScale 407 testID={testID} 408 style={[styles.ctrl, a.flex_1]} 409 onPress={onPress} 410 onLongPress={onLongPress} 411 accessible={accessible} 412 accessibilityLabel={accessibilityLabel} 413 accessibilityHint={accessibilityHint} 414 targetScale={0.8} 415 accessibilityLargeContentTitle={accessibilityLabel} 416 accessibilityShowsLargeContentViewer> 417 {icon} 418 {notificationCount ? ( 419 <View 420 style={[ 421 styles.notificationCount, 422 enableSquareButtons ? a.rounded_sm : a.rounded_full, 423 {backgroundColor: t.palette.primary_500}, 424 ]}> 425 <Text style={styles.notificationCountLabel}>{notificationCount}</Text> 426 </View> 427 ) : hasNew ? ( 428 <View 429 style={[ 430 styles.hasNewBadge, 431 enableSquareButtons ? a.rounded_sm : a.rounded_full, 432 ]} 433 /> 434 ) : null} 435 </PressableScale> 436 ) 437}