PWI: Refactor Shell (#1989)

* Vendor createNativeStackNavigator for further tweaks

* Completely disable withAuthRequired

* Render LoggedOut for protected routes

* Move web shell into the navigator

* Simplify the logic

* Add login modal

* Delete withAuthRequired

* Reset app state on session change

* Move TS suppression
zio/stable
dan 2023-11-24 22:31:33 +00:00 committed by GitHub
parent 4b59a21cac
commit f2d164ec23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1627 additions and 1665 deletions

View File

@ -28,6 +28,7 @@ import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences' import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import I18nProvider from './locale/i18nProvider' import I18nProvider from './locale/i18nProvider'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
@ -42,7 +43,7 @@ SplashScreen.preventAutoHideAsync()
function InnerApp() { function InnerApp() {
const colorMode = useColorMode() const colorMode = useColorMode()
const {isInitialLoad} = useSession() const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
// init // init
@ -69,19 +70,25 @@ function InnerApp() {
*/ */
return ( return (
<UnreadNotifsProvider> <React.Fragment
<ThemeProvider theme={colorMode}> // Resets the entire tree below when it changes:
<analytics.Provider> key={currentAccount?.did}>
{/* All components should be within this provider */} <LoggedOutViewProvider>
<RootSiblingParent> <UnreadNotifsProvider>
<GestureHandlerRootView style={s.h100pct}> <ThemeProvider theme={colorMode}>
<TestCtrls /> <analytics.Provider>
<Shell /> {/* All components should be within this provider */}
</GestureHandlerRootView> <RootSiblingParent>
</RootSiblingParent> <GestureHandlerRootView style={s.h100pct}>
</analytics.Provider> <TestCtrls />
</ThemeProvider> <Shell />
</UnreadNotifsProvider> </GestureHandlerRootView>
</RootSiblingParent>
</analytics.Provider>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>
</React.Fragment>
) )
} }

View File

@ -22,6 +22,7 @@ import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences' import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import I18nProvider from './locale/i18nProvider' import I18nProvider from './locale/i18nProvider'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
@ -34,7 +35,7 @@ import * as persisted from '#/state/persisted'
enableFreeze(true) enableFreeze(true)
function InnerApp() { function InnerApp() {
const {isInitialLoad} = useSession() const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const colorMode = useColorMode() const colorMode = useColorMode()
@ -57,19 +58,25 @@ function InnerApp() {
*/ */
return ( return (
<UnreadNotifsProvider> <React.Fragment
<ThemeProvider theme={colorMode}> // Resets the entire tree below when it changes:
<analytics.Provider> key={currentAccount?.did}>
{/* All components should be within this provider */} <LoggedOutViewProvider>
<RootSiblingParent> <UnreadNotifsProvider>
<SafeAreaProvider> <ThemeProvider theme={colorMode}>
<Shell /> <analytics.Provider>
</SafeAreaProvider> {/* All components should be within this provider */}
</RootSiblingParent> <RootSiblingParent>
<ToastContainer /> <SafeAreaProvider>
</analytics.Provider> <Shell />
</ThemeProvider> </SafeAreaProvider>
</UnreadNotifsProvider> </RootSiblingParent>
<ToastContainer />
</analytics.Provider>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>
</React.Fragment>
) )
} }

View File

@ -9,7 +9,6 @@ import {
DefaultTheme, DefaultTheme,
DarkTheme, DarkTheme,
} from '@react-navigation/native' } from '@react-navigation/native'
import {createNativeStackNavigator} from '@react-navigation/native-stack'
import { import {
BottomTabBarProps, BottomTabBarProps,
createBottomTabNavigator, createBottomTabNavigator,
@ -69,16 +68,18 @@ import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
import {SavedFeeds} from 'view/screens/SavedFeeds' import {SavedFeeds} from 'view/screens/SavedFeeds'
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed' import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads' import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>() const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>() const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
const FeedsTab = createNativeStackNavigator<FeedsTabNavigatorParams>() const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>()
const NotificationsTab = const NotificationsTab =
createNativeStackNavigator<NotificationsTabNavigatorParams>() createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>() const MyProfileTab =
const Flat = createNativeStackNavigator<FlatNavigatorParams>() createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()
const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
/** /**
@ -97,37 +98,37 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen <Stack.Screen
name="Lists" name="Lists"
component={ListsScreen} component={ListsScreen}
options={{title: title('Lists')}} options={{title: title('Lists'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Moderation" name="Moderation"
getComponent={() => ModerationScreen} getComponent={() => ModerationScreen}
options={{title: title('Moderation')}} options={{title: title('Moderation'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ModerationModlists" name="ModerationModlists"
getComponent={() => ModerationModlistsScreen} getComponent={() => ModerationModlistsScreen}
options={{title: title('Moderation Lists')}} options={{title: title('Moderation Lists'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ModerationMutedAccounts" name="ModerationMutedAccounts"
getComponent={() => ModerationMutedAccounts} getComponent={() => ModerationMutedAccounts}
options={{title: title('Muted Accounts')}} options={{title: title('Muted Accounts'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ModerationBlockedAccounts" name="ModerationBlockedAccounts"
getComponent={() => ModerationBlockedAccounts} getComponent={() => ModerationBlockedAccounts}
options={{title: title('Blocked Accounts')}} options={{title: title('Blocked Accounts'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Settings" name="Settings"
getComponent={() => SettingsScreen} getComponent={() => SettingsScreen}
options={{title: title('Settings')}} options={{title: title('Settings'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="LanguageSettings" name="LanguageSettings"
getComponent={() => LanguageSettingsScreen} getComponent={() => LanguageSettingsScreen}
options={{title: title('Language Settings')}} options={{title: title('Language Settings'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Profile" name="Profile"
@ -154,7 +155,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen <Stack.Screen
name="ProfileList" name="ProfileList"
getComponent={() => ProfileListScreen} getComponent={() => ProfileListScreen}
options={{title: title('List')}} options={{title: title('List'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PostThread" name="PostThread"
@ -184,12 +185,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen <Stack.Screen
name="Debug" name="Debug"
getComponent={() => DebugScreen} getComponent={() => DebugScreen}
options={{title: title('Debug')}} options={{title: title('Debug'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Log" name="Log"
getComponent={() => LogScreen} getComponent={() => LogScreen}
options={{title: title('Log')}} options={{title: title('Log'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Support" name="Support"
@ -219,22 +220,22 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen <Stack.Screen
name="AppPasswords" name="AppPasswords"
getComponent={() => AppPasswords} getComponent={() => AppPasswords}
options={{title: title('App Passwords')}} options={{title: title('App Passwords'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="SavedFeeds" name="SavedFeeds"
getComponent={() => SavedFeeds} getComponent={() => SavedFeeds}
options={{title: title('Edit My Feeds')}} options={{title: title('Edit My Feeds'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PreferencesHomeFeed" name="PreferencesHomeFeed"
getComponent={() => PreferencesHomeFeed} getComponent={() => PreferencesHomeFeed}
options={{title: title('Home Feed Preferences')}} options={{title: title('Home Feed Preferences'), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PreferencesThreads" name="PreferencesThreads"
getComponent={() => PreferencesThreads} getComponent={() => PreferencesThreads}
options={{title: title('Threads Preferences')}} options={{title: title('Threads Preferences'), requireAuth: true}}
/> />
</> </>
) )
@ -339,6 +340,7 @@ function NotificationsTabNavigator() {
<NotificationsTab.Screen <NotificationsTab.Screen
name="Notifications" name="Notifications"
getComponent={() => NotificationsScreen} getComponent={() => NotificationsScreen}
options={{requireAuth: true}}
/> />
{commonScreens(NotificationsTab as typeof HomeTab)} {commonScreens(NotificationsTab as typeof HomeTab)}
</NotificationsTab.Navigator> </NotificationsTab.Navigator>
@ -357,8 +359,8 @@ function MyProfileTabNavigator() {
contentStyle, contentStyle,
}}> }}>
<MyProfileTab.Screen <MyProfileTab.Screen
name="MyProfile"
// @ts-ignore // TODO: fix this broken type in ProfileScreen // @ts-ignore // TODO: fix this broken type in ProfileScreen
name="MyProfile"
getComponent={() => ProfileScreen} getComponent={() => ProfileScreen}
initialParams={{ initialParams={{
name: 'me', name: 'me',
@ -405,7 +407,7 @@ const FlatNavigator = () => {
<Flat.Screen <Flat.Screen
name="Notifications" name="Notifications"
getComponent={() => NotificationsScreen} getComponent={() => NotificationsScreen}
options={{title: title('Notifications')}} options={{title: title('Notifications'), requireAuth: true}}
/> />
{commonScreens(Flat as typeof HomeTab, numUnread)} {commonScreens(Flat as typeof HomeTab, numUnread)}
</Flat.Navigator> </Flat.Navigator>

View File

@ -7,7 +7,6 @@ import {Provider as ColorModeProvider} from './color-mode'
import {Provider as OnboardingProvider} from './onboarding' import {Provider as OnboardingProvider} from './onboarding'
import {Provider as ComposerProvider} from './composer' import {Provider as ComposerProvider} from './composer'
import {Provider as TickEveryMinuteProvider} from './tick-every-minute' import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
import {Provider as LoggedOutViewProvider} from './logged-out'
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
export { export {
@ -23,23 +22,19 @@ export {useTickEveryMinute} from './tick-every-minute'
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
return ( return (
<ShellLayoutProvder> <ShellLayoutProvder>
<LoggedOutViewProvider> <DrawerOpenProvider>
<DrawerOpenProvider> <DrawerSwipableProvider>
<DrawerSwipableProvider> <MinimalModeProvider>
<MinimalModeProvider> <ColorModeProvider>
<ColorModeProvider> <OnboardingProvider>
<OnboardingProvider> <ComposerProvider>
<ComposerProvider> <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider>
<TickEveryMinuteProvider> </ComposerProvider>
{children} </OnboardingProvider>
</TickEveryMinuteProvider> </ColorModeProvider>
</ComposerProvider> </MinimalModeProvider>
</OnboardingProvider> </DrawerSwipableProvider>
</ColorModeProvider> </DrawerOpenProvider>
</MinimalModeProvider>
</DrawerSwipableProvider>
</DrawerOpenProvider>
</LoggedOutViewProvider>
</ShellLayoutProvder> </ShellLayoutProvder>
) )
} }

View File

@ -1,94 +0,0 @@
import React from 'react'
import {
ActivityIndicator,
Linking,
StyleSheet,
TouchableOpacity,
} from 'react-native'
import {CenteredView} from '../util/Views'
import {LoggedOut} from './LoggedOut'
import {Onboarding} from './Onboarding'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {STATUS_PAGE_URL} from 'lib/constants'
import {useOnboardingState} from '#/state/shell'
import {useSession} from '#/state/session'
import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {IS_PROD} from '#/env'
export const withAuthRequired = <P extends object>(
Component: React.ComponentType<P>,
options: {
isPublic?: boolean // TODO(pwi) need to enable in TF somehow
} = {},
): React.FC<P> =>
function AuthRequired(props: P) {
const {isInitialLoad, hasSession} = useSession()
const onboardingState = useOnboardingState()
const {showLoggedOut} = useLoggedOutView()
const {setShowLoggedOut} = useLoggedOutViewControls()
if (isInitialLoad) {
return <Loading />
}
if (!hasSession) {
if (showLoggedOut) {
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
} else if (!options?.isPublic || IS_PROD) {
return <LoggedOut />
}
}
if (onboardingState.isActive) {
return <Onboarding />
}
return <Component {...props} />
}
function Loading() {
const pal = usePalette('default')
const [isTakingTooLong, setIsTakingTooLong] = React.useState(false)
React.useEffect(() => {
const t = setTimeout(() => setIsTakingTooLong(true), 15e3) // 15 seconds
return () => clearTimeout(t)
}, [setIsTakingTooLong])
return (
<CenteredView style={[styles.loading, pal.view]}>
<ActivityIndicator size="large" />
<Text type="2xl" style={[styles.loadingText, pal.textLight]}>
{isTakingTooLong
? "This is taking too long. There may be a problem with your internet or with the service, but we're going to try a couple more times..."
: 'Connecting...'}
</Text>
{isTakingTooLong ? (
<TouchableOpacity
onPress={() => {
Linking.openURL(STATUS_PAGE_URL)
}}
accessibilityRole="button">
<Text type="2xl" style={[styles.loadingText, pal.link]}>
Check Bluesky status page
</Text>
</TouchableOpacity>
) : null}
</CenteredView>
)
}
const styles = StyleSheet.create({
loading: {
height: '100%',
alignContent: 'center',
justifyContent: 'center',
paddingBottom: 100,
},
loadingText: {
paddingVertical: 20,
paddingHorizontal: 20,
textAlign: 'center',
},
})

View File

@ -12,7 +12,6 @@ import {Button} from '../com/util/forms/Button'
import * as Toast from '../com/util/Toast' import * as Toast from '../com/util/Toast'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
@ -32,125 +31,111 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export const AppPasswords = withAuthRequired( export function AppPasswords({}: Props) {
function AppPasswordsImpl({}: Props) { const pal = usePalette('default')
const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics()
const {screen} = useAnalytics() const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrDesktop} = useWebMediaQueries() const {openModal} = useModalControls()
const {openModal} = useModalControls() const {data: appPasswords, error} = useAppPasswordsQuery()
const {data: appPasswords, error} = useAppPasswordsQuery()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('AppPasswords') screen('AppPasswords')
setMinimalShellMode(false) setMinimalShellMode(false)
}, [screen, setMinimalShellMode]), }, [screen, setMinimalShellMode]),
)
const onAdd = React.useCallback(async () => {
openModal({name: 'add-app-password'})
}, [openModal])
if (error) {
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<ErrorScreen
title="Oops!"
message="There was an issue with fetching your app passwords"
details={cleanError(error)}
/>
</CenteredView>
) )
}
const onAdd = React.useCallback(async () => { // no app passwords (empty) state
openModal({name: 'add-app-password'}) if (appPasswords?.length === 0) {
}, [openModal]) return (
<CenteredView
if (error) { style={[
return ( styles.container,
<CenteredView isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}>
<Trans>
You have not created any app passwords yet. You can create one by
pressing the button below.
</Trans>
</Text>
</View>
{!isTabletOrDesktop && <View style={styles.flex1} />}
<View
style={[ style={[
styles.container, styles.btnContainer,
isTabletOrDesktop && styles.containerDesktop, isTabletOrDesktop && styles.btnContainerDesktop,
pal.view, ]}>
pal.border, <Button
]} testID="appPasswordBtn"
testID="appPasswordsScreen"> type="primary"
<ErrorScreen label="Add App Password"
title="Oops!" style={styles.btn}
message="There was an issue with fetching your app passwords" labelStyle={styles.btnLabel}
details={cleanError(error)} onPress={onAdd}
/> />
</CenteredView> </View>
) </CenteredView>
} )
}
// no app passwords (empty) state if (appPasswords?.length) {
if (appPasswords?.length === 0) { // has app passwords
return ( return (
<CenteredView <CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<ScrollView
style={[ style={[
styles.container, styles.scrollContainer,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border, pal.border,
]} !isTabletOrDesktop && styles.flex1,
testID="appPasswordsScreen"> ]}>
<AppPasswordsHeader /> {appPasswords.map((password, i) => (
<View style={[styles.empty, pal.viewLight]}> <AppPassword
<Text type="lg" style={[pal.text, styles.emptyText]}> key={password.name}
<Trans> testID={`appPassword-${i}`}
You have not created any app passwords yet. You can create one name={password.name}
by pressing the button below. createdAt={password.createdAt}
</Trans>
</Text>
</View>
{!isTabletOrDesktop && <View style={styles.flex1} />}
<View
style={[
styles.btnContainer,
isTabletOrDesktop && styles.btnContainerDesktop,
]}>
<Button
testID="appPasswordBtn"
type="primary"
label="Add App Password"
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/> />
</View> ))}
</CenteredView> {isTabletOrDesktop && (
) <View style={[styles.btnContainer, styles.btnContainerDesktop]}>
}
if (appPasswords?.length) {
// has app passwords
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<ScrollView
style={[
styles.scrollContainer,
pal.border,
!isTabletOrDesktop && styles.flex1,
]}>
{appPasswords.map((password, i) => (
<AppPassword
key={password.name}
testID={`appPassword-${i}`}
name={password.name}
createdAt={password.createdAt}
/>
))}
{isTabletOrDesktop && (
<View style={[styles.btnContainer, styles.btnContainerDesktop]}>
<Button
testID="appPasswordBtn"
type="primary"
label="Add App Password"
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
)}
</ScrollView>
{!isTabletOrDesktop && (
<View style={styles.btnContainer}>
<Button <Button
testID="appPasswordBtn" testID="appPasswordBtn"
type="primary" type="primary"
@ -161,24 +146,36 @@ export const AppPasswords = withAuthRequired(
/> />
</View> </View>
)} )}
</CenteredView> </ScrollView>
) {!isTabletOrDesktop && (
} <View style={styles.btnContainer}>
<Button
return ( testID="appPasswordBtn"
<CenteredView type="primary"
style={[ label="Add App Password"
styles.container, style={styles.btn}
isTabletOrDesktop && styles.containerDesktop, labelStyle={styles.btnLabel}
pal.view, onPress={onAdd}
pal.border, />
]} </View>
testID="appPasswordsScreen"> )}
<ActivityIndicator />
</CenteredView> </CenteredView>
) )
}, }
)
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<ActivityIndicator />
</CenteredView>
)
}
function AppPasswordsHeader() { function AppPasswordsHeader() {
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()

View File

@ -2,7 +2,6 @@ import React from 'react'
import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native' import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from 'view/com/util/ViewHeader' import {ViewHeader} from 'view/com/util/ViewHeader'
import {FAB} from 'view/com/util/fab/FAB' import {FAB} from 'view/com/util/fab/FAB'
import {Link} from 'view/com/util/Link' import {Link} from 'view/com/util/Link'
@ -88,437 +87,432 @@ type FlatlistSlice =
key: string key: string
} }
export const FeedsScreen = withAuthRequired( export function FeedsScreen(_props: Props) {
function FeedsScreenImpl(_props: Props) { const pal = usePalette('default')
const pal = usePalette('default') const {openComposer} = useComposerControls()
const {openComposer} = useComposerControls() const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const [query, setQuery] = React.useState('')
const [query, setQuery] = React.useState('') const [isPTR, setIsPTR] = React.useState(false)
const [isPTR, setIsPTR] = React.useState(false) const {
const { data: preferences,
data: preferences, isLoading: isPreferencesLoading,
isLoading: isPreferencesLoading, error: preferencesError,
error: preferencesError, } = usePreferencesQuery()
} = usePreferencesQuery() const {
const { data: popularFeeds,
data: popularFeeds, isFetching: isPopularFeedsFetching,
isFetching: isPopularFeedsFetching, error: popularFeedsError,
error: popularFeedsError, refetch: refetchPopularFeeds,
refetch: refetchPopularFeeds, fetchNextPage: fetchNextPopularFeedsPage,
fetchNextPage: fetchNextPopularFeedsPage, isFetchingNextPage: isPopularFeedsFetchingNextPage,
isFetchingNextPage: isPopularFeedsFetchingNextPage, hasNextPage: hasNextPopularFeedsPage,
hasNextPage: hasNextPopularFeedsPage, } = useGetPopularFeedsQuery()
} = useGetPopularFeedsQuery() const {_} = useLingui()
const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {
const { data: searchResults,
data: searchResults, mutate: search,
mutate: search, reset: resetSearch,
reset: resetSearch, isPending: isSearchPending,
isPending: isSearchPending, error: searchError,
error: searchError, } = useSearchPopularFeedsMutation()
} = useSearchPopularFeedsMutation() const {hasSession} = useSession()
const {hasSession} = useSession()
/** /**
* A search query is present. We may not have search results yet. * A search query is present. We may not have search results yet.
*/ */
const isUserSearching = query.length > 1 const isUserSearching = query.length > 1
const debouncedSearch = React.useMemo( const debouncedSearch = React.useMemo(
() => debounce(q => search(q), 500), // debounce for 500ms () => debounce(q => search(q), 500), // debounce for 500ms
[search], [search],
)
const onPressCompose = React.useCallback(() => {
openComposer({})
}, [openComposer])
const onChangeQuery = React.useCallback(
(text: string) => {
setQuery(text)
if (text.length > 1) {
debouncedSearch(text)
} else {
refetchPopularFeeds()
resetSearch()
}
},
[setQuery, refetchPopularFeeds, debouncedSearch, resetSearch],
)
const onPressCancelSearch = React.useCallback(() => {
setQuery('')
refetchPopularFeeds()
resetSearch()
}, [refetchPopularFeeds, setQuery, resetSearch])
const onSubmitQuery = React.useCallback(() => {
debouncedSearch(query)
}, [query, debouncedSearch])
const onPullToRefresh = React.useCallback(async () => {
setIsPTR(true)
await refetchPopularFeeds()
setIsPTR(false)
}, [setIsPTR, refetchPopularFeeds])
const onEndReached = React.useCallback(() => {
if (
isPopularFeedsFetching ||
isUserSearching ||
!hasNextPopularFeedsPage ||
popularFeedsError
) )
const onPressCompose = React.useCallback(() => { return
openComposer({}) fetchNextPopularFeedsPage()
}, [openComposer]) }, [
const onChangeQuery = React.useCallback( isPopularFeedsFetching,
(text: string) => { isUserSearching,
setQuery(text) popularFeedsError,
if (text.length > 1) { hasNextPopularFeedsPage,
debouncedSearch(text) fetchNextPopularFeedsPage,
} else { ])
refetchPopularFeeds()
resetSearch()
}
},
[setQuery, refetchPopularFeeds, debouncedSearch, resetSearch],
)
const onPressCancelSearch = React.useCallback(() => {
setQuery('')
refetchPopularFeeds()
resetSearch()
}, [refetchPopularFeeds, setQuery, resetSearch])
const onSubmitQuery = React.useCallback(() => {
debouncedSearch(query)
}, [query, debouncedSearch])
const onPullToRefresh = React.useCallback(async () => {
setIsPTR(true)
await refetchPopularFeeds()
setIsPTR(false)
}, [setIsPTR, refetchPopularFeeds])
const onEndReached = React.useCallback(() => {
if (
isPopularFeedsFetching ||
isUserSearching ||
!hasNextPopularFeedsPage ||
popularFeedsError
)
return
fetchNextPopularFeedsPage()
}, [
isPopularFeedsFetching,
isUserSearching,
popularFeedsError,
hasNextPopularFeedsPage,
fetchNextPopularFeedsPage,
])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
const items = React.useMemo(() => { const items = React.useMemo(() => {
let slices: FlatlistSlice[] = [] let slices: FlatlistSlice[] = []
if (hasSession) { if (hasSession) {
slices.push({
key: 'savedFeedsHeader',
type: 'savedFeedsHeader',
})
if (preferencesError) {
slices.push({ slices.push({
key: 'savedFeedsHeader', key: 'savedFeedsError',
type: 'savedFeedsHeader', type: 'error',
error: cleanError(preferencesError.toString()),
}) })
} else {
if (preferencesError) { if (isPreferencesLoading || !preferences?.feeds?.saved) {
slices.push({ slices.push({
key: 'savedFeedsError', key: 'savedFeedsLoading',
type: 'error', type: 'savedFeedsLoading',
error: cleanError(preferencesError.toString()), // pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
}) })
} else { } else {
if (isPreferencesLoading || !preferences?.feeds?.saved) { if (preferences?.feeds?.saved.length === 0) {
slices.push({ slices.push({
key: 'savedFeedsLoading', key: 'savedFeedNoResults',
type: 'savedFeedsLoading', type: 'savedFeedNoResults',
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
}) })
} else { } else {
if (preferences?.feeds?.saved.length === 0) { const {saved, pinned} = preferences.feeds
slices.push({
key: 'savedFeedNoResults',
type: 'savedFeedNoResults',
})
} else {
const {saved, pinned} = preferences.feeds
slices = slices.concat( slices = slices.concat(
pinned.map(uri => ({ pinned.map(uri => ({
key: `savedFeed:${uri}`,
type: 'savedFeed',
feedUri: uri,
})),
)
slices = slices.concat(
saved
.filter(uri => !pinned.includes(uri))
.map(uri => ({
key: `savedFeed:${uri}`, key: `savedFeed:${uri}`,
type: 'savedFeed', type: 'savedFeed',
feedUri: uri, feedUri: uri,
})), })),
) )
}
}
}
}
slices.push({
key: 'popularFeedsHeader',
type: 'popularFeedsHeader',
})
if (popularFeedsError || searchError) {
slices.push({
key: 'popularFeedsError',
type: 'error',
error: cleanError(
popularFeedsError?.toString() ?? searchError?.toString() ?? '',
),
})
} else {
if (isUserSearching) {
if (isSearchPending || !searchResults) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (!searchResults || searchResults?.length === 0) {
slices.push({
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
})
} else {
slices = slices.concat(
searchResults.map(feed => ({
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
})),
)
}
}
} else {
if (isPopularFeedsFetching && !popularFeeds?.pages) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (
!popularFeeds?.pages ||
popularFeeds?.pages[0]?.feeds?.length === 0
) {
slices.push({
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
})
} else {
for (const page of popularFeeds.pages || []) {
slices = slices.concat( slices = slices.concat(
saved page.feeds
.filter(uri => !pinned.includes(uri)) .filter(feed => !preferences?.feeds?.saved.includes(feed.uri))
.map(uri => ({ .map(feed => ({
key: `savedFeed:${uri}`, key: `popularFeed:${feed.uri}`,
type: 'savedFeed', type: 'popularFeed',
feedUri: uri, feedUri: feed.uri,
})), })),
) )
} }
}
}
}
slices.push({ if (isPopularFeedsFetchingNextPage) {
key: 'popularFeedsHeader',
type: 'popularFeedsHeader',
})
if (popularFeedsError || searchError) {
slices.push({
key: 'popularFeedsError',
type: 'error',
error: cleanError(
popularFeedsError?.toString() ?? searchError?.toString() ?? '',
),
})
} else {
if (isUserSearching) {
if (isSearchPending || !searchResults) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (!searchResults || searchResults?.length === 0) {
slices.push({ slices.push({
key: 'popularFeedsNoResults', key: 'popularFeedsLoadingMore',
type: 'popularFeedsNoResults', type: 'popularFeedsLoadingMore',
}) })
} else {
slices = slices.concat(
searchResults.map(feed => ({
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
})),
)
}
}
} else {
if (isPopularFeedsFetching && !popularFeeds?.pages) {
slices.push({
key: 'popularFeedsLoading',
type: 'popularFeedsLoading',
})
} else {
if (
!popularFeeds?.pages ||
popularFeeds?.pages[0]?.feeds?.length === 0
) {
slices.push({
key: 'popularFeedsNoResults',
type: 'popularFeedsNoResults',
})
} else {
for (const page of popularFeeds.pages || []) {
slices = slices.concat(
page.feeds
.filter(
feed => !preferences?.feeds?.saved.includes(feed.uri),
)
.map(feed => ({
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
})),
)
}
if (isPopularFeedsFetchingNextPage) {
slices.push({
key: 'popularFeedsLoadingMore',
type: 'popularFeedsLoadingMore',
})
}
} }
} }
} }
} }
}
return slices return slices
}, [ }, [
hasSession, hasSession,
preferences, preferences,
isPreferencesLoading, isPreferencesLoading,
preferencesError, preferencesError,
popularFeeds, popularFeeds,
isPopularFeedsFetching, isPopularFeedsFetching,
popularFeedsError, popularFeedsError,
isPopularFeedsFetchingNextPage, isPopularFeedsFetchingNextPage,
searchResults, searchResults,
isSearchPending, isSearchPending,
searchError, searchError,
isUserSearching, isUserSearching,
]) ])
const renderHeaderBtn = React.useCallback(() => {
return (
<Link
href="/settings/saved-feeds"
hitSlop={10}
accessibilityRole="button"
accessibilityLabel={_(msg`Edit Saved Feeds`)}
accessibilityHint="Opens screen to edit Saved Feeds">
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
</Link>
)
}, [pal, _])
const renderItem = React.useCallback(
({item}: {item: FlatlistSlice}) => {
if (item.type === 'error') {
return <ErrorMessage message={item.error} />
} else if (
item.type === 'popularFeedsLoadingMore' ||
item.type === 'savedFeedsLoading'
) {
return (
<View style={s.p10}>
<ActivityIndicator />
</View>
)
} else if (item.type === 'savedFeedsHeader') {
if (!isMobile) {
return (
<View
style={[
pal.view,
styles.header,
pal.border,
{
borderBottomWidth: 1,
},
]}>
<Text type="title-lg" style={[pal.text, s.bold]}>
<Trans>My Feeds</Trans>
</Text>
<Link
href="/settings/saved-feeds"
accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityHint="">
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
</Link>
</View>
)
}
return <View />
} else if (item.type === 'savedFeedNoResults') {
return (
<View
style={{
paddingHorizontal: 16,
paddingTop: 10,
}}>
<Text type="lg" style={pal.textLight}>
<Trans>You don't have any saved feeds!</Trans>
</Text>
</View>
)
} else if (item.type === 'savedFeed') {
return <SavedFeed feedUri={item.feedUri} />
} else if (item.type === 'popularFeedsHeader') {
return (
<>
<View
style={[
pal.view,
styles.header,
{
// This is first in the flatlist without a session -esb
marginTop: hasSession ? 16 : 0,
paddingLeft: isMobile ? 12 : undefined,
paddingRight: 10,
paddingBottom: isMobile ? 6 : undefined,
},
]}>
<Text type="title-lg" style={[pal.text, s.bold]}>
<Trans>Discover new feeds</Trans>
</Text>
{!isMobile && (
<SearchInput
query={query}
onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
style={{flex: 1, maxWidth: 250}}
/>
)}
</View>
{isMobile && (
<View style={{paddingHorizontal: 8, paddingBottom: 10}}>
<SearchInput
query={query}
onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
/>
</View>
)}
</>
)
} else if (item.type === 'popularFeedsLoading') {
return <FeedFeedLoadingPlaceholder />
} else if (item.type === 'popularFeed') {
return (
<FeedSourceCard
feedUri={item.feedUri}
showSaveBtn={hasSession}
showDescription
showLikes
/>
)
} else if (item.type === 'popularFeedsNoResults') {
return (
<View
style={{
paddingHorizontal: 16,
paddingTop: 10,
paddingBottom: '150%',
}}>
<Text type="lg" style={pal.textLight}>
<Trans>No results found for "{query}"</Trans>
</Text>
</View>
)
}
return null
},
[
_,
hasSession,
isMobile,
pal,
query,
onChangeQuery,
onPressCancelSearch,
onSubmitQuery,
],
)
const renderHeaderBtn = React.useCallback(() => {
return ( return (
<View style={[pal.view, styles.container]}> <Link
{isMobile && ( href="/settings/saved-feeds"
<ViewHeader hitSlop={10}
title={_(msg`Feeds`)} accessibilityRole="button"
canGoBack={false} accessibilityLabel={_(msg`Edit Saved Feeds`)}
renderButton={renderHeaderBtn} accessibilityHint="Opens screen to edit Saved Feeds">
showBorder <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
/> </Link>
)}
{preferences ? <View /> : <ActivityIndicator />}
<FlatList
style={[!isTabletOrDesktop && s.flex1, styles.list]}
data={items}
keyExtractor={item => item.key}
contentContainerStyle={styles.contentContainer}
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={isPTR}
onRefresh={isUserSearching ? undefined : onPullToRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
initialNumToRender={10}
onEndReached={onEndReached}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
{hasSession && (
<FAB
testID="composeFAB"
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
)}
</View>
) )
}, }, [pal, _])
{isPublic: true},
) const renderItem = React.useCallback(
({item}: {item: FlatlistSlice}) => {
if (item.type === 'error') {
return <ErrorMessage message={item.error} />
} else if (
item.type === 'popularFeedsLoadingMore' ||
item.type === 'savedFeedsLoading'
) {
return (
<View style={s.p10}>
<ActivityIndicator />
</View>
)
} else if (item.type === 'savedFeedsHeader') {
if (!isMobile) {
return (
<View
style={[
pal.view,
styles.header,
pal.border,
{
borderBottomWidth: 1,
},
]}>
<Text type="title-lg" style={[pal.text, s.bold]}>
<Trans>My Feeds</Trans>
</Text>
<Link
href="/settings/saved-feeds"
accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityHint="">
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
</Link>
</View>
)
}
return <View />
} else if (item.type === 'savedFeedNoResults') {
return (
<View
style={{
paddingHorizontal: 16,
paddingTop: 10,
}}>
<Text type="lg" style={pal.textLight}>
<Trans>You don't have any saved feeds!</Trans>
</Text>
</View>
)
} else if (item.type === 'savedFeed') {
return <SavedFeed feedUri={item.feedUri} />
} else if (item.type === 'popularFeedsHeader') {
return (
<>
<View
style={[
pal.view,
styles.header,
{
// This is first in the flatlist without a session -esb
marginTop: hasSession ? 16 : 0,
paddingLeft: isMobile ? 12 : undefined,
paddingRight: 10,
paddingBottom: isMobile ? 6 : undefined,
},
]}>
<Text type="title-lg" style={[pal.text, s.bold]}>
<Trans>Discover new feeds</Trans>
</Text>
{!isMobile && (
<SearchInput
query={query}
onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
style={{flex: 1, maxWidth: 250}}
/>
)}
</View>
{isMobile && (
<View style={{paddingHorizontal: 8, paddingBottom: 10}}>
<SearchInput
query={query}
onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
/>
</View>
)}
</>
)
} else if (item.type === 'popularFeedsLoading') {
return <FeedFeedLoadingPlaceholder />
} else if (item.type === 'popularFeed') {
return (
<FeedSourceCard
feedUri={item.feedUri}
showSaveBtn={hasSession}
showDescription
showLikes
/>
)
} else if (item.type === 'popularFeedsNoResults') {
return (
<View
style={{
paddingHorizontal: 16,
paddingTop: 10,
paddingBottom: '150%',
}}>
<Text type="lg" style={pal.textLight}>
<Trans>No results found for "{query}"</Trans>
</Text>
</View>
)
}
return null
},
[
_,
hasSession,
isMobile,
pal,
query,
onChangeQuery,
onPressCancelSearch,
onSubmitQuery,
],
)
return (
<View style={[pal.view, styles.container]}>
{isMobile && (
<ViewHeader
title={_(msg`Feeds`)}
canGoBack={false}
renderButton={renderHeaderBtn}
showBorder
/>
)}
{preferences ? <View /> : <ActivityIndicator />}
<FlatList
style={[!isTabletOrDesktop && s.flex1, styles.list]}
data={items}
keyExtractor={item => item.key}
contentContainerStyle={styles.contentContainer}
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={isPTR}
onRefresh={isUserSearching ? undefined : onPullToRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
initialNumToRender={10}
onEndReached={onEndReached}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
{hasSession && (
<FAB
testID="composeFAB"
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
)}
</View>
)
}
function SavedFeed({feedUri}: {feedUri: string}) { function SavedFeed({feedUri}: {feedUri: string}) {
const pal = usePalette('default') const pal = usePalette('default')

View File

@ -3,7 +3,6 @@ import {View, ActivityIndicator, StyleSheet} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
@ -17,29 +16,24 @@ import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = withAuthRequired( export function HomeScreen(props: Props) {
function HomeScreenImpl(props: Props) { const {hasSession} = useSession()
const {hasSession} = useSession() const {data: preferences} = usePreferencesQuery()
const {data: preferences} = usePreferencesQuery()
if (!hasSession) { if (!hasSession) {
return <HomeScreenPublic /> return <HomeScreenPublic />
} }
if (preferences) { if (preferences) {
return <HomeScreenReady {...props} preferences={preferences} /> return <HomeScreenReady {...props} preferences={preferences} />
} else { } else {
return ( return (
<View style={styles.loading}> <View style={styles.loading}>
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
</View> </View>
) )
} }
}, }
{
isPublic: true,
},
)
function HomeScreenPublic() { function HomeScreenPublic() {
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()

View File

@ -4,7 +4,6 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {MyLists} from '#/view/com/lists/MyLists' import {MyLists} from '#/view/com/lists/MyLists'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
@ -18,70 +17,68 @@ import {useModalControls} from '#/state/modals'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
export const ListsScreen = withAuthRequired( export function ListsScreen({}: Props) {
function ListsScreenImpl({}: Props) { const pal = usePalette('default')
const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {isMobile} = useWebMediaQueries()
const {isMobile} = useWebMediaQueries() const navigation = useNavigation<NavigationProp>()
const navigation = useNavigation<NavigationProp>() const {openModal} = useModalControls()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
const onPressNewList = React.useCallback(() => { const onPressNewList = React.useCallback(() => {
openModal({ openModal({
name: 'create-or-edit-list', name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#curatelist', purpose: 'app.bsky.graph.defs#curatelist',
onSave: (uri: string) => { onSave: (uri: string) => {
try { try {
const urip = new AtUri(uri) const urip = new AtUri(uri)
navigation.navigate('ProfileList', { navigation.navigate('ProfileList', {
name: urip.hostname, name: urip.hostname,
rkey: urip.rkey, rkey: urip.rkey,
}) })
} catch {} } catch {}
}, },
}) })
}, [openModal, navigation]) }, [openModal, navigation])
return ( return (
<View style={s.hContentRegion} testID="listsScreen"> <View style={s.hContentRegion} testID="listsScreen">
<SimpleViewHeader <SimpleViewHeader
showBackButton={isMobile} showBackButton={isMobile}
style={ style={
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
}> }>
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
<Trans>User Lists</Trans> <Trans>User Lists</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>Public, shareable lists which can drive feeds.</Trans>
</Text>
</View>
<View>
<Button
testID="newUserListBtn"
type="default"
onPress={onPressNewList}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}>
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
<Text type="button" style={pal.text}>
<Trans>New</Trans>
</Text> </Text>
<Text style={pal.textLight}> </Button>
<Trans>Public, shareable lists which can drive feeds.</Trans> </View>
</Text> </SimpleViewHeader>
</View> <MyLists filter="curate" style={s.flexGrow1} />
<View> </View>
<Button )
testID="newUserListBtn" }
type="default"
onPress={onPressNewList}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}>
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
<Text type="button" style={pal.text}>
<Trans>New</Trans>
</Text>
</Button>
</View>
</SimpleViewHeader>
<MyLists filter="curate" style={s.flexGrow1} />
</View>
)
},
)

View File

@ -6,7 +6,6 @@ import {
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {CenteredView} from '../com/util/Views' import {CenteredView} from '../com/util/Views'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
@ -21,100 +20,98 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
export const ModerationScreen = withAuthRequired( export function ModerationScreen({}: Props) {
function Moderation({}: Props) { const pal = usePalette('default')
const pal = usePalette('default') const {_} = useLingui()
const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics()
const {screen, track} = useAnalytics() const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrDesktop} = useWebMediaQueries() const {openModal} = useModalControls()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('Moderation') screen('Moderation')
setMinimalShellMode(false) setMinimalShellMode(false)
}, [screen, setMinimalShellMode]), }, [screen, setMinimalShellMode]),
) )
const onPressContentFiltering = React.useCallback(() => { const onPressContentFiltering = React.useCallback(() => {
track('Moderation:ContentfilteringButtonClicked') track('Moderation:ContentfilteringButtonClicked')
openModal({name: 'content-filtering-settings'}) openModal({name: 'content-filtering-settings'})
}, [track, openModal]) }, [track, openModal])
return ( return (
<CenteredView <CenteredView
style={[ style={[
s.hContentRegion, s.hContentRegion,
pal.border, pal.border,
isTabletOrDesktop ? styles.desktopContainer : pal.viewLight, isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
]} ]}
testID="moderationScreen"> testID="moderationScreen">
<ViewHeader title={_(msg`Moderation`)} showOnDesktop /> <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
<View style={styles.spacer} /> <View style={styles.spacer} />
<TouchableOpacity <TouchableOpacity
testID="contentFilteringBtn" testID="contentFilteringBtn"
style={[styles.linkCard, pal.view]} style={[styles.linkCard, pal.view]}
onPress={onPressContentFiltering} onPress={onPressContentFiltering}
accessibilityRole="tab" accessibilityRole="tab"
accessibilityHint="Content filtering" accessibilityHint="Content filtering"
accessibilityLabel=""> accessibilityLabel="">
<View style={[styles.iconContainer, pal.btn]}> <View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="eye" icon="eye"
style={pal.text as FontAwesomeIconStyle} style={pal.text as FontAwesomeIconStyle}
/> />
</View> </View>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
<Trans>Content filtering</Trans> <Trans>Content filtering</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<Link <Link
testID="moderationlistsBtn" testID="moderationlistsBtn"
style={[styles.linkCard, pal.view]} style={[styles.linkCard, pal.view]}
href="/moderation/modlists"> href="/moderation/modlists">
<View style={[styles.iconContainer, pal.btn]}> <View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="users-slash" icon="users-slash"
style={pal.text as FontAwesomeIconStyle} style={pal.text as FontAwesomeIconStyle}
/> />
</View> </View>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
<Trans>Moderation lists</Trans> <Trans>Moderation lists</Trans>
</Text> </Text>
</Link> </Link>
<Link <Link
testID="mutedAccountsBtn" testID="mutedAccountsBtn"
style={[styles.linkCard, pal.view]} style={[styles.linkCard, pal.view]}
href="/moderation/muted-accounts"> href="/moderation/muted-accounts">
<View style={[styles.iconContainer, pal.btn]}> <View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="user-slash" icon="user-slash"
style={pal.text as FontAwesomeIconStyle} style={pal.text as FontAwesomeIconStyle}
/> />
</View> </View>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
<Trans>Muted accounts</Trans> <Trans>Muted accounts</Trans>
</Text> </Text>
</Link> </Link>
<Link <Link
testID="blockedAccountsBtn" testID="blockedAccountsBtn"
style={[styles.linkCard, pal.view]} style={[styles.linkCard, pal.view]}
href="/moderation/blocked-accounts"> href="/moderation/blocked-accounts">
<View style={[styles.iconContainer, pal.btn]}> <View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="ban" icon="ban"
style={pal.text as FontAwesomeIconStyle} style={pal.text as FontAwesomeIconStyle}
/> />
</View> </View>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
<Trans>Blocked accounts</Trans> <Trans>Blocked accounts</Trans>
</Text> </Text>
</Link> </Link>
</CenteredView> </CenteredView>
) )
}, }
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
desktopContainer: { desktopContainer: {

View File

@ -10,7 +10,6 @@ import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
@ -30,146 +29,144 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'ModerationBlockedAccounts' 'ModerationBlockedAccounts'
> >
export const ModerationBlockedAccounts = withAuthRequired( export function ModerationBlockedAccounts({}: Props) {
function ModerationBlockedAccountsImpl({}: Props) { const pal = usePalette('default')
const pal = usePalette('default') const {_} = useLingui()
const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics()
const {screen} = useAnalytics() const [isPTRing, setIsPTRing] = React.useState(false)
const [isPTRing, setIsPTRing] = React.useState(false) const {
const { data,
data, isFetching,
isFetching, isError,
isError, error,
error, refetch,
refetch, hasNextPage,
hasNextPage, fetchNextPage,
fetchNextPage, isFetchingNextPage,
isFetchingNextPage, } = useMyBlockedAccountsQuery()
} = useMyBlockedAccountsQuery() const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
const isEmpty = !isFetching && !data?.pages[0]?.blocks.length const profiles = React.useMemo(() => {
const profiles = React.useMemo(() => { if (data?.pages) {
if (data?.pages) { return data.pages.flatMap(page => page.blocks)
return data.pages.flatMap(page => page.blocks) }
} return []
return [] }, [data])
}, [data])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('BlockedAccounts') screen('BlockedAccounts')
setMinimalShellMode(false) setMinimalShellMode(false)
}, [screen, setMinimalShellMode]), }, [screen, setMinimalShellMode]),
) )
const onRefresh = React.useCallback(async () => { const onRefresh = React.useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
try { try {
await refetch() await refetch()
} catch (err) { } catch (err) {
logger.error('Failed to refresh my muted accounts', {error: err}) logger.error('Failed to refresh my muted accounts', {error: err})
} }
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetching || !hasNextPage || isError) return
try { try {
await fetchNextPage() await fetchNextPage()
} catch (err) { } catch (err) {
logger.error('Failed to load more of my muted accounts', {error: err}) logger.error('Failed to load more of my muted accounts', {error: err})
} }
}, [isFetching, hasNextPage, isError, fetchNextPage]) }, [isFetching, hasNextPage, isError, fetchNextPage])
const renderItem = ({ const renderItem = ({
item, item,
index, index,
}: { }: {
item: ActorDefs.ProfileView item: ActorDefs.ProfileView
index: number index: number
}) => ( }) => (
<ProfileCard <ProfileCard
testID={`blockedAccount-${index}`} testID={`blockedAccount-${index}`}
key={item.did} key={item.did}
profile={item} profile={item}
/> />
) )
return ( return (
<CenteredView <CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="blockedAccountsScreen">
<ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
<Text
type="sm"
style={[ style={[
styles.container, styles.description,
isTabletOrDesktop && styles.containerDesktop, pal.text,
pal.view, isTabletOrDesktop && styles.descriptionDesktop,
pal.border, ]}>
]} <Trans>
testID="blockedAccountsScreen"> Blocked accounts cannot reply in your threads, mention you, or
<ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> otherwise interact with you. You will not see their content and they
<Text will be prevented from seeing yours.
type="sm" </Trans>
style={[ </Text>
styles.description, {isEmpty ? (
pal.text, <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
isTabletOrDesktop && styles.descriptionDesktop, {isError ? (
]}> <ErrorScreen
<Trans> title="Oops!"
Blocked accounts cannot reply in your threads, mention you, or message={cleanError(error)}
otherwise interact with you. You will not see their content and they onPressTryAgain={refetch}
will be prevented from seeing yours. />
</Trans> ) : (
</Text> <View style={[styles.empty, pal.viewLight]}>
{isEmpty ? ( <Text type="lg" style={[pal.text, styles.emptyText]}>
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> <Trans>
{isError ? ( You have not blocked any accounts yet. To block an account, go
<ErrorScreen to their profile and selected "Block account" from the menu on
title="Oops!" their account.
message={cleanError(error)} </Trans>
onPressTryAgain={refetch} </Text>
/> </View>
) : ( )}
<View style={[styles.empty, pal.viewLight]}> </View>
<Text type="lg" style={[pal.text, styles.emptyText]}> ) : (
<Trans> <FlatList
You have not blocked any accounts yet. To block an account, style={[!isTabletOrDesktop && styles.flex1]}
go to their profile and selected "Block account" from the data={profiles}
menu on their account. keyExtractor={(item: ActorDefs.ProfileView) => item.did}
</Trans> refreshControl={
</Text> <RefreshControl
</View> refreshing={isPTRing}
)} onRefresh={onRefresh}
</View> tintColor={pal.colors.text}
) : ( titleColor={pal.colors.text}
<FlatList />
style={[!isTabletOrDesktop && styles.flex1]} }
data={profiles} onEndReached={onEndReached}
keyExtractor={(item: ActorDefs.ProfileView) => item.did} renderItem={renderItem}
refreshControl={ initialNumToRender={15}
<RefreshControl // FIXME(dan)
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
// FIXME(dan)
ListFooterComponent={() => ( ListFooterComponent={() => (
<View style={styles.footer}> <View style={styles.footer}>
{(isFetching || isFetchingNextPage) && <ActivityIndicator />} {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
</View> </View>
)} )}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
/> />
)} )}
</CenteredView> </CenteredView>
) )
}, }
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -4,7 +4,6 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {MyLists} from '#/view/com/lists/MyLists' import {MyLists} from '#/view/com/lists/MyLists'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
@ -17,70 +16,68 @@ import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
export const ModerationModlistsScreen = withAuthRequired( export function ModerationModlistsScreen({}: Props) {
function ModerationModlistsScreenImpl({}: Props) { const pal = usePalette('default')
const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {isMobile} = useWebMediaQueries()
const {isMobile} = useWebMediaQueries() const navigation = useNavigation<NavigationProp>()
const navigation = useNavigation<NavigationProp>() const {openModal} = useModalControls()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
const onPressNewList = React.useCallback(() => { const onPressNewList = React.useCallback(() => {
openModal({ openModal({
name: 'create-or-edit-list', name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#modlist', purpose: 'app.bsky.graph.defs#modlist',
onSave: (uri: string) => { onSave: (uri: string) => {
try { try {
const urip = new AtUri(uri) const urip = new AtUri(uri)
navigation.navigate('ProfileList', { navigation.navigate('ProfileList', {
name: urip.hostname, name: urip.hostname,
rkey: urip.rkey, rkey: urip.rkey,
}) })
} catch {} } catch {}
}, },
}) })
}, [openModal, navigation]) }, [openModal, navigation])
return ( return (
<View style={s.hContentRegion} testID="moderationModlistsScreen"> <View style={s.hContentRegion} testID="moderationModlistsScreen">
<SimpleViewHeader <SimpleViewHeader
showBackButton={isMobile} showBackButton={isMobile}
style={ style={
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
}> }>
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
Moderation Lists Moderation Lists
</Text>
<Text style={pal.textLight}>
Public, shareable lists of users to mute or block in bulk.
</Text>
</View>
<View>
<Button
testID="newModListBtn"
type="default"
onPress={onPressNewList}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}>
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
<Text type="button" style={pal.text}>
New
</Text> </Text>
<Text style={pal.textLight}> </Button>
Public, shareable lists of users to mute or block in bulk. </View>
</Text> </SimpleViewHeader>
</View> <MyLists filter="mod" style={s.flexGrow1} />
<View> </View>
<Button )
testID="newModListBtn" }
type="default"
onPress={onPressNewList}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}>
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
<Text type="button" style={pal.text}>
New
</Text>
</Button>
</View>
</SimpleViewHeader>
<MyLists filter="mod" style={s.flexGrow1} />
</View>
)
},
)

View File

@ -10,7 +10,6 @@ import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
@ -30,145 +29,143 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'ModerationMutedAccounts' 'ModerationMutedAccounts'
> >
export const ModerationMutedAccounts = withAuthRequired( export function ModerationMutedAccounts({}: Props) {
function ModerationMutedAccountsImpl({}: Props) { const pal = usePalette('default')
const pal = usePalette('default') const {_} = useLingui()
const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics()
const {screen} = useAnalytics() const [isPTRing, setIsPTRing] = React.useState(false)
const [isPTRing, setIsPTRing] = React.useState(false) const {
const { data,
data, isFetching,
isFetching, isError,
isError, error,
error, refetch,
refetch, hasNextPage,
hasNextPage, fetchNextPage,
fetchNextPage, isFetchingNextPage,
isFetchingNextPage, } = useMyMutedAccountsQuery()
} = useMyMutedAccountsQuery() const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
const isEmpty = !isFetching && !data?.pages[0]?.mutes.length const profiles = React.useMemo(() => {
const profiles = React.useMemo(() => { if (data?.pages) {
if (data?.pages) { return data.pages.flatMap(page => page.mutes)
return data.pages.flatMap(page => page.mutes) }
} return []
return [] }, [data])
}, [data])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('MutedAccounts') screen('MutedAccounts')
setMinimalShellMode(false) setMinimalShellMode(false)
}, [screen, setMinimalShellMode]), }, [screen, setMinimalShellMode]),
) )
const onRefresh = React.useCallback(async () => { const onRefresh = React.useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
try { try {
await refetch() await refetch()
} catch (err) { } catch (err) {
logger.error('Failed to refresh my muted accounts', {error: err}) logger.error('Failed to refresh my muted accounts', {error: err})
} }
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetching || !hasNextPage || isError) return
try { try {
await fetchNextPage() await fetchNextPage()
} catch (err) { } catch (err) {
logger.error('Failed to load more of my muted accounts', {error: err}) logger.error('Failed to load more of my muted accounts', {error: err})
} }
}, [isFetching, hasNextPage, isError, fetchNextPage]) }, [isFetching, hasNextPage, isError, fetchNextPage])
const renderItem = ({ const renderItem = ({
item, item,
index, index,
}: { }: {
item: ActorDefs.ProfileView item: ActorDefs.ProfileView
index: number index: number
}) => ( }) => (
<ProfileCard <ProfileCard
testID={`mutedAccount-${index}`} testID={`mutedAccount-${index}`}
key={item.did} key={item.did}
profile={item} profile={item}
/> />
) )
return ( return (
<CenteredView <CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="mutedAccountsScreen">
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
<Text
type="sm"
style={[ style={[
styles.container, styles.description,
isTabletOrDesktop && styles.containerDesktop, pal.text,
pal.view, isTabletOrDesktop && styles.descriptionDesktop,
pal.border, ]}>
]} <Trans>
testID="mutedAccountsScreen"> Muted accounts have their posts removed from your feed and from your
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> notifications. Mutes are completely private.
<Text </Trans>
type="sm" </Text>
style={[ {isEmpty ? (
styles.description, <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
pal.text, {isError ? (
isTabletOrDesktop && styles.descriptionDesktop, <ErrorScreen
]}> title="Oops!"
<Trans> message={cleanError(error)}
Muted accounts have their posts removed from your feed and from your onPressTryAgain={refetch}
notifications. Mutes are completely private. />
</Trans> ) : (
</Text> <View style={[styles.empty, pal.viewLight]}>
{isEmpty ? ( <Text type="lg" style={[pal.text, styles.emptyText]}>
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> <Trans>
{isError ? ( You have not muted any accounts yet. To mute an account, go to
<ErrorScreen their profile and selected "Mute account" from the menu on
title="Oops!" their account.
message={cleanError(error)} </Trans>
onPressTryAgain={refetch} </Text>
/> </View>
) : ( )}
<View style={[styles.empty, pal.viewLight]}> </View>
<Text type="lg" style={[pal.text, styles.emptyText]}> ) : (
<Trans> <FlatList
You have not muted any accounts yet. To mute an account, go style={[!isTabletOrDesktop && styles.flex1]}
to their profile and selected "Mute account" from the menu data={profiles}
on their account. keyExtractor={item => item.did}
</Trans> refreshControl={
</Text> <RefreshControl
</View> refreshing={isPTRing}
)} onRefresh={onRefresh}
</View> tintColor={pal.colors.text}
) : ( titleColor={pal.colors.text}
<FlatList />
style={[!isTabletOrDesktop && styles.flex1]} }
data={profiles} onEndReached={onEndReached}
keyExtractor={item => item.did} renderItem={renderItem}
refreshControl={ initialNumToRender={15}
<RefreshControl // FIXME(dan)
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
// FIXME(dan)
ListFooterComponent={() => ( ListFooterComponent={() => (
<View style={styles.footer}> <View style={styles.footer}>
{(isFetching || isFetchingNextPage) && <ActivityIndicator />} {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
</View> </View>
)} )}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
/> />
)} )}
</CenteredView> </CenteredView>
) )
}, }
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -6,7 +6,6 @@ import {
NativeStackScreenProps, NativeStackScreenProps,
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
@ -28,102 +27,100 @@ type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
'Notifications' 'Notifications'
> >
export const NotificationsScreen = withAuthRequired( export function NotificationsScreen({}: Props) {
function NotificationsScreenImpl({}: Props) { const {_} = useLingui()
const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() const scrollElRef = React.useRef<FlatList>(null)
const scrollElRef = React.useRef<FlatList>(null) const {screen} = useAnalytics()
const {screen} = useAnalytics() const pal = usePalette('default')
const pal = usePalette('default') const {isDesktop} = useWebMediaQueries()
const {isDesktop} = useWebMediaQueries() const unreadNotifs = useUnreadNotifications()
const unreadNotifs = useUnreadNotifications() const queryClient = useQueryClient()
const queryClient = useQueryClient() const hasNew = !!unreadNotifs
const hasNew = !!unreadNotifs
// event handlers // event handlers
// = // =
const scrollToTop = React.useCallback(() => { const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: 0}) scrollElRef.current?.scrollToOffset({offset: 0})
resetMainScroll() resetMainScroll()
}, [scrollElRef, resetMainScroll]) }, [scrollElRef, resetMainScroll])
const onPressLoadLatest = React.useCallback(() => { const onPressLoadLatest = React.useCallback(() => {
scrollToTop() scrollToTop()
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: NOTIFS_RQKEY(), queryKey: NOTIFS_RQKEY(),
}) })
}, [scrollToTop, queryClient]) }, [scrollToTop, queryClient])
// on-visible setup // on-visible setup
// = // =
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
logger.debug('NotificationsScreen: Updating feed') logger.debug('NotificationsScreen: Updating feed')
screen('Notifications') screen('Notifications')
return listenSoftReset(onPressLoadLatest) return listenSoftReset(onPressLoadLatest)
}, [screen, onPressLoadLatest, setMinimalShellMode]), }, [screen, onPressLoadLatest, setMinimalShellMode]),
) )
const ListHeaderComponent = React.useCallback(() => { const ListHeaderComponent = React.useCallback(() => {
if (isDesktop) { if (isDesktop) {
return ( return (
<View <View
style={[ style={[
pal.view, pal.view,
{ {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 18, paddingHorizontal: 18,
paddingVertical: 12, paddingVertical: 12,
}, },
]}> ]}>
<TextLink <TextLink
type="title-lg" type="title-lg"
href="/notifications" href="/notifications"
style={[pal.text, {fontWeight: 'bold'}]} style={[pal.text, {fontWeight: 'bold'}]}
text={ text={
<> <>
<Trans>Notifications</Trans>{' '} <Trans>Notifications</Trans>{' '}
{hasNew && ( {hasNew && (
<View <View
style={{ style={{
top: -8, top: -8,
backgroundColor: colors.blue3, backgroundColor: colors.blue3,
width: 8, width: 8,
height: 8, height: 8,
borderRadius: 4, borderRadius: 4,
}} }}
/> />
)} )}
</> </>
} }
onPress={emitSoftReset} onPress={emitSoftReset}
/>
</View>
)
}
return <></>
}, [isDesktop, pal, hasNew])
return (
<View testID="notificationsScreen" style={s.hContentRegion}>
<ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
<Feed
onScroll={onMainScroll}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
/>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
label={_(msg`Load new notifications`)}
showIndicator={hasNew}
/> />
)} </View>
</View> )
) }
}, return <></>
) }, [isDesktop, pal, hasNew])
return (
<View testID="notificationsScreen" style={s.hContentRegion}>
<ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
<Feed
onScroll={onMainScroll}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
/>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
label={_(msg`Load new notifications`)}
showIndicator={hasNew}
/>
)}
</View>
)
}

View File

@ -2,7 +2,6 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
@ -11,25 +10,22 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
export const PostLikedByScreen = withAuthRequired( export const PostLikedByScreen = ({route}: Props) => {
({route}: Props) => { const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {name, rkey} = route.params
const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const {_} = useLingui()
const {_} = useLingui()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
return ( return (
<View> <View>
<ViewHeader title={_(msg`Liked by`)} /> <ViewHeader title={_(msg`Liked by`)} />
<PostLikedByComponent uri={uri} /> <PostLikedByComponent uri={uri} />
</View> </View>
) )
}, }
{isPublic: true},
)

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy' import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
@ -11,25 +10,22 @@ import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
export const PostRepostedByScreen = withAuthRequired( export const PostRepostedByScreen = ({route}: Props) => {
({route}: Props) => { const {name, rkey} = route.params
const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {_} = useLingui()
const {_} = useLingui()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
return ( return (
<View> <View>
<ViewHeader title={_(msg`Reposted by`)} /> <ViewHeader title={_(msg`Reposted by`)} />
<PostRepostedByComponent uri={uri} /> <PostRepostedByComponent uri={uri} />
</View> </View>
) )
}, }
{isPublic: true},
)

View File

@ -5,7 +5,6 @@ import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ComposePrompt} from 'view/com/composer/Prompt' import {ComposePrompt} from 'view/com/composer/Prompt'
@ -27,85 +26,82 @@ import {CenteredView} from '../com/util/Views'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export const PostThreadScreen = withAuthRequired( export function PostThreadScreen({route}: Props) {
function PostThreadScreenImpl({route}: Props) { const queryClient = useQueryClient()
const queryClient = useQueryClient() const {_} = useLingui()
const {_} = useLingui() const {fabMinimalShellTransform} = useMinimalShellMode()
const {fabMinimalShellTransform} = useMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {openComposer} = useComposerControls()
const {openComposer} = useComposerControls() const safeAreaInsets = useSafeAreaInsets()
const safeAreaInsets = useSafeAreaInsets() const {name, rkey} = route.params
const {name, rkey} = route.params const {isMobile} = useWebMediaQueries()
const {isMobile} = useWebMediaQueries() const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
)
const onPressReply = React.useCallback(() => {
if (!resolvedUri) {
return
}
const thread = queryClient.getQueryData<ThreadNode>(
POST_THREAD_RQKEY(resolvedUri.uri),
) )
if (thread?.type !== 'post') {
const onPressReply = React.useCallback(() => { return
if (!resolvedUri) { }
return openComposer({
} replyTo: {
const thread = queryClient.getQueryData<ThreadNode>( uri: thread.post.uri,
POST_THREAD_RQKEY(resolvedUri.uri), cid: thread.post.cid,
) text: thread.record.text,
if (thread?.type !== 'post') { author: {
return handle: thread.post.author.handle,
} displayName: thread.post.author.displayName,
openComposer({ avatar: thread.post.author.avatar,
replyTo: {
uri: thread.post.uri,
cid: thread.post.cid,
text: thread.record.text,
author: {
handle: thread.post.author.handle,
displayName: thread.post.author.displayName,
avatar: thread.post.author.avatar,
},
}, },
onPost: () => },
queryClient.invalidateQueries({ onPost: () =>
queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), queryClient.invalidateQueries({
}), queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
}) }),
}, [openComposer, queryClient, resolvedUri]) })
}, [openComposer, queryClient, resolvedUri])
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
{isMobile && <ViewHeader title={_(msg`Post`)} />} {isMobile && <ViewHeader title={_(msg`Post`)} />}
<View style={s.flex1}> <View style={s.flex1}>
{uriError ? ( {uriError ? (
<CenteredView> <CenteredView>
<ErrorMessage message={String(uriError)} /> <ErrorMessage message={String(uriError)} />
</CenteredView> </CenteredView>
) : ( ) : (
<PostThreadComponent <PostThreadComponent
uri={resolvedUri?.uri} uri={resolvedUri?.uri}
onPressReply={onPressReply} onPressReply={onPressReply}
/> />
)}
</View>
{isMobile && (
<Animated.View
style={[
styles.prompt,
fabMinimalShellTransform,
{
bottom: clamp(safeAreaInsets.bottom, 15, 30),
},
]}>
<ComposePrompt onPressCompose={onPressReply} />
</Animated.View>
)} )}
</View> </View>
) {isMobile && (
}, <Animated.View
{isPublic: true}, style={[
) styles.prompt,
fabMinimalShellTransform,
{
bottom: clamp(safeAreaInsets.bottom, 15, 30),
},
]}>
<ComposePrompt onPressCompose={onPressReply} />
</Animated.View>
)}
</View>
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
prompt: { prompt: {

View File

@ -5,7 +5,6 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewSelectorHandle} from '../com/util/ViewSelector' import {ViewSelectorHandle} from '../com/util/ViewSelector'
import {CenteredView, FlatList} from '../com/util/Views' import {CenteredView, FlatList} from '../com/util/Views'
import {ScreenHider} from 'view/com/util/moderation/ScreenHider' import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
@ -43,83 +42,78 @@ interface SectionRef {
} }
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
export const ProfileScreen = withAuthRequired( export function ProfileScreen({route}: Props) {
function ProfileScreenImpl({route}: Props) { const {currentAccount} = useSession()
const {currentAccount} = useSession() const name =
const name = route.params.name === 'me' ? currentAccount?.did : route.params.name
route.params.name === 'me' ? currentAccount?.did : route.params.name const moderationOpts = useModerationOpts()
const moderationOpts = useModerationOpts() const {
const { data: resolvedDid,
data: resolvedDid, error: resolveError,
error: resolveError, refetch: refetchDid,
refetch: refetchDid, isFetching: isFetchingDid,
isFetching: isFetchingDid, } = useResolveDidQuery(name)
} = useResolveDidQuery(name) const {
const { data: profile,
data: profile, error: profileError,
error: profileError, refetch: refetchProfile,
refetch: refetchProfile, isFetching: isFetchingProfile,
isFetching: isFetchingProfile, } = useProfileQuery({
} = useProfileQuery({ did: resolvedDid?.did,
did: resolvedDid?.did, })
})
const onPressTryAgain = React.useCallback(() => { const onPressTryAgain = React.useCallback(() => {
if (resolveError) { if (resolveError) {
refetchDid() refetchDid()
} else { } else {
refetchProfile() refetchProfile()
} }
}, [resolveError, refetchDid, refetchProfile]) }, [resolveError, refetchDid, refetchProfile])
if (isFetchingDid || isFetchingProfile || !moderationOpts) { if (isFetchingDid || isFetchingProfile || !moderationOpts) {
return ( return (
<CenteredView> <CenteredView>
<ProfileHeader <ProfileHeader
profile={null} profile={null}
moderation={null} moderation={null}
isProfilePreview={true} isProfilePreview={true}
/>
</CenteredView>
)
}
if (resolveError || profileError) {
return (
<CenteredView>
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message={cleanError(resolveError || profileError)}
onPressTryAgain={onPressTryAgain}
/>
</CenteredView>
)
}
if (profile && moderationOpts) {
return (
<ProfileScreenLoaded
profile={profile}
moderationOpts={moderationOpts}
hideBackButton={!!route.params.hideBackButton}
/> />
) </CenteredView>
} )
// should never happen }
if (resolveError || profileError) {
return ( return (
<CenteredView> <CenteredView>
<ErrorScreen <ErrorScreen
testID="profileErrorScreen" testID="profileErrorScreen"
title="Oops!" title="Oops!"
message="Something went wrong and we're not sure what." message={cleanError(resolveError || profileError)}
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
/> />
</CenteredView> </CenteredView>
) )
}, }
{ if (profile && moderationOpts) {
isPublic: true, return (
}, <ProfileScreenLoaded
) profile={profile}
moderationOpts={moderationOpts}
hideBackButton={!!route.params.hideBackButton}
/>
)
}
// should never happen
return (
<CenteredView>
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message="Something went wrong and we're not sure what."
onPressTryAgain={onPressTryAgain}
/>
</CenteredView>
)
}
function ProfileScreenLoaded({ function ProfileScreenLoaded({
profile: profileUnshadowed, profile: profileUnshadowed,

View File

@ -16,7 +16,6 @@ import {CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {colors, s} from 'lib/styles' import {colors, s} from 'lib/styles'
import {FeedDescriptor} from '#/state/queries/post-feed' import {FeedDescriptor} from '#/state/queries/post-feed'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed' import {Feed} from 'view/com/posts/Feed'
@ -69,70 +68,65 @@ interface SectionRef {
} }
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
export const ProfileFeedScreen = withAuthRequired( export function ProfileFeedScreen(props: Props) {
function ProfileFeedScreenImpl(props: Props) { const {rkey, name: handleOrDid} = props.route.params
const {rkey, name: handleOrDid} = props.route.params
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const uri = useMemo( const uri = useMemo(
() => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
[rkey, handleOrDid], [rkey, handleOrDid],
) )
const {error, data: resolvedUri} = useResolveUriQuery(uri) const {error, data: resolvedUri} = useResolveUriQuery(uri)
const onPressBack = React.useCallback(() => { const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
navigation.goBack() navigation.goBack()
} else { } else {
navigation.navigate('Home') navigation.navigate('Home')
}
}, [navigation])
if (error) {
return (
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb10]}>
<Trans>Could not load feed</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb20]}>
{error.toString()}
</Text>
<View style={{flexDirection: 'row'}}>
<Button
type="default"
accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page"
onPress={onPressBack}
style={{flexShrink: 1}}>
<Text type="button" style={pal.text}>
<Trans>Go Back</Trans>
</Text>
</Button>
</View>
</View>
</CenteredView>
)
} }
}, [navigation])
return resolvedUri ? ( if (error) {
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} /> return (
) : (
<CenteredView> <CenteredView>
<View style={s.p20}> <View style={[pal.view, pal.border, styles.notFoundContainer]}>
<ActivityIndicator size="large" /> <Text type="title-lg" style={[pal.text, s.mb10]}>
<Trans>Could not load feed</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb20]}>
{error.toString()}
</Text>
<View style={{flexDirection: 'row'}}>
<Button
type="default"
accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page"
onPress={onPressBack}
style={{flexShrink: 1}}>
<Text type="button" style={pal.text}>
<Trans>Go Back</Trans>
</Text>
</Button>
</View>
</View> </View>
</CenteredView> </CenteredView>
) )
}, }
{
isPublic: true, return resolvedUri ? (
}, <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
) ) : (
<CenteredView>
<View style={s.p20}>
<ActivityIndicator size="large" />
</View>
</CenteredView>
)
}
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()

View File

@ -2,7 +2,6 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
@ -11,25 +10,22 @@ import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'>
export const ProfileFeedLikedByScreen = withAuthRequired( export const ProfileFeedLikedByScreen = ({route}: Props) => {
({route}: Props) => { const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {name, rkey} = route.params
const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) const {_} = useLingui()
const {_} = useLingui()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
return ( return (
<View> <View>
<ViewHeader title={_(msg`Liked by`)} /> <ViewHeader title={_(msg`Liked by`)} />
<PostLikedByComponent uri={uri} /> <PostLikedByComponent uri={uri} />
</View> </View>
) )
}, }
{isPublic: true},
)

View File

@ -2,7 +2,6 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
@ -10,24 +9,21 @@ import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
export const ProfileFollowersScreen = withAuthRequired( export const ProfileFollowersScreen = ({route}: Props) => {
({route}: Props) => { const {name} = route.params
const {name} = route.params const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {_} = useLingui()
const {_} = useLingui()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
return ( return (
<View> <View>
<ViewHeader title={_(msg`Followers`)} /> <ViewHeader title={_(msg`Followers`)} />
<ProfileFollowersComponent name={name} /> <ProfileFollowersComponent name={name} />
</View> </View>
) )
}, }
{isPublic: true},
)

View File

@ -2,7 +2,6 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
@ -10,24 +9,21 @@ import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
export const ProfileFollowsScreen = withAuthRequired( export const ProfileFollowsScreen = ({route}: Props) => {
({route}: Props) => { const {name} = route.params
const {name} = route.params const setMinimalShellMode = useSetMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const {_} = useLingui()
const {_} = useLingui()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
return ( return (
<View> <View>
<ViewHeader title={_(msg`Following`)} /> <ViewHeader title={_(msg`Following`)} />
<ProfileFollowsComponent name={name} /> <ProfileFollowsComponent name={name} />
</View> </View>
) )
}, }
{isPublic: true},
)

View File

@ -12,7 +12,6 @@ import {useNavigation} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed' import {Feed} from 'view/com/posts/Feed'
@ -64,42 +63,40 @@ interface SectionRef {
} }
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
export const ProfileListScreen = withAuthRequired( export function ProfileListScreen(props: Props) {
function ProfileListScreenImpl(props: Props) { const {name: handleOrDid, rkey} = props.route.params
const {name: handleOrDid, rkey} = props.route.params const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
const {data: resolvedUri, error: resolveError} = useResolveUriQuery( AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), )
) const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
if (resolveError) { if (resolveError) {
return ( return (
<CenteredView>
<ErrorScreen
error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`}
/>
</CenteredView>
)
}
if (listError) {
return (
<CenteredView>
<ErrorScreen error={cleanError(listError)} />
</CenteredView>
)
}
return resolvedUri && list ? (
<ProfileListScreenLoaded {...props} uri={resolvedUri.uri} list={list} />
) : (
<CenteredView> <CenteredView>
<View style={s.p20}> <ErrorScreen
<ActivityIndicator size="large" /> error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`}
</View> />
</CenteredView> </CenteredView>
) )
}, }
) if (listError) {
return (
<CenteredView>
<ErrorScreen error={cleanError(listError)} />
</CenteredView>
)
}
return resolvedUri && list ? (
<ProfileListScreenLoaded {...props} uri={resolvedUri.uri} list={list} />
) : (
<CenteredView>
<View style={s.p20}>
<ActivityIndicator size="large" />
</View>
</CenteredView>
)
}
function ProfileListScreenLoaded({ function ProfileListScreenLoaded({
route, route,

View File

@ -14,7 +14,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from 'view/com/util/ViewHeader' import {ViewHeader} from 'view/com/util/ViewHeader'
import {ScrollView, CenteredView} from 'view/com/util/Views' import {ScrollView, CenteredView} from 'view/com/util/Views'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
@ -51,7 +50,7 @@ const HITSLOP_BOTTOM = {
} }
type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) { export function SavedFeeds({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
@ -147,7 +146,7 @@ export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) {
</ScrollView> </ScrollView>
</CenteredView> </CenteredView>
) )
}) }
function ListItem({ function ListItem({
feedUri, feedUri,

View File

@ -1,6 +1,3 @@
import {withAuthRequired} from '#/view/com/auth/withAuthRequired'
import {SearchScreenMobile} from '#/view/screens/Search/Search' import {SearchScreenMobile} from '#/view/screens/Search/Search'
export const SearchScreen = withAuthRequired(SearchScreenMobile, { export const SearchScreen = SearchScreenMobile
isPublic: true,
})

View File

@ -1,6 +1,3 @@
import {withAuthRequired} from '#/view/com/auth/withAuthRequired'
import {SearchScreenDesktop} from '#/view/screens/Search/Search' import {SearchScreenDesktop} from '#/view/screens/Search/Search'
export const SearchScreen = withAuthRequired(SearchScreenDesktop, { export const SearchScreen = SearchScreenDesktop
isPublic: true,
})

View File

@ -20,7 +20,6 @@ import {
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import * as AppInfo from 'lib/app-info' import * as AppInfo from 'lib/app-info'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {ScrollView} from '../com/util/Views' import {ScrollView} from '../com/util/Views'
@ -141,7 +140,7 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
} }
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export const SettingsScreen = withAuthRequired(function Settings({}: Props) { export function SettingsScreen({}: Props) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const colorMode = useColorMode() const colorMode = useColorMode()
const setColorMode = useSetColorMode() const setColorMode = useSetColorMode()
@ -731,7 +730,7 @@ export const SettingsScreen = withAuthRequired(function Settings({}: Props) {
</ScrollView> </ScrollView>
</View> </View>
) )
}) }
function EmailConfirmationNotice() { function EmailConfirmationNotice() {
const pal = usePalette('default') const pal = usePalette('default')

View File

@ -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)

View File

@ -1,7 +1,5 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {View, StyleSheet, TouchableOpacity} from 'react-native' import {View, StyleSheet, TouchableOpacity} from 'react-native'
import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav'
import {ErrorBoundary} from '../com/util/ErrorBoundary' import {ErrorBoundary} from '../com/util/ErrorBoundary'
import {Lightbox} from '../com/lightbox/Lightbox' import {Lightbox} from '../com/lightbox/Lightbox'
import {ModalsContainer} from '../com/modals/Modal' import {ModalsContainer} from '../com/modals/Modal'
@ -11,27 +9,19 @@ import {s, colors} from 'lib/styles'
import {RoutesContainer, FlatNavigator} from '../../Navigation' import {RoutesContainer, FlatNavigator} from '../../Navigation'
import {DrawerContent} from './Drawer' import {DrawerContent} from './Drawer'
import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {useAuxClick} from 'lib/hooks/useAuxClick' import {useAuxClick} from 'lib/hooks/useAuxClick'
import {t} from '@lingui/macro' import {t} from '@lingui/macro'
import { import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
useIsDrawerOpen,
useSetDrawerOpen,
useOnboardingState,
} from '#/state/shell'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {useLoggedOutView} from '#/state/shell/logged-out'
function ShellInner() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const onboardingState = useOnboardingState() const {isDesktop} = useWebMediaQueries()
const {isDesktop, isMobile} = useWebMediaQueries()
const navigator = useNavigation<NavigationProp>() const navigator = useNavigation<NavigationProp>()
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const {showLoggedOut} = useLoggedOutView()
useAuxClick() useAuxClick()
@ -42,8 +32,6 @@ function ShellInner() {
return unsubscribe return unsubscribe
}, [navigator, closeAllActiveElements]) }, [navigator, closeAllActiveElements])
const showBottomBar = isMobile && !onboardingState.isActive
const showSideNavs = !isMobile && !onboardingState.isActive && !showLoggedOut
return ( return (
<View style={[s.hContentRegion, {overflow: 'hidden'}]}> <View style={[s.hContentRegion, {overflow: 'hidden'}]}>
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
@ -51,22 +39,9 @@ function ShellInner() {
<FlatNavigator /> <FlatNavigator />
</ErrorBoundary> </ErrorBoundary>
</View> </View>
{showSideNavs && (
<>
<DesktopLeftNav />
<DesktopRightNav />
</>
)}
<Composer winHeight={0} /> <Composer winHeight={0} />
{showBottomBar && <BottomBarWeb />}
<ModalsContainer /> <ModalsContainer />
<Lightbox /> <Lightbox />
{!isDesktop && isDrawerOpen && ( {!isDesktop && isDrawerOpen && (
<TouchableOpacity <TouchableOpacity
onPress={() => setDrawerOpen(false)} onPress={() => setDrawerOpen(false)}