diff options
Diffstat (limited to 'src/view/shell')
-rw-r--r-- | src/view/shell/Composer.tsx | 27 | ||||
-rw-r--r-- | src/view/shell/Composer.web.tsx | 47 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 367 | ||||
-rw-r--r-- | src/view/shell/NavSignupCard.tsx | 61 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 186 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBarStyles.tsx | 3 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBarWeb.tsx | 77 | ||||
-rw-r--r-- | src/view/shell/createNativeStackNavigatorWithAuth.tsx | 150 | ||||
-rw-r--r-- | src/view/shell/desktop/Feeds.tsx | 62 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 295 | ||||
-rw-r--r-- | src/view/shell/desktop/RightNav.tsx | 136 | ||||
-rw-r--r-- | src/view/shell/desktop/Search.tsx | 209 | ||||
-rw-r--r-- | src/view/shell/index.tsx | 36 | ||||
-rw-r--r-- | src/view/shell/index.web.tsx | 49 |
14 files changed, 1072 insertions, 633 deletions
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index 219a594ed..d37ff4fb7 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -2,30 +2,21 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {Animated, Easing, Platform, StyleSheet, View} from 'react-native' import {ComposePost} from '../com/composer/Composer' -import {ComposerOpts} from 'state/models/ui/shell' +import {useComposerState} from 'state/shell/composer' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {usePalette} from 'lib/hooks/usePalette' export const Composer = observer(function ComposerImpl({ - active, winHeight, - replyTo, - onPost, - quote, - mention, }: { - active: boolean winHeight: number - replyTo?: ComposerOpts['replyTo'] - onPost?: ComposerOpts['onPost'] - quote?: ComposerOpts['quote'] - mention?: ComposerOpts['mention'] }) { + const state = useComposerState() const pal = usePalette('default') const initInterp = useAnimatedValue(0) useEffect(() => { - if (active) { + if (state) { Animated.timing(initInterp, { toValue: 1, duration: 300, @@ -35,7 +26,7 @@ export const Composer = observer(function ComposerImpl({ } else { initInterp.setValue(0) } - }, [initInterp, active]) + }, [initInterp, state]) const wrapperAnimStyle = { transform: [ { @@ -50,7 +41,7 @@ export const Composer = observer(function ComposerImpl({ // rendering // = - if (!active) { + if (!state) { return <View /> } @@ -60,10 +51,10 @@ export const Composer = observer(function ComposerImpl({ aria-modal accessibilityViewIsModal> <ComposePost - replyTo={replyTo} - onPost={onPost} - quote={quote} - mention={mention} + replyTo={state.replyTo} + onPost={state.onPost} + quote={state.quote} + mention={state.mention} /> </Animated.View> ) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index c3ec37e57..73f9f540e 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -1,40 +1,35 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, View} from 'react-native' +import Animated, {FadeIn, FadeInDown, FadeOut} from 'react-native-reanimated' import {ComposePost} from '../com/composer/Composer' -import {ComposerOpts} from 'state/models/ui/shell' +import {useComposerState} from 'state/shell/composer' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' const BOTTOM_BAR_HEIGHT = 61 -export const Composer = observer(function ComposerImpl({ - active, - replyTo, - quote, - onPost, - mention, -}: { - active: boolean - winHeight: number - replyTo?: ComposerOpts['replyTo'] - quote: ComposerOpts['quote'] - onPost?: ComposerOpts['onPost'] - mention?: ComposerOpts['mention'] -}) { +export function Composer({}: {winHeight: number}) { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() + const state = useComposerState() // rendering // = - if (!active) { + if (!state) { return <View /> } return ( - <View style={styles.mask} aria-modal accessibilityViewIsModal> - <View + <Animated.View + style={styles.mask} + aria-modal + accessibilityViewIsModal + entering={FadeIn.duration(100)} + exiting={FadeOut}> + <Animated.View + entering={FadeInDown.duration(150)} + exiting={FadeOut} style={[ styles.container, isMobile && styles.containerMobile, @@ -42,15 +37,15 @@ export const Composer = observer(function ComposerImpl({ pal.border, ]}> <ComposePost - replyTo={replyTo} - quote={quote} - onPost={onPost} - mention={mention} + replyTo={state.replyTo} + quote={state.quote} + onPost={state.onPost} + mention={state.mention} /> - </View> - </View> + </Animated.View> + </Animated.View> ) -}) +} const styles = StyleSheet.create({ mask: { diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 7f5e6c5e5..459a021c4 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -10,14 +10,13 @@ import { ViewStyle, } from 'react-native' import {useNavigation, StackActions} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {useQueryClient} from '@tanstack/react-query' import {s, colors} from 'lib/styles' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' -import {useStores} from 'state/index' import { HomeIcon, HomeIconSolid, @@ -42,20 +41,81 @@ import {getTabState, TabState} from 'lib/routes/helpers' import {NavigationProp} from 'lib/routes/types' import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' import {isWeb} from 'platform/detection' -import {formatCount, formatCountShortOnly} from 'view/com/util/numeric/format' +import {formatCountShortOnly} from 'view/com/util/numeric/format' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useSetDrawerOpen} from '#/state/shell' +import {useModalControls} from '#/state/modals' +import {useSession, SessionAccount} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {emitSoftReset} from '#/state/events' +import {useInviteCodesQuery} from '#/state/queries/invites' +import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' +import {NavSignupCard} from '#/view/shell/NavSignupCard' +import {truncateAndInvalidate} from '#/state/queries/util' -export const DrawerContent = observer(function DrawerContentImpl() { +export function DrawerProfileCard({ + account, + onPressProfile, +}: { + account: SessionAccount + onPressProfile: () => void +}) { + const {_} = useLingui() + const pal = usePalette('default') + const {data: profile} = useProfileQuery({did: account.did}) + + return ( + <TouchableOpacity + testID="profileCardButton" + accessibilityLabel={_(msg`Profile`)} + accessibilityHint="Navigates to your profile" + onPress={onPressProfile}> + <UserAvatar + size={80} + avatar={profile?.avatar} + // See https://github.com/bluesky-social/social-app/pull/1801: + usePlainRNImage={true} + /> + <Text + type="title-lg" + style={[pal.text, s.bold, styles.profileCardDisplayName]} + numberOfLines={1}> + {profile?.displayName || account.handle} + </Text> + <Text + type="2xl" + style={[pal.textLight, styles.profileCardHandle]} + numberOfLines={1}> + @{account.handle} + </Text> + <Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}> + <Text type="xl-medium" style={pal.text}> + {formatCountShortOnly(profile?.followersCount ?? 0)} + </Text>{' '} + {pluralize(profile?.followersCount || 0, 'follower')} ·{' '} + <Text type="xl-medium" style={pal.text}> + {formatCountShortOnly(profile?.followsCount ?? 0)} + </Text>{' '} + following + </Text> + </TouchableOpacity> + ) +} + +export function DrawerContent() { const theme = useTheme() const pal = usePalette('default') - const store = useStores() + const {_} = useLingui() + const queryClient = useQueryClient() const setDrawerOpen = useSetDrawerOpen() const navigation = useNavigation<NavigationProp>() const {track} = useAnalytics() const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = useNavigationTabState() - - const {notifications} = store.me + const {hasSession, currentAccount} = useSession() + const numUnreadNotifications = useUnreadNotifications() // events // = @@ -68,7 +128,7 @@ export const DrawerContent = observer(function DrawerContentImpl() { if (isWeb) { // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh if (tab === 'MyProfile') { - navigation.navigate('Profile', {name: store.me.handle}) + navigation.navigate('Profile', {name: currentAccount!.handle}) } else { // @ts-ignore must be Home, Search, Notifications, or MyProfile navigation.navigate(tab) @@ -76,16 +136,20 @@ export const DrawerContent = observer(function DrawerContentImpl() { } else { const tabState = getTabState(state, tab) if (tabState === TabState.InsideAtRoot) { - store.emitScreenSoftReset() + emitSoftReset() } else if (tabState === TabState.Inside) { navigation.dispatch(StackActions.popToTop()) } else { + if (tab === 'Notifications') { + // fetch new notifs on view + truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) + } // @ts-ignore must be Home, Search, Notifications, or MyProfile navigation.navigate(`${tab}Tab`) } } }, - [store, track, navigation, setDrawerOpen], + [track, navigation, setDrawerOpen, currentAccount, queryClient], ) const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab]) @@ -131,11 +195,11 @@ export const DrawerContent = observer(function DrawerContentImpl() { track('Menu:FeedbackClicked') Linking.openURL( FEEDBACK_FORM_URL({ - email: store.session.currentSession?.email, - handle: store.session.currentSession?.handle, + email: currentAccount?.email, + handle: currentAccount?.handle, }), ) - }, [track, store.session.currentSession]) + }, [track, currentAccount]) const onPressHelp = React.useCallback(() => { track('Menu:HelpClicked') @@ -154,48 +218,20 @@ export const DrawerContent = observer(function DrawerContentImpl() { ]}> <SafeAreaView style={s.flex1}> <ScrollView style={styles.main}> - <View style={{}}> - <TouchableOpacity - testID="profileCardButton" - accessibilityLabel="Profile" - accessibilityHint="Navigates to your profile" - onPress={onPressProfile}> - <UserAvatar - size={80} - avatar={store.me.avatar} - // See https://github.com/bluesky-social/social-app/pull/1801: - usePlainRNImage={true} + {hasSession && currentAccount ? ( + <View style={{}}> + <DrawerProfileCard + account={currentAccount} + onPressProfile={onPressProfile} /> - <Text - type="title-lg" - style={[pal.text, s.bold, styles.profileCardDisplayName]} - numberOfLines={1}> - {store.me.displayName || store.me.handle} - </Text> - <Text - type="2xl" - style={[pal.textLight, styles.profileCardHandle]} - numberOfLines={1}> - @{store.me.handle} - </Text> - <Text - type="xl" - style={[pal.textLight, styles.profileCardFollowers]}> - <Text type="xl-medium" style={pal.text}> - {formatCountShortOnly(store.me.followersCount ?? 0)} - </Text>{' '} - {pluralize(store.me.followersCount || 0, 'follower')} ·{' '} - <Text type="xl-medium" style={pal.text}> - {formatCountShortOnly(store.me.followsCount ?? 0)} - </Text>{' '} - following - </Text> - </TouchableOpacity> - </View> + </View> + ) : ( + <NavSignupCard /> + )} - <InviteCodes style={{paddingLeft: 0}} /> + {hasSession && <InviteCodes style={{paddingLeft: 0}} />} - <View style={{height: 10}} /> + {hasSession && <View style={{height: 10}} />} <MenuItem icon={ @@ -213,8 +249,8 @@ export const DrawerContent = observer(function DrawerContentImpl() { /> ) } - label="Search" - accessibilityLabel="Search" + label={_(msg`Search`)} + accessibilityLabel={_(msg`Search`)} accessibilityHint="" bold={isAtSearch} onPress={onPressSearch} @@ -235,39 +271,43 @@ export const DrawerContent = observer(function DrawerContentImpl() { /> ) } - label="Home" - accessibilityLabel="Home" + label={_(msg`Home`)} + accessibilityLabel={_(msg`Home`)} accessibilityHint="" bold={isAtHome} onPress={onPressHome} /> - <MenuItem - icon={ - isAtNotifications ? ( - <BellIconSolid - style={pal.text as StyleProp<ViewStyle>} - size="24" - strokeWidth={1.7} - /> - ) : ( - <BellIcon - style={pal.text as StyleProp<ViewStyle>} - size="24" - strokeWidth={1.7} - /> - ) - } - label="Notifications" - accessibilityLabel="Notifications" - accessibilityHint={ - notifications.unreadCountLabel === '' - ? '' - : `${notifications.unreadCountLabel} unread` - } - count={notifications.unreadCountLabel} - bold={isAtNotifications} - onPress={onPressNotifications} - /> + + {hasSession && ( + <MenuItem + icon={ + isAtNotifications ? ( + <BellIconSolid + style={pal.text as StyleProp<ViewStyle>} + size="24" + strokeWidth={1.7} + /> + ) : ( + <BellIcon + style={pal.text as StyleProp<ViewStyle>} + size="24" + strokeWidth={1.7} + /> + ) + } + label={_(msg`Notifications`)} + accessibilityLabel={_(msg`Notifications`)} + accessibilityHint={ + numUnreadNotifications === '' + ? '' + : `${numUnreadNotifications} unread` + } + count={numUnreadNotifications} + bold={isAtNotifications} + onPress={onPressNotifications} + /> + )} + <MenuItem icon={ isAtFeeds ? ( @@ -284,68 +324,74 @@ export const DrawerContent = observer(function DrawerContentImpl() { /> ) } - label="Feeds" - accessibilityLabel="Feeds" + label={_(msg`Feeds`)} + accessibilityLabel={_(msg`Feeds`)} accessibilityHint="" bold={isAtFeeds} onPress={onPressMyFeeds} /> - <MenuItem - icon={<ListIcon strokeWidth={2} style={pal.text} size={26} />} - label="Lists" - accessibilityLabel="Lists" - accessibilityHint="" - onPress={onPressLists} - /> - <MenuItem - icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />} - label="Moderation" - accessibilityLabel="Moderation" - accessibilityHint="" - onPress={onPressModeration} - /> - <MenuItem - icon={ - isAtMyProfile ? ( - <UserIconSolid - style={pal.text as StyleProp<ViewStyle>} - size="26" - strokeWidth={1.5} - /> - ) : ( - <UserIcon - style={pal.text as StyleProp<ViewStyle>} - size="26" - strokeWidth={1.5} - /> - ) - } - label="Profile" - accessibilityLabel="Profile" - accessibilityHint="" - onPress={onPressProfile} - /> - <MenuItem - icon={ - <CogIcon - style={pal.text as StyleProp<ViewStyle>} - size="26" - strokeWidth={1.75} + + {hasSession && ( + <> + <MenuItem + icon={<ListIcon strokeWidth={2} style={pal.text} size={26} />} + label={_(msg`Lists`)} + accessibilityLabel={_(msg`Lists`)} + accessibilityHint="" + onPress={onPressLists} /> - } - label="Settings" - accessibilityLabel="Settings" - accessibilityHint="" - onPress={onPressSettings} - /> + <MenuItem + icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />} + label={_(msg`Moderation`)} + accessibilityLabel={_(msg`Moderation`)} + accessibilityHint="" + onPress={onPressModeration} + /> + <MenuItem + icon={ + isAtMyProfile ? ( + <UserIconSolid + style={pal.text as StyleProp<ViewStyle>} + size="26" + strokeWidth={1.5} + /> + ) : ( + <UserIcon + style={pal.text as StyleProp<ViewStyle>} + size="26" + strokeWidth={1.5} + /> + ) + } + label={_(msg`Profile`)} + accessibilityLabel={_(msg`Profile`)} + accessibilityHint="" + onPress={onPressProfile} + /> + <MenuItem + icon={ + <CogIcon + style={pal.text as StyleProp<ViewStyle>} + size="26" + strokeWidth={1.75} + /> + } + label={_(msg`Settings`)} + accessibilityLabel={_(msg`Settings`)} + accessibilityHint="" + onPress={onPressSettings} + /> + </> + )} <View style={styles.smallSpacer} /> <View style={styles.smallSpacer} /> </ScrollView> + <View style={styles.footer}> <TouchableOpacity accessibilityRole="link" - accessibilityLabel="Send feedback" + accessibilityLabel={_(msg`Send feedback`)} accessibilityHint="" onPress={onPressFeedback} style={[ @@ -361,24 +407,24 @@ export const DrawerContent = observer(function DrawerContentImpl() { icon={['far', 'message']} /> <Text type="lg-medium" style={[pal.link, s.pl10]}> - Feedback + <Trans>Feedback</Trans> </Text> </TouchableOpacity> <TouchableOpacity accessibilityRole="link" - accessibilityLabel="Send feedback" + accessibilityLabel={_(msg`Send feedback`)} accessibilityHint="" onPress={onPressHelp} style={[styles.footerBtn]}> <Text type="lg-medium" style={[pal.link, s.pl10]}> - Help + <Trans>Help</Trans> </Text> </TouchableOpacity> </View> </SafeAreaView> </View> ) -}) +} interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> { icon: JSX.Element @@ -432,50 +478,54 @@ function MenuItem({ ) } -const InviteCodes = observer(function InviteCodesImpl({ - style, -}: { - style?: StyleProp<ViewStyle> -}) { +function InviteCodes({style}: {style?: StyleProp<ViewStyle>}) { const {track} = useAnalytics() - const store = useStores() const setDrawerOpen = useSetDrawerOpen() const pal = usePalette('default') - const {invitesAvailable} = store.me + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 + const {openModal} = useModalControls() + const {_} = useLingui() + const onPress = React.useCallback(() => { track('Menu:ItemClicked', {url: '#invite-codes'}) setDrawerOpen(false) - store.shell.openModal({name: 'invite-codes'}) - }, [store, track, setDrawerOpen]) + openModal({name: 'invite-codes'}) + }, [openModal, track, setDrawerOpen]) + return ( <TouchableOpacity testID="menuItemInviteCodes" style={[styles.inviteCodes, style]} onPress={onPress} accessibilityRole="button" - accessibilityLabel={ - invitesAvailable === 1 - ? 'Invite codes: 1 available' - : `Invite codes: ${invitesAvailable} available` - } - accessibilityHint="Opens list of invite codes"> + accessibilityLabel={_(msg`Invite codes: ${invitesAvailable} available`)} + accessibilityHint={_(msg`Opens list of invite codes`)} + disabled={invites?.disabled}> <FontAwesomeIcon icon="ticket" style={[ styles.inviteCodesIcon, - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, + invitesAvailable > 0 ? pal.link : pal.textLight, ]} size={18} /> <Text type="lg-medium" - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> - {formatCount(store.me.invitesAvailable)} invite{' '} - {pluralize(store.me.invitesAvailable, 'code')} + style={invitesAvailable > 0 ? pal.link : pal.textLight}> + {invites?.disabled ? ( + <Trans> + Your invite codes are hidden when logged in using an App Password + </Trans> + ) : invitesAvailable === 1 ? ( + <Trans>{invitesAvailable} invite code available</Trans> + ) : ( + <Trans>{invitesAvailable} invite codes available</Trans> + )} </Text> </TouchableOpacity> ) -}) +} const styles = StyleSheet.create({ view: { @@ -548,10 +598,11 @@ const styles = StyleSheet.create({ paddingLeft: 22, paddingVertical: 8, flexDirection: 'row', - alignItems: 'center', }, inviteCodesIcon: { marginRight: 6, + flexShrink: 0, + marginTop: 2, }, footer: { diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx new file mode 100644 index 000000000..7026dd2a6 --- /dev/null +++ b/src/view/shell/NavSignupCard.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {DefaultAvatar} from '#/view/com/util/UserAvatar' +import {Text} from '#/view/com/util/text/Text' +import {Button} from '#/view/com/util/forms/Button' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' + +export function NavSignupCard() { + const {_} = useLingui() + const pal = usePalette('default') + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeAllActiveElements = useCloseAllActiveElements() + + const showLoggedOut = React.useCallback(() => { + closeAllActiveElements() + setShowLoggedOut(true) + }, [setShowLoggedOut, closeAllActiveElements]) + + return ( + <View + style={{ + alignItems: 'flex-start', + paddingTop: 6, + marginBottom: 24, + }}> + <DefaultAvatar type="user" size={48} /> + + <View style={{paddingTop: 12}}> + <Text type="md" style={[pal.text, s.bold]}> + <Trans>Sign up or sign in to join the conversation</Trans> + </Text> + </View> + + <View style={{flexDirection: 'row', paddingTop: 12, gap: 8}}> + <Button + onPress={showLoggedOut} + accessibilityHint={_(msg`Sign up`)} + accessibilityLabel={_(msg`Sign up`)}> + <Text type="md" style={[{color: 'white'}, s.bold]}> + <Trans>Sign up</Trans> + </Text> + </Button> + <Button + type="default" + onPress={showLoggedOut} + accessibilityHint={_(msg`Sign in`)} + accessibilityLabel={_(msg`Sign in`)}> + <Text type="md" style={[pal.text, s.bold]}> + Sign in + </Text> + </Button> + </View> + </View> + ) +} diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index d360ceead..746b4d123 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -1,12 +1,11 @@ import React, {ComponentProps} from 'react' import {GestureResponderEvent, TouchableOpacity, View} from 'react-native' import Animated from 'react-native-reanimated' +import {useQueryClient} from '@tanstack/react-query' import {StackActions} from '@react-navigation/native' import {BottomTabBarProps} from '@react-navigation/bottom-tabs' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {observer} from 'mobx-react-lite' import {Text} from 'view/com/util/text/Text' -import {useStores} from 'state/index' import {useAnalytics} from 'lib/analytics/analytics' import {clamp} from 'lib/numbers' import { @@ -24,21 +23,33 @@ import {styles} from './BottomBarStyles' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' import {UserAvatar} from 'view/com/util/UserAvatar' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {useModalControls} from '#/state/modals' +import {useShellLayout} from '#/state/shell/shell-layout' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {emitSoftReset} from '#/state/events' +import {useSession} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' +import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' +import {truncateAndInvalidate} from '#/state/queries/util' type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' -export const BottomBar = observer(function BottomBarImpl({ - navigation, -}: BottomTabBarProps) { - const store = useStores() +export function BottomBar({navigation}: BottomTabBarProps) { + const {openModal} = useModalControls() + const {hasSession, currentAccount} = useSession() const pal = usePalette('default') + const {_} = useLingui() + const queryClient = useQueryClient() const safeAreaInsets = useSafeAreaInsets() const {track} = useAnalytics() + const {footerHeight} = useShellLayout() const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = useNavigationTabState() - - const {minimalShellMode, footerMinimalShellTransform} = useMinimalShellMode() - const {notifications} = store.me + const numUnreadNotifications = useUnreadNotifications() + const {footerMinimalShellTransform} = useMinimalShellMode() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) const onPressTab = React.useCallback( (tab: TabOptions) => { @@ -46,14 +57,18 @@ export const BottomBar = observer(function BottomBarImpl({ const state = navigation.getState() const tabState = getTabState(state, tab) if (tabState === TabState.InsideAtRoot) { - store.emitScreenSoftReset() + emitSoftReset() } else if (tabState === TabState.Inside) { navigation.dispatch(StackActions.popToTop()) } else { + if (tab === 'Notifications') { + // fetch new notifs on view + truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) + } navigation.navigate(`${tab}Tab`) } }, - [store, track, navigation], + [track, navigation, queryClient], ) const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab]) const onPressSearch = React.useCallback( @@ -72,8 +87,8 @@ export const BottomBar = observer(function BottomBarImpl({ onPressTab('MyProfile') }, [onPressTab]) const onLongPressProfile = React.useCallback(() => { - store.shell.openModal({name: 'switch-account'}) - }, [store]) + openModal({name: 'switch-account'}) + }, [openModal]) return ( <Animated.View @@ -83,8 +98,10 @@ export const BottomBar = observer(function BottomBarImpl({ pal.border, {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)}, footerMinimalShellTransform, - minimalShellMode && styles.disabled, - ]}> + ]} + onLayout={e => { + footerHeight.value = e.nativeEvent.layout.height + }}> <Btn testID="bottomBarHomeBtn" icon={ @@ -104,7 +121,7 @@ export const BottomBar = observer(function BottomBarImpl({ } onPress={onPressHome} accessibilityRole="tab" - accessibilityLabel="Home" + accessibilityLabel={_(msg`Home`)} accessibilityHint="" /> <Btn @@ -126,7 +143,7 @@ export const BottomBar = observer(function BottomBarImpl({ } onPress={onPressSearch} accessibilityRole="search" - accessibilityLabel="Search" + accessibilityLabel={_(msg`Search`)} accessibilityHint="" /> <Btn @@ -148,78 +165,83 @@ export const BottomBar = observer(function BottomBarImpl({ } onPress={onPressFeeds} accessibilityRole="tab" - accessibilityLabel="Feeds" + accessibilityLabel={_(msg`Feeds`)} accessibilityHint="" /> - <Btn - testID="bottomBarNotificationsBtn" - icon={ - isAtNotifications ? ( - <BellIconSolid - size={24} - strokeWidth={1.9} - style={[styles.ctrlIcon, pal.text, styles.bellIcon]} - /> - ) : ( - <BellIcon - size={24} - strokeWidth={1.9} - style={[styles.ctrlIcon, pal.text, styles.bellIcon]} - /> - ) - } - onPress={onPressNotifications} - notificationCount={notifications.unreadCountLabel} - accessible={true} - accessibilityRole="tab" - accessibilityLabel="Notifications" - accessibilityHint={ - notifications.unreadCountLabel === '' - ? '' - : `${notifications.unreadCountLabel} unread` - } - /> - <Btn - testID="bottomBarProfileBtn" - icon={ - <View style={styles.ctrlIconSizingWrapper}> - {isAtMyProfile ? ( - <View - style={[ - styles.ctrlIcon, - pal.text, - styles.profileIcon, - styles.onProfile, - {borderColor: pal.text.color}, - ]}> - <UserAvatar - avatar={store.me.avatar} - size={27} - // See https://github.com/bluesky-social/social-app/pull/1801: - usePlainRNImage={true} + + {hasSession && ( + <> + <Btn + testID="bottomBarNotificationsBtn" + icon={ + isAtNotifications ? ( + <BellIconSolid + size={24} + strokeWidth={1.9} + style={[styles.ctrlIcon, pal.text, styles.bellIcon]} /> - </View> - ) : ( - <View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}> - <UserAvatar - avatar={store.me.avatar} - size={28} - // See https://github.com/bluesky-social/social-app/pull/1801: - usePlainRNImage={true} + ) : ( + <BellIcon + size={24} + strokeWidth={1.9} + style={[styles.ctrlIcon, pal.text, styles.bellIcon]} /> + ) + } + onPress={onPressNotifications} + notificationCount={numUnreadNotifications} + accessible={true} + accessibilityRole="tab" + accessibilityLabel={_(msg`Notifications`)} + accessibilityHint={ + numUnreadNotifications === '' + ? '' + : `${numUnreadNotifications} unread` + } + /> + <Btn + testID="bottomBarProfileBtn" + icon={ + <View style={styles.ctrlIconSizingWrapper}> + {isAtMyProfile ? ( + <View + style={[ + styles.ctrlIcon, + pal.text, + styles.profileIcon, + styles.onProfile, + {borderColor: pal.text.color}, + ]}> + <UserAvatar + avatar={profile?.avatar} + size={27} + // See https://github.com/bluesky-social/social-app/pull/1801: + usePlainRNImage={true} + /> + </View> + ) : ( + <View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}> + <UserAvatar + avatar={profile?.avatar} + size={28} + // See https://github.com/bluesky-social/social-app/pull/1801: + usePlainRNImage={true} + /> + </View> + )} </View> - )} - </View> - } - onPress={onPressProfile} - onLongPress={onLongPressProfile} - accessibilityRole="tab" - accessibilityLabel="Profile" - accessibilityHint="" - /> + } + onPress={onPressProfile} + onLongPress={onLongPressProfile} + accessibilityRole="tab" + accessibilityLabel={_(msg`Profile`)} + accessibilityHint="" + /> + </> + )} </Animated.View> ) -}) +} interface BtnProps extends Pick< diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx index c175ed848..ae9381440 100644 --- a/src/view/shell/bottom-bar/BottomBarStyles.tsx +++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx @@ -65,7 +65,4 @@ export const styles = StyleSheet.create({ borderWidth: 1, borderRadius: 100, }, - disabled: { - pointerEvents: 'none', - }, }) diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index ebcc527a1..3a60bd3b1 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -1,6 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useNavigationState} from '@react-navigation/native' import Animated from 'react-native-reanimated' @@ -23,9 +21,10 @@ import {Link} from 'view/com/util/Link' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {makeProfileLink} from 'lib/routes/links' import {CommonNavigatorParams} from 'lib/routes/types' +import {useSession} from '#/state/session' -export const BottomBarWeb = observer(function BottomBarWebImpl() { - const store = useStores() +export function BottomBarWeb() { + const {hasSession, currentAccount} = useSession() const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() const {footerMinimalShellTransform} = useMinimalShellMode() @@ -76,55 +75,69 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() { ) }} </NavItem> - <NavItem routeName="Notifications" href="/notifications"> - {({isActive}) => { - const Icon = isActive ? BellIconSolid : BellIcon - return ( - <Icon - size={24} - strokeWidth={1.9} - style={[styles.ctrlIcon, pal.text, styles.bellIcon]} - /> - ) - }} - </NavItem> - <NavItem routeName="Profile" href={makeProfileLink(store.me)}> - {({isActive}) => { - const Icon = isActive ? UserIconSolid : UserIcon - return ( - <Icon - size={28} - strokeWidth={1.5} - style={[styles.ctrlIcon, pal.text, styles.profileIcon]} - /> - ) - }} - </NavItem> + + {hasSession && ( + <> + <NavItem routeName="Notifications" href="/notifications"> + {({isActive}) => { + const Icon = isActive ? BellIconSolid : BellIcon + return ( + <Icon + size={24} + strokeWidth={1.9} + style={[styles.ctrlIcon, pal.text, styles.bellIcon]} + /> + ) + }} + </NavItem> + <NavItem + routeName="Profile" + href={ + currentAccount + ? makeProfileLink({ + did: currentAccount.did, + handle: currentAccount.handle, + }) + : '/' + }> + {({isActive}) => { + const Icon = isActive ? UserIconSolid : UserIcon + return ( + <Icon + size={28} + strokeWidth={1.5} + style={[styles.ctrlIcon, pal.text, styles.profileIcon]} + /> + ) + }} + </NavItem> + </> + )} </Animated.View> ) -}) +} const NavItem: React.FC<{ children: (props: {isActive: boolean}) => React.ReactChild href: string routeName: string }> = ({children, href, routeName}) => { + const {currentAccount} = useSession() const currentRoute = useNavigationState(state => { if (!state) { return {name: 'Home'} } return getCurrentRoute(state) }) - const store = useStores() const isActive = currentRoute.name === 'Profile' ? isTab(currentRoute.name, routeName) && (currentRoute.params as CommonNavigatorParams['Profile']).name === - store.me.handle + currentAccount?.handle : isTab(currentRoute.name, routeName) return ( - <Link href={href} style={styles.ctrl}> + <Link href={href} style={styles.ctrl} navigationAction="navigate"> {children({isActive})} </Link> ) diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx new file mode 100644 index 000000000..c7b5d1d2e --- /dev/null +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -0,0 +1,150 @@ +import * as React from 'react' +import {View} from 'react-native' + +// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts +// MIT License +// Copyright (c) 2017 React Navigation Contributors + +import { + createNavigatorFactory, + EventArg, + ParamListBase, + StackActionHelpers, + StackActions, + StackNavigationState, + StackRouter, + StackRouterOptions, + useNavigationBuilder, +} from '@react-navigation/native' +import type { + NativeStackNavigationEventMap, + NativeStackNavigationOptions, +} from '@react-navigation/native-stack' +import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types' +import {NativeStackView} from '@react-navigation/native-stack' + +import {BottomBarWeb} from './bottom-bar/BottomBarWeb' +import {DesktopLeftNav} from './desktop/LeftNav' +import {DesktopRightNav} from './desktop/RightNav' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {useOnboardingState} from '#/state/shell' +import { + useLoggedOutView, + useLoggedOutViewControls, +} from '#/state/shell/logged-out' +import {useSession} from '#/state/session' +import {isWeb} from 'platform/detection' +import {LoggedOut} from '../com/auth/LoggedOut' +import {Onboarding} from '../com/auth/Onboarding' + +type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { + requireAuth?: boolean +} + +function NativeStackNavigator({ + id, + initialRouteName, + children, + screenListeners, + screenOptions, + ...rest +}: NativeStackNavigatorProps) { + // --- this is copy and pasted from the original native stack navigator --- + const {state, descriptors, navigation, NavigationContent} = + useNavigationBuilder< + StackNavigationState<ParamListBase>, + StackRouterOptions, + StackActionHelpers<ParamListBase>, + NativeStackNavigationOptionsWithAuth, + NativeStackNavigationEventMap + >(StackRouter, { + id, + initialRouteName, + children, + screenListeners, + screenOptions, + }) + React.useEffect( + () => + // @ts-expect-error: there may not be a tab navigator in parent + navigation?.addListener?.('tabPress', (e: any) => { + const isFocused = navigation.isFocused() + + // Run the operation in the next frame so we're sure all listeners have been run + // This is necessary to know if preventDefault() has been called + requestAnimationFrame(() => { + if ( + state.index > 0 && + isFocused && + !(e as EventArg<'tabPress', true>).defaultPrevented + ) { + // When user taps on already focused tab and we're inside the tab, + // reset the stack to replicate native behaviour + navigation.dispatch({ + ...StackActions.popToTop(), + target: state.key, + }) + } + }) + }), + [navigation, state.index, state.key], + ) + + // --- our custom logic starts here --- + const {hasSession} = useSession() + const activeRoute = state.routes[state.index] + const activeDescriptor = descriptors[activeRoute.key] + const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false + const onboardingState = useOnboardingState() + const {showLoggedOut} = useLoggedOutView() + const {setShowLoggedOut} = useLoggedOutViewControls() + const {isMobile} = useWebMediaQueries() + if (activeRouteRequiresAuth && !hasSession) { + return <LoggedOut /> + } + if (showLoggedOut) { + return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> + } + if (onboardingState.isActive) { + return <Onboarding /> + } + const newDescriptors: typeof descriptors = {} + for (let key in descriptors) { + const descriptor = descriptors[key] + const requireAuth = descriptor.options.requireAuth ?? false + newDescriptors[key] = { + ...descriptor, + render() { + if (requireAuth && !hasSession) { + return <View /> + } else { + return descriptor.render() + } + }, + } + } + return ( + <NavigationContent> + <NativeStackView + {...rest} + state={state} + navigation={navigation} + descriptors={newDescriptors} + /> + {isWeb && isMobile && <BottomBarWeb />} + {isWeb && !isMobile && ( + <> + <DesktopLeftNav /> + <DesktopRightNav /> + </> + )} + </NavigationContent> + ) +} + +export const createNativeStackNavigatorWithAuth = createNavigatorFactory< + StackNavigationState<ParamListBase>, + NativeStackNavigationOptionsWithAuth, + NativeStackNavigationEventMap, + typeof NativeStackNavigator +>(NativeStackNavigator) diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 3237d2cdd..ff51ffe22 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -1,17 +1,17 @@ import React from 'react' import {View, StyleSheet} from 'react-native' import {useNavigationState} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {useDesktopRightNavItems} from 'lib/hooks/useDesktopRightNavItems' import {TextLink} from 'view/com/util/Link' import {getCurrentRoute} from 'lib/routes/helpers' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {usePinnedFeedsInfos} from '#/state/queries/feed' -export const DesktopFeeds = observer(function DesktopFeeds() { - const store = useStores() +export function DesktopFeeds() { const pal = usePalette('default') - const items = useDesktopRightNavItems(store.preferences.pinnedFeeds) + const {_} = useLingui() + const {feeds} = usePinnedFeedsInfos() const route = useNavigationState(state => { if (!state) { @@ -23,40 +23,40 @@ export const DesktopFeeds = observer(function DesktopFeeds() { return ( <View style={[styles.container, pal.view, pal.border]}> <FeedItem href="/" title="Following" current={route.name === 'Home'} /> - {items.map(item => { - try { - const params = route.params as Record<string, string> - const routeName = - item.collection === 'app.bsky.feed.generator' - ? 'ProfileFeed' - : 'ProfileList' - return ( - <FeedItem - key={item.uri} - href={item.href} - title={item.displayName} - current={ - route.name === routeName && - params.name === item.hostname && - params.rkey === item.rkey - } - /> - ) - } catch { - return null - } - })} + {feeds + .filter(f => f.displayName !== 'Following') + .map(feed => { + try { + const params = route.params as Record<string, string> + const routeName = + feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList' + return ( + <FeedItem + key={feed.uri} + href={feed.route.href} + title={feed.displayName} + current={ + route.name === routeName && + params.name === feed.route.params.name && + params.rkey === feed.route.params.rkey + } + /> + ) + } catch { + return null + } + })} <View style={{paddingTop: 8, paddingBottom: 6}}> <TextLink type="lg" href="/feeds" - text="More feeds" + text={_(msg`More feeds`)} style={[pal.link]} /> </View> </View> ) -}) +} function FeedItem({ title, diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 39271605c..2ed294501 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {PressableWithHover} from 'view/com/util/PressableWithHover' import { @@ -16,7 +15,6 @@ import {UserAvatar} from 'view/com/util/UserAvatar' import {Link} from 'view/com/util/Link' import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s, colors} from 'lib/styles' import { @@ -39,18 +37,36 @@ import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types' import {router} from '../../../routes' import {makeProfileLink} from 'lib/routes/links' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {useComposerControls} from '#/state/shell/composer' +import {useFetchHandle} from '#/state/queries/handle' +import {emitSoftReset} from '#/state/events' +import {useQueryClient} from '@tanstack/react-query' +import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' +import {NavSignupCard} from '#/view/shell/NavSignupCard' +import {truncateAndInvalidate} from '#/state/queries/util' -const ProfileCard = observer(function ProfileCardImpl() { - const store = useStores() +function ProfileCard() { + const {currentAccount} = useSession() + const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did}) const {isDesktop} = useWebMediaQueries() + const {_} = useLingui() const size = 48 - return store.me.handle ? ( + + return !isLoading && profile ? ( <Link - href={makeProfileLink(store.me)} + href={makeProfileLink({ + did: currentAccount!.did, + handle: currentAccount!.handle, + })} style={[styles.profileCard, !isDesktop && styles.profileCardTablet]} - title="My Profile" + title={_(msg`My Profile`)} asAnchor> - <UserAvatar avatar={store.me.avatar} size={size} /> + <UserAvatar avatar={profile.avatar} size={size} /> </Link> ) : ( <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}> @@ -61,12 +77,13 @@ const ProfileCard = observer(function ProfileCardImpl() { /> </View> ) -}) +} function BackBtn() { const {isTablet} = useWebMediaQueries() const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const {_} = useLingui() const shouldShow = useNavigationState(state => !isStateAtTabRoot(state)) const onPressBack = React.useCallback(() => { @@ -86,7 +103,7 @@ function BackBtn() { onPress={onPressBack} style={styles.backBtn} accessibilityRole="button" - accessibilityLabel="Go back" + accessibilityLabel={_(msg`Go back`)} accessibilityHint=""> <FontAwesomeIcon size={24} @@ -104,15 +121,10 @@ interface NavItemProps { iconFilled: JSX.Element label: string } -const NavItem = observer(function NavItemImpl({ - count, - href, - icon, - iconFilled, - label, -}: NavItemProps) { +function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { const pal = usePalette('default') - const store = useStores() + const queryClient = useQueryClient() + const {currentAccount} = useSession() const {isDesktop, isTablet} = useWebMediaQueries() const [pathName] = React.useMemo(() => router.matchPath(href), [href]) const currentRouteInfo = useNavigationState(state => { @@ -125,7 +137,7 @@ const NavItem = observer(function NavItemImpl({ currentRouteInfo.name === 'Profile' ? isTab(currentRouteInfo.name, pathName) && (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === - store.me.handle + currentAccount?.handle : isTab(currentRouteInfo.name, pathName) const {onPress} = useLinkProps({to: href}) const onPressWrapped = React.useCallback( @@ -135,12 +147,16 @@ const NavItem = observer(function NavItemImpl({ } e.preventDefault() if (isCurrent) { - store.emitScreenSoftReset() + emitSoftReset() } else { + if (href === '/notifications') { + // fetch new notifs on view + truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) + } onPress() } }, - [onPress, isCurrent, store], + [onPress, isCurrent, queryClient, href], ) return ( @@ -179,12 +195,16 @@ const NavItem = observer(function NavItemImpl({ )} </PressableWithHover> ) -}) +} function ComposeBtn() { - const store = useStores() + const {currentAccount} = useSession() const {getState} = useNavigation() + const {openComposer} = useComposerControls() + const {_} = useLingui() const {isTablet} = useWebMediaQueries() + const [isFetchingHandle, setIsFetchingHandle] = React.useState(false) + const fetchHandle = useFetchHandle() const getProfileHandle = async () => { const {routes} = getState() @@ -196,13 +216,21 @@ function ComposeBtn() { ).name if (handle.startsWith('did:')) { - const cached = await store.profiles.cache.get(handle) - const profile = cached ? cached.data : undefined - // if we can't resolve handle, set to undefined - handle = profile?.handle || undefined + try { + setIsFetchingHandle(true) + handle = await fetchHandle(handle) + } catch (e) { + handle = undefined + } finally { + setIsFetchingHandle(false) + } } - if (!handle || handle === store.me.handle || handle === 'handle.invalid') + if ( + !handle || + handle === currentAccount?.handle || + handle === 'handle.invalid' + ) return undefined return handle @@ -212,17 +240,18 @@ function ComposeBtn() { } const onPressCompose = async () => - store.shell.openComposer({mention: await getProfileHandle()}) + openComposer({mention: await getProfileHandle()}) if (isTablet) { return null } return ( <TouchableOpacity + disabled={isFetchingHandle} style={[styles.newPostBtn]} onPress={onPressCompose} accessibilityRole="button" - accessibilityLabel="New post" + accessibilityLabel={_(msg`New post`)} accessibilityHint=""> <View style={styles.newPostBtnIconWrapper}> <ComposeIcon2 @@ -232,16 +261,18 @@ function ComposeBtn() { /> </View> <Text type="button" style={styles.newPostBtnLabel}> - New Post + <Trans>New Post</Trans> </Text> </TouchableOpacity> ) } -export const DesktopLeftNav = observer(function DesktopLeftNav() { - const store = useStores() +export function DesktopLeftNav() { + const {hasSession, currentAccount} = useSession() const pal = usePalette('default') + const {_} = useLingui() const {isDesktop, isTablet} = useWebMediaQueries() + const numUnread = useUnreadNotifications() return ( <View @@ -251,8 +282,16 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { pal.view, pal.border, ]}> - {store.session.hasSession && <ProfileCard />} + {hasSession ? ( + <ProfileCard /> + ) : isDesktop ? ( + <View style={{paddingHorizontal: 12}}> + <NavSignupCard /> + </View> + ) : null} + <BackBtn /> + <NavItem href="/" icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />} @@ -263,7 +302,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { style={pal.text} /> } - label="Home" + label={_(msg`Home`)} /> <NavItem href="/search" @@ -281,7 +320,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { style={pal.text} /> } - label="Search" + label={_(msg`Search`)} /> <NavItem href="/feeds" @@ -299,105 +338,109 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { size={isDesktop ? 24 : 28} /> } - label="Feeds" + label={_(msg`Feeds`)} /> - <NavItem - href="/notifications" - count={store.me.notifications.unreadCountLabel} - icon={ - <BellIcon - strokeWidth={2} - size={isDesktop ? 24 : 26} - style={pal.text} + + {hasSession && ( + <> + <NavItem + href="/notifications" + count={numUnread} + icon={ + <BellIcon + strokeWidth={2} + size={isDesktop ? 24 : 26} + style={pal.text} + /> + } + iconFilled={ + <BellIconSolid + strokeWidth={1.5} + size={isDesktop ? 24 : 26} + style={pal.text} + /> + } + label={_(msg`Notifications`)} /> - } - iconFilled={ - <BellIconSolid - strokeWidth={1.5} - size={isDesktop ? 24 : 26} - style={pal.text} + <NavItem + href="/lists" + icon={ + <ListIcon + style={pal.text} + size={isDesktop ? 26 : 30} + strokeWidth={2} + /> + } + iconFilled={ + <ListIcon + style={pal.text} + size={isDesktop ? 26 : 30} + strokeWidth={3} + /> + } + label={_(msg`Lists`)} /> - } - label="Notifications" - /> - <NavItem - href="/lists" - icon={ - <ListIcon - style={pal.text} - size={isDesktop ? 26 : 30} - strokeWidth={2} + <NavItem + href="/moderation" + icon={ + <HandIcon + style={pal.text} + size={isDesktop ? 24 : 27} + strokeWidth={5.5} + /> + } + iconFilled={ + <FontAwesomeIcon + icon="hand" + style={pal.text as FontAwesomeIconStyle} + size={isDesktop ? 20 : 26} + /> + } + label={_(msg`Moderation`)} /> - } - iconFilled={ - <ListIcon - style={pal.text} - size={isDesktop ? 26 : 30} - strokeWidth={3} + <NavItem + href={currentAccount ? makeProfileLink(currentAccount) : '/'} + icon={ + <UserIcon + strokeWidth={1.75} + size={isDesktop ? 28 : 30} + style={pal.text} + /> + } + iconFilled={ + <UserIconSolid + strokeWidth={1.75} + size={isDesktop ? 28 : 30} + style={pal.text} + /> + } + label="Profile" /> - } - label="Lists" - /> - <NavItem - href="/moderation" - icon={ - <HandIcon - style={pal.text} - size={isDesktop ? 24 : 27} - strokeWidth={5.5} + <NavItem + href="/settings" + icon={ + <CogIcon + strokeWidth={1.75} + size={isDesktop ? 28 : 32} + style={pal.text} + /> + } + iconFilled={ + <CogIconSolid + strokeWidth={1.5} + size={isDesktop ? 28 : 32} + style={pal.text} + /> + } + label={_(msg`Settings`)} /> - } - iconFilled={ - <FontAwesomeIcon - icon="hand" - style={pal.text as FontAwesomeIconStyle} - size={isDesktop ? 20 : 26} - /> - } - label="Moderation" - /> - {store.session.hasSession && ( - <NavItem - href={makeProfileLink(store.me)} - icon={ - <UserIcon - strokeWidth={1.75} - size={isDesktop ? 28 : 30} - style={pal.text} - /> - } - iconFilled={ - <UserIconSolid - strokeWidth={1.75} - size={isDesktop ? 28 : 30} - style={pal.text} - /> - } - label="Profile" - /> + + <ComposeBtn /> + </> )} - <NavItem - href="/settings" - icon={ - <CogIcon - strokeWidth={1.75} - size={isDesktop ? 28 : 32} - style={pal.text} - /> - } - iconFilled={ - <CogIconSolid - strokeWidth={1.5} - size={isDesktop ? 28 : 32} - style={pal.text} - /> - } - label="Settings" - /> - {store.session.hasSession && <ComposeBtn />} </View> ) -}) +} const styles = StyleSheet.create({ leftNav: { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 84d7d7854..9a5186549 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' @@ -9,15 +8,19 @@ import {Text} from 'view/com/util/text/Text' import {TextLink} from 'view/com/util/Link' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' import {s} from 'lib/styles' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {pluralize} from 'lib/strings/helpers' import {formatCount} from 'view/com/util/numeric/format' +import {useModalControls} from '#/state/modals' +import {useLingui} from '@lingui/react' +import {Plural, Trans, msg, plural} from '@lingui/macro' +import {useSession} from '#/state/session' +import {useInviteCodesQuery} from '#/state/queries/invites' -export const DesktopRightNav = observer(function DesktopRightNavImpl() { - const store = useStores() +export function DesktopRightNav() { const pal = usePalette('default') const palError = usePalette('error') + const {_} = useLingui() + const {isSandbox, hasSession, currentAccount} = useSession() const {isTablet} = useWebMediaQueries() if (isTablet) { @@ -26,10 +29,22 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { return ( <View style={[styles.rightNav, pal.view]}> - {store.session.hasSession && <DesktopSearch />} - {store.session.hasSession && <DesktopFeeds />} - <View style={styles.message}> - {store.session.isSandbox ? ( + <DesktopSearch /> + + {hasSession && ( + <View style={{paddingTop: 18, marginBottom: 18}}> + <DesktopFeeds /> + </View> + )} + + <View + style={[ + styles.message, + { + paddingTop: hasSession ? 0 : 18, + }, + ]}> + {isSandbox ? ( <View style={[palError.view, styles.messageLine, s.p10]}> <Text type="md" style={[palError.text, s.bold]}> SANDBOX. Posts and accounts are not permanent. @@ -37,23 +52,27 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { </View> ) : undefined} <View style={[s.flexRow]}> - <TextLink - type="md" - style={pal.link} - href={FEEDBACK_FORM_URL({ - email: store.session.currentSession?.email, - handle: store.session.currentSession?.handle, - })} - text="Send feedback" - /> - <Text type="md" style={pal.textLight}> - · - </Text> + {hasSession && ( + <> + <TextLink + type="md" + style={pal.link} + href={FEEDBACK_FORM_URL({ + email: currentAccount!.email, + handle: currentAccount!.handle, + })} + text={_(msg`Feedback`)} + /> + <Text type="md" style={pal.textLight}> + · + </Text> + </> + )} <TextLink type="md" style={pal.link} href="https://blueskyweb.xyz/support/privacy-policy" - text="Privacy" + text={_(msg`Privacy`)} /> <Text type="md" style={pal.textLight}> · @@ -62,7 +81,7 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { type="md" style={pal.link} href="https://blueskyweb.xyz/support/tos" - text="Terms" + text={_(msg`Terms`)} /> <Text type="md" style={pal.textLight}> · @@ -71,52 +90,80 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { type="md" style={pal.link} href={HELP_DESK_URL} - text="Help" + text={_(msg`Help`)} /> </View> </View> - <InviteCodes /> + + {hasSession && <InviteCodes />} </View> ) -}) +} -const InviteCodes = observer(function InviteCodesImpl() { - const store = useStores() +function InviteCodes() { const pal = usePalette('default') - - const {invitesAvailable} = store.me + const {openModal} = useModalControls() + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 + const {_} = useLingui() const onPress = React.useCallback(() => { - store.shell.openModal({name: 'invite-codes'}) - }, [store]) + openModal({name: 'invite-codes'}) + }, [openModal]) + + if (!invites) { + return null + } + + if (invites?.disabled) { + return ( + <View style={[styles.inviteCodes, pal.border]}> + <FontAwesomeIcon + icon="ticket" + style={[styles.inviteCodesIcon, pal.textLight]} + size={16} + /> + <Text type="md-medium" style={pal.textLight}> + <Trans> + Your invite codes are hidden when logged in using an App Password + </Trans> + </Text> + </View> + ) + } + return ( <TouchableOpacity style={[styles.inviteCodes, pal.border]} onPress={onPress} accessibilityRole="button" - accessibilityLabel={ - invitesAvailable === 1 - ? 'Invite codes: 1 available' - : `Invite codes: ${invitesAvailable} available` - } - accessibilityHint="Opens list of invite codes"> + accessibilityLabel={_( + plural(invitesAvailable, { + one: 'Invite codes: # available', + other: 'Invite codes: # available', + }), + )} + accessibilityHint={_(msg`Opens list of invite codes`)}> <FontAwesomeIcon icon="ticket" style={[ styles.inviteCodesIcon, - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, + invitesAvailable > 0 ? pal.link : pal.textLight, ]} size={16} /> <Text type="md-medium" - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> - {formatCount(store.me.invitesAvailable)} invite{' '} - {pluralize(store.me.invitesAvailable, 'code')} available + style={invitesAvailable > 0 ? pal.link : pal.textLight}> + <Plural + value={formatCount(invitesAvailable)} + one="# invite code available" + other="# invite codes available" + /> </Text> </TouchableOpacity> ) -}) +} const styles = StyleSheet.create({ rightNav: { @@ -142,9 +189,10 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingVertical: 12, flexDirection: 'row', - alignItems: 'center', }, inviteCodesIcon: { + marginTop: 2, marginRight: 6, + flexShrink: 0, }, }) diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index caecea4a8..f899431b6 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -1,56 +1,150 @@ import React from 'react' -import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native' +import { + ViewStyle, + TextInput, + View, + StyleSheet, + TouchableOpacity, + ActivityIndicator, +} from 'react-native' import {useNavigation, StackActions} from '@react-navigation/native' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' +import { + AppBskyActorDefs, + moderateProfile, + ProfileModeration, +} from '@atproto/api' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {s} from '#/lib/styles' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {makeProfileLink} from '#/lib/routes/links' +import {Link} from '#/view/com/util/Link' import {usePalette} from 'lib/hooks/usePalette' import {MagnifyingGlassIcon2} from 'lib/icons' import {NavigationProp} from 'lib/routes/types' -import {ProfileCard} from 'view/com/profile/ProfileCard' import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' +import {useModerationOpts} from '#/state/queries/preferences' -export const DesktopSearch = observer(function DesktopSearch() { - const store = useStores() +export function SearchResultCard({ + profile, + style, + moderation, +}: { + profile: AppBskyActorDefs.ProfileViewBasic + style: ViewStyle + moderation: ProfileModeration +}) { const pal = usePalette('default') - const textInput = React.useRef<TextInput>(null) - const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) - const [query, setQuery] = React.useState<string>('') - const autocompleteView = React.useMemo<UserAutocompleteModel>( - () => new UserAutocompleteModel(store), - [store], + + return ( + <Link + href={makeProfileLink(profile)} + title={profile.handle} + asAnchor + anchorNoUnderline> + <View + style={[ + pal.border, + style, + { + borderTopWidth: 1, + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingVertical: 8, + paddingHorizontal: 12, + }, + ]}> + <UserAvatar + size={40} + avatar={profile.avatar} + moderation={moderation.avatar} + /> + <View style={{flex: 1}}> + <Text + type="lg" + style={[s.bold, pal.text]} + numberOfLines={1} + lineHeight={1.2}> + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.profile, + )} + </Text> + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + {sanitizeHandle(profile.handle, '@')} + </Text> + </View> + </View> + </Link> ) +} + +export function DesktopSearch() { + const {_} = useLingui() + const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>( + undefined, + ) + const [isActive, setIsActive] = React.useState<boolean>(false) + const [isFetching, setIsFetching] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const [searchResults, setSearchResults] = React.useState< + AppBskyActorDefs.ProfileViewBasic[] + >([]) - // initial setup - React.useEffect(() => { - if (store.me.did) { - autocompleteView.setup() - } - }, [autocompleteView, store.me.did]) + const moderationOpts = useModerationOpts() + const search = useActorAutocompleteFn() - const onChangeQuery = React.useCallback( - (text: string) => { + const onChangeText = React.useCallback( + async (text: string) => { setQuery(text) - if (text.length > 0 && isInputFocused) { - autocompleteView.setActive(true) - autocompleteView.setPrefix(text) + + if (text.length > 0) { + setIsFetching(true) + setIsActive(true) + + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + + searchDebounceTimeout.current = setTimeout(async () => { + const results = await search({query: text}) + + if (results) { + setSearchResults(results) + setIsFetching(false) + } + }, 300) } else { - autocompleteView.setActive(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + setSearchResults([]) + setIsFetching(false) + setIsActive(false) } }, - [setQuery, autocompleteView, isInputFocused], + [setQuery, search, setSearchResults], ) const onPressCancelSearch = React.useCallback(() => { setQuery('') - autocompleteView.setActive(false) - }, [setQuery, autocompleteView]) - + setIsActive(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + }, [setQuery]) const onSubmit = React.useCallback(() => { + setIsActive(false) + if (!query.length) return + setSearchResults([]) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) navigation.dispatch(StackActions.push('Search', {q: query})) - autocompleteView.setActive(false) - }, [query, navigation, autocompleteView]) + }, [query, navigation, setSearchResults]) return ( <View style={[styles.container, pal.view]}> @@ -63,19 +157,16 @@ export const DesktopSearch = observer(function DesktopSearch() { /> <TextInput testID="searchTextInput" - ref={textInput} - placeholder="Search" + placeholder={_(msg`Search`)} placeholderTextColor={pal.colors.textLight} selectTextOnFocus returnKeyType="search" value={query} style={[pal.textLight, styles.input]} - onFocus={() => setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - onChangeText={onChangeQuery} + onChangeText={onChangeText} onSubmitEditing={onSubmit} accessibilityRole="search" - accessibilityLabel="Search" + accessibilityLabel={_(msg`Search`)} accessibilityHint="" /> {query ? ( @@ -83,11 +174,11 @@ export const DesktopSearch = observer(function DesktopSearch() { <TouchableOpacity onPress={onPressCancelSearch} accessibilityRole="button" - accessibilityLabel="Cancel search" + accessibilityLabel={_(msg`Cancel search`)} accessibilityHint="Exits inputting search query" onAccessibilityEscape={onPressCancelSearch}> <Text type="lg" style={[pal.link]}> - Cancel + <Trans>Cancel</Trans> </Text> </TouchableOpacity> </View> @@ -95,32 +186,42 @@ export const DesktopSearch = observer(function DesktopSearch() { </View> </View> - {query !== '' && ( + {query !== '' && isActive && moderationOpts && ( <View style={[pal.view, pal.borderDark, styles.resultsContainer]}> - {autocompleteView.suggestions.length ? ( + {isFetching ? ( + <View style={{padding: 8}}> + <ActivityIndicator /> + </View> + ) : ( <> - {autocompleteView.suggestions.map((item, i) => ( - <ProfileCard key={item.did} profile={item} noBorder={i === 0} /> - ))} + {searchResults.length ? ( + searchResults.map((item, i) => ( + <SearchResultCard + key={item.did} + profile={item} + moderation={moderateProfile(item, moderationOpts)} + style={i === 0 ? {borderTopWidth: 0} : {}} + /> + )) + ) : ( + <View> + <Text style={[pal.textLight, styles.noResults]}> + <Trans>No results found for {query}</Trans> + </Text> + </View> + )} </> - ) : ( - <View> - <Text style={[pal.textLight, styles.noResults]}> - No results found for {autocompleteView.prefix} - </Text> - </View> )} </View> )} </View> ) -}) +} const styles = StyleSheet.create({ container: { position: 'relative', width: 300, - paddingBottom: 18, }, search: { paddingHorizontal: 16, @@ -150,15 +251,11 @@ const styles = StyleSheet.create({ paddingVertical: 7, }, resultsContainer: { - // @ts-ignore supported by web - // position: 'fixed', marginTop: 10, - flexDirection: 'column', width: 300, borderWidth: 1, borderRadius: 6, - paddingVertical: 4, }, noResults: { textAlign: 'center', diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 703edf27a..5562af9ac 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StatusBar} from 'expo-status-bar' import { DimensionValue, @@ -11,7 +10,6 @@ import { import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Drawer} from 'react-native-drawer-layout' import {useNavigationState} from '@react-navigation/native' -import {useStores} from 'state/index' import {ModalsContainer} from 'view/com/modals/Modal' import {Lightbox} from 'view/com/lightbox/Lightbox' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' @@ -25,20 +23,19 @@ import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context' -import {useOTAUpdate} from 'lib/hooks/useOTAUpdate' import { useIsDrawerOpen, useSetDrawerOpen, useIsDrawerSwipeDisabled, } from '#/state/shell' import {isAndroid} from 'platform/detection' +import {useSession} from '#/state/session' +import {useCloseAnyActiveElement} from '#/state/util' -const ShellInner = observer(function ShellInnerImpl() { - const store = useStores() +function ShellInner() { const isDrawerOpen = useIsDrawerOpen() const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() const setIsDrawerOpen = useSetDrawerOpen() - useOTAUpdate() // this hook polls for OTA updates every few seconds const winDim = useWindowDimensions() const safeAreaInsets = useSafeAreaInsets() const containerPadding = React.useMemo( @@ -55,18 +52,20 @@ const ShellInner = observer(function ShellInnerImpl() { [setIsDrawerOpen], ) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) + const {hasSession} = useSession() + const closeAnyActiveElement = useCloseAnyActiveElement() + React.useEffect(() => { let listener = {remove() {}} if (isAndroid) { listener = BackHandler.addEventListener('hardwareBackPress', () => { - setIsDrawerOpen(false) - return store.shell.closeAnyActiveElement() + return closeAnyActiveElement() }) } return () => { listener.remove() } - }, [store, setIsDrawerOpen]) + }, [closeAnyActiveElement]) return ( <> @@ -78,28 +77,19 @@ const ShellInner = observer(function ShellInnerImpl() { onOpen={onOpenDrawer} onClose={onCloseDrawer} swipeEdgeWidth={winDim.width / 2} - swipeEnabled={ - !canGoBack && store.session.hasSession && !isDrawerSwipeDisabled - }> + swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}> <TabsNavigator /> </Drawer> </ErrorBoundary> </View> - <Composer - active={store.shell.isComposerActive} - winHeight={winDim.height} - replyTo={store.shell.composerOpts?.replyTo} - onPost={store.shell.composerOpts?.onPost} - quote={store.shell.composerOpts?.quote} - mention={store.shell.composerOpts?.mention} - /> + <Composer winHeight={winDim.height} /> <ModalsContainer /> <Lightbox /> </> ) -}) +} -export const Shell: React.FC = observer(function ShellImpl() { +export const Shell: React.FC = function ShellImpl() { const pal = usePalette('default') const theme = useTheme() return ( @@ -112,7 +102,7 @@ export const Shell: React.FC = observer(function ShellImpl() { </View> </SafeAreaProvider> ) -}) +} const styles = StyleSheet.create({ outerContainer: { diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 843d0b284..38da860bd 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -1,9 +1,5 @@ import React, {useEffect} from 'react' -import {observer} from 'mobx-react-lite' import {View, StyleSheet, TouchableOpacity} from 'react-native' -import {useStores} from 'state/index' -import {DesktopLeftNav} from './desktop/LeftNav' -import {DesktopRightNav} from './desktop/RightNav' import {ErrorBoundary} from '../com/util/ErrorBoundary' import {Lightbox} from '../com/lightbox/Lightbox' import {ModalsContainer} from '../com/modals/Modal' @@ -13,30 +9,29 @@ import {s, colors} from 'lib/styles' import {RoutesContainer, FlatNavigator} from '../../Navigation' import {DrawerContent} from './Drawer' import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' -import {BottomBarWeb} from './bottom-bar/BottomBarWeb' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {useAuxClick} from 'lib/hooks/useAuxClick' +import {t} from '@lingui/macro' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' +import {useCloseAllActiveElements} from '#/state/util' -const ShellInner = observer(function ShellInnerImpl() { - const store = useStores() +function ShellInner() { const isDrawerOpen = useIsDrawerOpen() const setDrawerOpen = useSetDrawerOpen() - const {isDesktop, isMobile} = useWebMediaQueries() + const {isDesktop} = useWebMediaQueries() const navigator = useNavigation<NavigationProp>() + const closeAllActiveElements = useCloseAllActiveElements() + useAuxClick() useEffect(() => { - navigator.addListener('state', () => { - setDrawerOpen(false) - store.shell.closeAnyActiveElement() + const unsubscribe = navigator.addListener('state', () => { + closeAllActiveElements() }) - }, [navigator, store.shell, setDrawerOpen]) + return unsubscribe + }, [navigator, closeAllActiveElements]) - const showBottomBar = isMobile && !store.onboarding.isActive - const showSideNavs = - !isMobile && store.session.hasSession && !store.onboarding.isActive return ( <View style={[s.hContentRegion, {overflow: 'hidden'}]}> <View style={s.hContentRegion}> @@ -44,28 +39,14 @@ const ShellInner = observer(function ShellInnerImpl() { <FlatNavigator /> </ErrorBoundary> </View> - {showSideNavs && ( - <> - <DesktopLeftNav /> - <DesktopRightNav /> - </> - )} - <Composer - active={store.shell.isComposerActive} - winHeight={0} - replyTo={store.shell.composerOpts?.replyTo} - quote={store.shell.composerOpts?.quote} - onPost={store.shell.composerOpts?.onPost} - mention={store.shell.composerOpts?.mention} - /> - {showBottomBar && <BottomBarWeb />} + <Composer winHeight={0} /> <ModalsContainer /> <Lightbox /> {!isDesktop && isDrawerOpen && ( <TouchableOpacity onPress={() => setDrawerOpen(false)} style={styles.drawerMask} - accessibilityLabel="Close navigation footer" + accessibilityLabel={t`Close navigation footer`} accessibilityHint="Closes bottom navigation bar"> <View style={styles.drawerContainer}> <DrawerContent /> @@ -74,9 +55,9 @@ const ShellInner = observer(function ShellInnerImpl() { )} </View> ) -}) +} -export const Shell: React.FC = observer(function ShellImpl() { +export const Shell: React.FC = function ShellImpl() { const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( <View style={[s.hContentRegion, pageBg]}> @@ -85,7 +66,7 @@ export const Shell: React.FC = observer(function ShellImpl() { </RoutesContainer> </View> ) -}) +} const styles = StyleSheet.create({ bgLight: { |