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

Configure Feed

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

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