diff options
author | dan <dan.abramov@gmail.com> | 2023-11-24 22:31:33 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-24 22:31:33 +0000 |
commit | f2d164ec23247d878f7f019d568a3073a5ae94c4 (patch) | |
tree | 7db9131e8b1f642494bb0b626a75a5ec7be36755 /src | |
parent | 4b59a21cacc36d3c05e68d22379538c0f32550c9 (diff) | |
download | voidsky-f2d164ec23247d878f7f019d568a3073a5ae94c4.tar.zst |
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
Diffstat (limited to 'src')
29 files changed, 1607 insertions, 1645 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index b9f9519c9..64c7e718f 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -28,6 +28,7 @@ import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as PrefsStateProvider} from 'state/preferences' +import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' import I18nProvider from './locale/i18nProvider' import { Provider as SessionProvider, @@ -42,7 +43,7 @@ SplashScreen.preventAutoHideAsync() function InnerApp() { const colorMode = useColorMode() - const {isInitialLoad} = useSession() + const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() // init @@ -69,19 +70,25 @@ function InnerApp() { */ return ( - <UnreadNotifsProvider> - <ThemeProvider theme={colorMode}> - <analytics.Provider> - {/* All components should be within this provider */} - <RootSiblingParent> - <GestureHandlerRootView style={s.h100pct}> - <TestCtrls /> - <Shell /> - </GestureHandlerRootView> - </RootSiblingParent> - </analytics.Provider> - </ThemeProvider> - </UnreadNotifsProvider> + <React.Fragment + // Resets the entire tree below when it changes: + key={currentAccount?.did}> + <LoggedOutViewProvider> + <UnreadNotifsProvider> + <ThemeProvider theme={colorMode}> + <analytics.Provider> + {/* All components should be within this provider */} + <RootSiblingParent> + <GestureHandlerRootView style={s.h100pct}> + <TestCtrls /> + <Shell /> + </GestureHandlerRootView> + </RootSiblingParent> + </analytics.Provider> + </ThemeProvider> + </UnreadNotifsProvider> + </LoggedOutViewProvider> + </React.Fragment> ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index fa0b225c0..e939dda6d 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -22,6 +22,7 @@ import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as PrefsStateProvider} from 'state/preferences' +import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' import I18nProvider from './locale/i18nProvider' import { Provider as SessionProvider, @@ -34,7 +35,7 @@ import * as persisted from '#/state/persisted' enableFreeze(true) function InnerApp() { - const {isInitialLoad} = useSession() + const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() const colorMode = useColorMode() @@ -57,19 +58,25 @@ function InnerApp() { */ return ( - <UnreadNotifsProvider> - <ThemeProvider theme={colorMode}> - <analytics.Provider> - {/* All components should be within this provider */} - <RootSiblingParent> - <SafeAreaProvider> - <Shell /> - </SafeAreaProvider> - </RootSiblingParent> - <ToastContainer /> - </analytics.Provider> - </ThemeProvider> - </UnreadNotifsProvider> + <React.Fragment + // Resets the entire tree below when it changes: + key={currentAccount?.did}> + <LoggedOutViewProvider> + <UnreadNotifsProvider> + <ThemeProvider theme={colorMode}> + <analytics.Provider> + {/* All components should be within this provider */} + <RootSiblingParent> + <SafeAreaProvider> + <Shell /> + </SafeAreaProvider> + </RootSiblingParent> + <ToastContainer /> + </analytics.Provider> + </ThemeProvider> + </UnreadNotifsProvider> + </LoggedOutViewProvider> + </React.Fragment> ) } diff --git a/src/Navigation.tsx b/src/Navigation.tsx index fb88dc84f..4718349b5 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -9,7 +9,6 @@ import { DefaultTheme, DarkTheme, } from '@react-navigation/native' -import {createNativeStackNavigator} from '@react-navigation/native-stack' import { BottomTabBarProps, createBottomTabNavigator, @@ -69,16 +68,18 @@ import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' import {SavedFeeds} from 'view/screens/SavedFeeds' import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed' import {PreferencesThreads} from 'view/screens/PreferencesThreads' +import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() -const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>() -const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>() -const FeedsTab = createNativeStackNavigator<FeedsTabNavigatorParams>() +const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>() +const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>() +const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>() const NotificationsTab = - createNativeStackNavigator<NotificationsTabNavigatorParams>() -const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>() -const Flat = createNativeStackNavigator<FlatNavigatorParams>() + createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>() +const MyProfileTab = + createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>() +const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>() const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() /** @@ -97,37 +98,37 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { <Stack.Screen name="Lists" component={ListsScreen} - options={{title: title('Lists')}} + options={{title: title('Lists'), requireAuth: true}} /> <Stack.Screen name="Moderation" getComponent={() => ModerationScreen} - options={{title: title('Moderation')}} + options={{title: title('Moderation'), requireAuth: true}} /> <Stack.Screen name="ModerationModlists" getComponent={() => ModerationModlistsScreen} - options={{title: title('Moderation Lists')}} + options={{title: title('Moderation Lists'), requireAuth: true}} /> <Stack.Screen name="ModerationMutedAccounts" getComponent={() => ModerationMutedAccounts} - options={{title: title('Muted Accounts')}} + options={{title: title('Muted Accounts'), requireAuth: true}} /> <Stack.Screen name="ModerationBlockedAccounts" getComponent={() => ModerationBlockedAccounts} - options={{title: title('Blocked Accounts')}} + options={{title: title('Blocked Accounts'), requireAuth: true}} /> <Stack.Screen name="Settings" getComponent={() => SettingsScreen} - options={{title: title('Settings')}} + options={{title: title('Settings'), requireAuth: true}} /> <Stack.Screen name="LanguageSettings" getComponent={() => LanguageSettingsScreen} - options={{title: title('Language Settings')}} + options={{title: title('Language Settings'), requireAuth: true}} /> <Stack.Screen name="Profile" @@ -154,7 +155,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { <Stack.Screen name="ProfileList" getComponent={() => ProfileListScreen} - options={{title: title('List')}} + options={{title: title('List'), requireAuth: true}} /> <Stack.Screen name="PostThread" @@ -184,12 +185,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { <Stack.Screen name="Debug" getComponent={() => DebugScreen} - options={{title: title('Debug')}} + options={{title: title('Debug'), requireAuth: true}} /> <Stack.Screen name="Log" getComponent={() => LogScreen} - options={{title: title('Log')}} + options={{title: title('Log'), requireAuth: true}} /> <Stack.Screen name="Support" @@ -219,22 +220,22 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { <Stack.Screen name="AppPasswords" getComponent={() => AppPasswords} - options={{title: title('App Passwords')}} + options={{title: title('App Passwords'), requireAuth: true}} /> <Stack.Screen name="SavedFeeds" getComponent={() => SavedFeeds} - options={{title: title('Edit My Feeds')}} + options={{title: title('Edit My Feeds'), requireAuth: true}} /> <Stack.Screen name="PreferencesHomeFeed" getComponent={() => PreferencesHomeFeed} - options={{title: title('Home Feed Preferences')}} + options={{title: title('Home Feed Preferences'), requireAuth: true}} /> <Stack.Screen name="PreferencesThreads" getComponent={() => PreferencesThreads} - options={{title: title('Threads Preferences')}} + options={{title: title('Threads Preferences'), requireAuth: true}} /> </> ) @@ -339,6 +340,7 @@ function NotificationsTabNavigator() { <NotificationsTab.Screen name="Notifications" getComponent={() => NotificationsScreen} + options={{requireAuth: true}} /> {commonScreens(NotificationsTab as typeof HomeTab)} </NotificationsTab.Navigator> @@ -357,8 +359,8 @@ function MyProfileTabNavigator() { contentStyle, }}> <MyProfileTab.Screen - name="MyProfile" // @ts-ignore // TODO: fix this broken type in ProfileScreen + name="MyProfile" getComponent={() => ProfileScreen} initialParams={{ name: 'me', @@ -405,7 +407,7 @@ const FlatNavigator = () => { <Flat.Screen name="Notifications" getComponent={() => NotificationsScreen} - options={{title: title('Notifications')}} + options={{title: title('Notifications'), requireAuth: true}} /> {commonScreens(Flat as typeof HomeTab, numUnread)} </Flat.Navigator> diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx index 897a66020..53f05055c 100644 --- a/src/state/shell/index.tsx +++ b/src/state/shell/index.tsx @@ -7,7 +7,6 @@ import {Provider as ColorModeProvider} from './color-mode' import {Provider as OnboardingProvider} from './onboarding' import {Provider as ComposerProvider} from './composer' import {Provider as TickEveryMinuteProvider} from './tick-every-minute' -import {Provider as LoggedOutViewProvider} from './logged-out' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export { @@ -23,23 +22,19 @@ export {useTickEveryMinute} from './tick-every-minute' export function Provider({children}: React.PropsWithChildren<{}>) { return ( <ShellLayoutProvder> - <LoggedOutViewProvider> - <DrawerOpenProvider> - <DrawerSwipableProvider> - <MinimalModeProvider> - <ColorModeProvider> - <OnboardingProvider> - <ComposerProvider> - <TickEveryMinuteProvider> - {children} - </TickEveryMinuteProvider> - </ComposerProvider> - </OnboardingProvider> - </ColorModeProvider> - </MinimalModeProvider> - </DrawerSwipableProvider> - </DrawerOpenProvider> - </LoggedOutViewProvider> + <DrawerOpenProvider> + <DrawerSwipableProvider> + <MinimalModeProvider> + <ColorModeProvider> + <OnboardingProvider> + <ComposerProvider> + <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider> + </ComposerProvider> + </OnboardingProvider> + </ColorModeProvider> + </MinimalModeProvider> + </DrawerSwipableProvider> + </DrawerOpenProvider> </ShellLayoutProvder> ) } diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx deleted file mode 100644 index 7a9138545..000000000 --- a/src/view/com/auth/withAuthRequired.tsx +++ /dev/null @@ -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', - }, -}) diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index bc77a48cd..154035f22 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -12,7 +12,6 @@ import {Button} from '../com/util/forms/Button' import * as Toast from '../com/util/Toast' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from 'lib/routes/types' import {useAnalytics} from 'lib/analytics/analytics' @@ -32,125 +31,111 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen' import {cleanError} from '#/lib/strings/errors' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> -export const AppPasswords = withAuthRequired( - function AppPasswordsImpl({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {screen} = useAnalytics() - const {isTabletOrDesktop} = useWebMediaQueries() - const {openModal} = useModalControls() - const {data: appPasswords, error} = useAppPasswordsQuery() +export function AppPasswords({}: Props) { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const {screen} = useAnalytics() + const {isTabletOrDesktop} = useWebMediaQueries() + const {openModal} = useModalControls() + const {data: appPasswords, error} = useAppPasswordsQuery() - useFocusEffect( - React.useCallback(() => { - screen('AppPasswords') - setMinimalShellMode(false) - }, [screen, setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + screen('AppPasswords') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) - const onAdd = React.useCallback(async () => { - openModal({name: 'add-app-password'}) - }, [openModal]) + const onAdd = React.useCallback(async () => { + openModal({name: 'add-app-password'}) + }, [openModal]) - if (error) { - return ( - <CenteredView + 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> + ) + } + + // no app passwords (empty) state + if (appPasswords?.length === 0) { + return ( + <CenteredView + style={[ + styles.container, + 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={[ - 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)} + styles.btnContainer, + isTabletOrDesktop && styles.btnContainerDesktop, + ]}> + <Button + testID="appPasswordBtn" + type="primary" + label="Add App Password" + style={styles.btn} + labelStyle={styles.btnLabel} + onPress={onAdd} /> - </CenteredView> - ) - } + </View> + </CenteredView> + ) + } - // no app passwords (empty) state - if (appPasswords?.length === 0) { - return ( - <CenteredView + if (appPasswords?.length) { + // has app passwords + return ( + <CenteredView + style={[ + styles.container, + isTabletOrDesktop && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="appPasswordsScreen"> + <AppPasswordsHeader /> + <ScrollView style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, + styles.scrollContainer, 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={[ - styles.btnContainer, - isTabletOrDesktop && styles.btnContainerDesktop, - ]}> - <Button - testID="appPasswordBtn" - type="primary" - label="Add App Password" - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} + !isTabletOrDesktop && styles.flex1, + ]}> + {appPasswords.map((password, i) => ( + <AppPassword + key={password.name} + testID={`appPassword-${i}`} + name={password.name} + createdAt={password.createdAt} /> - </View> - </CenteredView> - ) - } - - 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}> + ))} + {isTabletOrDesktop && ( + <View style={[styles.btnContainer, styles.btnContainerDesktop]}> <Button testID="appPasswordBtn" type="primary" @@ -161,24 +146,36 @@ export const AppPasswords = withAuthRequired( /> </View> )} - </CenteredView> - ) - } - - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <ActivityIndicator /> + </ScrollView> + {!isTabletOrDesktop && ( + <View style={styles.btnContainer}> + <Button + testID="appPasswordBtn" + type="primary" + label="Add App Password" + style={styles.btn} + labelStyle={styles.btnLabel} + onPress={onAdd} + /> + </View> + )} </CenteredView> ) - }, -) + } + + return ( + <CenteredView + style={[ + styles.container, + isTabletOrDesktop && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="appPasswordsScreen"> + <ActivityIndicator /> + </CenteredView> + ) +} function AppPasswordsHeader() { const {isTabletOrDesktop} = useWebMediaQueries() diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index f8fc0db17..ced8592c5 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -2,7 +2,6 @@ import React from 'react' import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native' import {FontAwesomeIcon} 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 {FAB} from 'view/com/util/fab/FAB' import {Link} from 'view/com/util/Link' @@ -88,437 +87,432 @@ type FlatlistSlice = key: string } -export const FeedsScreen = withAuthRequired( - function FeedsScreenImpl(_props: Props) { - const pal = usePalette('default') - const {openComposer} = useComposerControls() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() - const [query, setQuery] = React.useState('') - const [isPTR, setIsPTR] = React.useState(false) - const { - data: preferences, - isLoading: isPreferencesLoading, - error: preferencesError, - } = usePreferencesQuery() - const { - data: popularFeeds, - isFetching: isPopularFeedsFetching, - error: popularFeedsError, - refetch: refetchPopularFeeds, - fetchNextPage: fetchNextPopularFeedsPage, - isFetchingNextPage: isPopularFeedsFetchingNextPage, - hasNextPage: hasNextPopularFeedsPage, - } = useGetPopularFeedsQuery() - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const { - data: searchResults, - mutate: search, - reset: resetSearch, - isPending: isSearchPending, - error: searchError, - } = useSearchPopularFeedsMutation() - const {hasSession} = useSession() +export function FeedsScreen(_props: Props) { + const pal = usePalette('default') + const {openComposer} = useComposerControls() + const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const [query, setQuery] = React.useState('') + const [isPTR, setIsPTR] = React.useState(false) + const { + data: preferences, + isLoading: isPreferencesLoading, + error: preferencesError, + } = usePreferencesQuery() + const { + data: popularFeeds, + isFetching: isPopularFeedsFetching, + error: popularFeedsError, + refetch: refetchPopularFeeds, + fetchNextPage: fetchNextPopularFeedsPage, + isFetchingNextPage: isPopularFeedsFetchingNextPage, + hasNextPage: hasNextPopularFeedsPage, + } = useGetPopularFeedsQuery() + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const { + data: searchResults, + mutate: search, + reset: resetSearch, + isPending: isSearchPending, + error: searchError, + } = useSearchPopularFeedsMutation() + const {hasSession} = useSession() - /** - * A search query is present. We may not have search results yet. - */ - const isUserSearching = query.length > 1 - const debouncedSearch = React.useMemo( - () => debounce(q => search(q), 500), // debounce for 500ms - [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], + /** + * A search query is present. We may not have search results yet. + */ + const isUserSearching = query.length > 1 + const debouncedSearch = React.useMemo( + () => debounce(q => search(q), 500), // debounce for 500ms + [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 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, - ]) + return + fetchNextPopularFeedsPage() + }, [ + isPopularFeedsFetching, + isUserSearching, + popularFeedsError, + hasNextPopularFeedsPage, + fetchNextPopularFeedsPage, + ]) - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - const items = React.useMemo(() => { - let slices: FlatlistSlice[] = [] + const items = React.useMemo(() => { + let slices: FlatlistSlice[] = [] - if (hasSession) { + if (hasSession) { + slices.push({ + key: 'savedFeedsHeader', + type: 'savedFeedsHeader', + }) + + if (preferencesError) { slices.push({ - key: 'savedFeedsHeader', - type: 'savedFeedsHeader', + key: 'savedFeedsError', + type: 'error', + error: cleanError(preferencesError.toString()), }) - - if (preferencesError) { + } else { + if (isPreferencesLoading || !preferences?.feeds?.saved) { slices.push({ - key: 'savedFeedsError', - type: 'error', - error: cleanError(preferencesError.toString()), + key: 'savedFeedsLoading', + type: 'savedFeedsLoading', + // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, }) } else { - if (isPreferencesLoading || !preferences?.feeds?.saved) { + if (preferences?.feeds?.saved.length === 0) { slices.push({ - key: 'savedFeedsLoading', - type: 'savedFeedsLoading', - // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, + key: 'savedFeedNoResults', + type: 'savedFeedNoResults', }) } else { - if (preferences?.feeds?.saved.length === 0) { - slices.push({ - key: 'savedFeedNoResults', - type: 'savedFeedNoResults', - }) - } else { - const {saved, pinned} = preferences.feeds + const {saved, pinned} = preferences.feeds - slices = slices.concat( - pinned.map(uri => ({ + slices = slices.concat( + pinned.map(uri => ({ + key: `savedFeed:${uri}`, + type: 'savedFeed', + feedUri: uri, + })), + ) + + slices = slices.concat( + saved + .filter(uri => !pinned.includes(uri)) + .map(uri => ({ key: `savedFeed:${uri}`, type: 'savedFeed', feedUri: uri, })), - ) - - slices = slices.concat( - saved - .filter(uri => !pinned.includes(uri)) - .map(uri => ({ - key: `savedFeed:${uri}`, - type: 'savedFeed', - feedUri: uri, - })), - ) - } + ) } } } + } + + slices.push({ + key: 'popularFeedsHeader', + type: 'popularFeedsHeader', + }) + if (popularFeedsError || searchError) { slices.push({ - key: 'popularFeedsHeader', - type: 'popularFeedsHeader', + key: 'popularFeedsError', + type: 'error', + error: cleanError( + popularFeedsError?.toString() ?? searchError?.toString() ?? '', + ), }) - - if (popularFeedsError || searchError) { - slices.push({ - key: 'popularFeedsError', - type: 'error', - error: cleanError( - popularFeedsError?.toString() ?? searchError?.toString() ?? '', - ), - }) - } else { - if (isUserSearching) { - if (isSearchPending || !searchResults) { + } else { + if (isUserSearching) { + if (isSearchPending || !searchResults) { + slices.push({ + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', + }) + } else { + if (!searchResults || searchResults?.length === 0) { slices.push({ - key: 'popularFeedsLoading', - type: 'popularFeedsLoading', + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', }) } 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, - })), - ) - } + 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 (isPopularFeedsFetching && !popularFeeds?.pages) { + if ( + !popularFeeds?.pages || + popularFeeds?.pages[0]?.feeds?.length === 0 + ) { slices.push({ - key: 'popularFeedsLoading', - type: 'popularFeedsLoading', + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', }) } else { - if ( - !popularFeeds?.pages || - popularFeeds?.pages[0]?.feeds?.length === 0 - ) { + 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: 'popularFeedsNoResults', - type: 'popularFeedsNoResults', + key: 'popularFeedsLoadingMore', + type: 'popularFeedsLoadingMore', }) - } 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 - }, [ - hasSession, - preferences, - isPreferencesLoading, - preferencesError, - popularFeeds, - isPopularFeedsFetching, - popularFeedsError, - isPopularFeedsFetchingNextPage, - searchResults, - isSearchPending, - searchError, - isUserSearching, - ]) + return slices + }, [ + hasSession, + preferences, + isPreferencesLoading, + preferencesError, + popularFeeds, + isPopularFeedsFetching, + popularFeedsError, + isPopularFeedsFetchingNextPage, + searchResults, + isSearchPending, + searchError, + 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 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') { + 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={{ - paddingHorizontal: 16, - paddingTop: 10, - }}> - <Text type="lg" style={pal.textLight}> - <Trans>You don't have any saved feeds!</Trans> + 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> ) - } 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 ( + } + 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={{ - paddingHorizontal: 16, - paddingTop: 10, - paddingBottom: '150%', - }}> - <Text type="lg" style={pal.textLight}> - <Trans>No results found for "{query}"</Trans> + 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> - ) - } - 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 + {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 /> - )} - - {preferences ? <View /> : <ActivityIndicator />} + ) + } 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, + ], + ) - <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 + return ( + <View style={[pal.view, styles.container]}> + {isMobile && ( + <ViewHeader + title={_(msg`Feeds`)} + canGoBack={false} + renderButton={renderHeaderBtn} + showBorder /> + )} - {hasSession && ( - <FAB - testID="composeFAB" - onPress={onPressCompose} - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" + {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} /> - )} - </View> - ) - }, - {isPublic: true}, -) + } + 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}) { const pal = usePalette('default') diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 8df945cd2..015c436f4 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -3,7 +3,6 @@ import {View, ActivityIndicator, StyleSheet} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' @@ -17,29 +16,24 @@ import {emitSoftReset} from '#/state/events' import {useSession} from '#/state/session' type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> -export const HomeScreen = withAuthRequired( - function HomeScreenImpl(props: Props) { - const {hasSession} = useSession() - const {data: preferences} = usePreferencesQuery() - - if (!hasSession) { - return <HomeScreenPublic /> - } - - if (preferences) { - return <HomeScreenReady {...props} preferences={preferences} /> - } else { - return ( - <View style={styles.loading}> - <ActivityIndicator size="large" /> - </View> - ) - } - }, - { - isPublic: true, - }, -) +export function HomeScreen(props: Props) { + const {hasSession} = useSession() + const {data: preferences} = usePreferencesQuery() + + if (!hasSession) { + return <HomeScreenPublic /> + } + + if (preferences) { + return <HomeScreenReady {...props} preferences={preferences} /> + } else { + return ( + <View style={styles.loading}> + <ActivityIndicator size="large" /> + </View> + ) + } +} function HomeScreenPublic() { const setMinimalShellMode = useSetMinimalShellMode() diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index c97be4a02..d28db7c6c 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -4,7 +4,6 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {AtUri} from '@atproto/api' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {MyLists} from '#/view/com/lists/MyLists' import {Text} from 'view/com/util/text/Text' import {Button} from 'view/com/util/forms/Button' @@ -18,70 +17,68 @@ import {useModalControls} from '#/state/modals' import {Trans} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> -export const ListsScreen = withAuthRequired( - function ListsScreenImpl({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile} = useWebMediaQueries() - const navigation = useNavigation<NavigationProp>() - const {openModal} = useModalControls() +export function ListsScreen({}: Props) { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const {isMobile} = useWebMediaQueries() + const navigation = useNavigation<NavigationProp>() + const {openModal} = useModalControls() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - const onPressNewList = React.useCallback(() => { - openModal({ - name: 'create-or-edit-list', - purpose: 'app.bsky.graph.defs#curatelist', - onSave: (uri: string) => { - try { - const urip = new AtUri(uri) - navigation.navigate('ProfileList', { - name: urip.hostname, - rkey: urip.rkey, - }) - } catch {} - }, - }) - }, [openModal, navigation]) + const onPressNewList = React.useCallback(() => { + openModal({ + name: 'create-or-edit-list', + purpose: 'app.bsky.graph.defs#curatelist', + onSave: (uri: string) => { + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} + }, + }) + }, [openModal, navigation]) - return ( - <View style={s.hContentRegion} testID="listsScreen"> - <SimpleViewHeader - showBackButton={isMobile} - style={ - !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] - }> - <View style={{flex: 1}}> - <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> - <Trans>User Lists</Trans> + return ( + <View style={s.hContentRegion} testID="listsScreen"> + <SimpleViewHeader + showBackButton={isMobile} + style={ + !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] + }> + <View style={{flex: 1}}> + <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> + <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 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> - </Button> - </View> - </SimpleViewHeader> - <MyLists filter="curate" style={s.flexGrow1} /> - </View> - ) - }, -) + </Button> + </View> + </SimpleViewHeader> + <MyLists filter="curate" style={s.flexGrow1} /> + </View> + ) +} diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx index 10b72fe9e..4d8d8cad7 100644 --- a/src/view/screens/Moderation.tsx +++ b/src/view/screens/Moderation.tsx @@ -6,7 +6,6 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {s} from 'lib/styles' import {CenteredView} from '../com/util/Views' import {ViewHeader} from '../com/util/ViewHeader' @@ -21,100 +20,98 @@ import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> -export const ModerationScreen = withAuthRequired( - function Moderation({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {screen, track} = useAnalytics() - const {isTabletOrDesktop} = useWebMediaQueries() - const {openModal} = useModalControls() +export function ModerationScreen({}: Props) { + const pal = usePalette('default') + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const {screen, track} = useAnalytics() + const {isTabletOrDesktop} = useWebMediaQueries() + const {openModal} = useModalControls() - useFocusEffect( - React.useCallback(() => { - screen('Moderation') - setMinimalShellMode(false) - }, [screen, setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + screen('Moderation') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) - const onPressContentFiltering = React.useCallback(() => { - track('Moderation:ContentfilteringButtonClicked') - openModal({name: 'content-filtering-settings'}) - }, [track, openModal]) + const onPressContentFiltering = React.useCallback(() => { + track('Moderation:ContentfilteringButtonClicked') + openModal({name: 'content-filtering-settings'}) + }, [track, openModal]) - return ( - <CenteredView - style={[ - s.hContentRegion, - pal.border, - isTabletOrDesktop ? styles.desktopContainer : pal.viewLight, - ]} - testID="moderationScreen"> - <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> - <View style={styles.spacer} /> - <TouchableOpacity - testID="contentFilteringBtn" - style={[styles.linkCard, pal.view]} - onPress={onPressContentFiltering} - accessibilityRole="tab" - accessibilityHint="Content filtering" - accessibilityLabel=""> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="eye" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Content filtering</Trans> - </Text> - </TouchableOpacity> - <Link - testID="moderationlistsBtn" - style={[styles.linkCard, pal.view]} - href="/moderation/modlists"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="users-slash" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Moderation lists</Trans> - </Text> - </Link> - <Link - testID="mutedAccountsBtn" - style={[styles.linkCard, pal.view]} - href="/moderation/muted-accounts"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="user-slash" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Muted accounts</Trans> - </Text> - </Link> - <Link - testID="blockedAccountsBtn" - style={[styles.linkCard, pal.view]} - href="/moderation/blocked-accounts"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="ban" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Blocked accounts</Trans> - </Text> - </Link> - </CenteredView> - ) - }, -) + return ( + <CenteredView + style={[ + s.hContentRegion, + pal.border, + isTabletOrDesktop ? styles.desktopContainer : pal.viewLight, + ]} + testID="moderationScreen"> + <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> + <View style={styles.spacer} /> + <TouchableOpacity + testID="contentFilteringBtn" + style={[styles.linkCard, pal.view]} + onPress={onPressContentFiltering} + accessibilityRole="tab" + accessibilityHint="Content filtering" + accessibilityLabel=""> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="eye" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Content filtering</Trans> + </Text> + </TouchableOpacity> + <Link + testID="moderationlistsBtn" + style={[styles.linkCard, pal.view]} + href="/moderation/modlists"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="users-slash" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Moderation lists</Trans> + </Text> + </Link> + <Link + testID="mutedAccountsBtn" + style={[styles.linkCard, pal.view]} + href="/moderation/muted-accounts"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="user-slash" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Muted accounts</Trans> + </Text> + </Link> + <Link + testID="blockedAccountsBtn" + style={[styles.linkCard, pal.view]} + href="/moderation/blocked-accounts"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="ban" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Blocked accounts</Trans> + </Text> + </Link> + </CenteredView> + ) +} const styles = StyleSheet.create({ desktopContainer: { diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index c03275f5d..8f6e2f729 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -10,7 +10,6 @@ import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {Text} from '../com/util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from 'lib/routes/types' import {useAnalytics} from 'lib/analytics/analytics' @@ -30,146 +29,144 @@ type Props = NativeStackScreenProps< CommonNavigatorParams, 'ModerationBlockedAccounts' > -export const ModerationBlockedAccounts = withAuthRequired( - function ModerationBlockedAccountsImpl({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrDesktop} = useWebMediaQueries() - const {screen} = useAnalytics() - const [isPTRing, setIsPTRing] = React.useState(false) - const { - data, - isFetching, - isError, - error, - refetch, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - } = useMyBlockedAccountsQuery() - const isEmpty = !isFetching && !data?.pages[0]?.blocks.length - const profiles = React.useMemo(() => { - if (data?.pages) { - return data.pages.flatMap(page => page.blocks) - } - return [] - }, [data]) +export function ModerationBlockedAccounts({}: Props) { + const pal = usePalette('default') + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const {isTabletOrDesktop} = useWebMediaQueries() + const {screen} = useAnalytics() + const [isPTRing, setIsPTRing] = React.useState(false) + const { + data, + isFetching, + isError, + error, + refetch, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = useMyBlockedAccountsQuery() + const isEmpty = !isFetching && !data?.pages[0]?.blocks.length + const profiles = React.useMemo(() => { + if (data?.pages) { + return data.pages.flatMap(page => page.blocks) + } + return [] + }, [data]) - useFocusEffect( - React.useCallback(() => { - screen('BlockedAccounts') - setMinimalShellMode(false) - }, [screen, setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + screen('BlockedAccounts') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) - const onRefresh = React.useCallback(async () => { - setIsPTRing(true) - try { - await refetch() - } catch (err) { - logger.error('Failed to refresh my muted accounts', {error: err}) - } - setIsPTRing(false) - }, [refetch, setIsPTRing]) + const onRefresh = React.useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh my muted accounts', {error: err}) + } + setIsPTRing(false) + }, [refetch, setIsPTRing]) - const onEndReached = React.useCallback(async () => { - if (isFetching || !hasNextPage || isError) return + const onEndReached = React.useCallback(async () => { + if (isFetching || !hasNextPage || isError) return - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more of my muted accounts', {error: err}) - } - }, [isFetching, hasNextPage, isError, fetchNextPage]) + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more of my muted accounts', {error: err}) + } + }, [isFetching, hasNextPage, isError, fetchNextPage]) - const renderItem = ({ - item, - index, - }: { - item: ActorDefs.ProfileView - index: number - }) => ( - <ProfileCard - testID={`blockedAccount-${index}`} - key={item.did} - profile={item} - /> - ) - return ( - <CenteredView + const renderItem = ({ + item, + index, + }: { + item: ActorDefs.ProfileView + index: number + }) => ( + <ProfileCard + testID={`blockedAccount-${index}`} + key={item.did} + profile={item} + /> + ) + return ( + <CenteredView + style={[ + styles.container, + isTabletOrDesktop && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="blockedAccountsScreen"> + <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> + <Text + type="sm" style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="blockedAccountsScreen"> - <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> - <Text - type="sm" - style={[ - styles.description, - pal.text, - isTabletOrDesktop && styles.descriptionDesktop, - ]}> - <Trans> - Blocked accounts cannot reply in your threads, mention you, or - otherwise interact with you. You will not see their content and they - will be prevented from seeing yours. - </Trans> - </Text> - {isEmpty ? ( - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> - {isError ? ( - <ErrorScreen - title="Oops!" - message={cleanError(error)} - onPressTryAgain={refetch} - /> - ) : ( - <View style={[styles.empty, pal.viewLight]}> - <Text type="lg" style={[pal.text, styles.emptyText]}> - <Trans> - You have not blocked any accounts yet. To block an account, - go to their profile and selected "Block account" from the - menu on their account. - </Trans> - </Text> - </View> - )} - </View> - ) : ( - <FlatList - style={[!isTabletOrDesktop && styles.flex1]} - data={profiles} - keyExtractor={(item: ActorDefs.ProfileView) => item.did} - refreshControl={ - <RefreshControl - refreshing={isPTRing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - onEndReached={onEndReached} - renderItem={renderItem} - initialNumToRender={15} - // FIXME(dan) + styles.description, + pal.text, + isTabletOrDesktop && styles.descriptionDesktop, + ]}> + <Trans> + Blocked accounts cannot reply in your threads, mention you, or + otherwise interact with you. You will not see their content and they + will be prevented from seeing yours. + </Trans> + </Text> + {isEmpty ? ( + <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> + {isError ? ( + <ErrorScreen + title="Oops!" + message={cleanError(error)} + onPressTryAgain={refetch} + /> + ) : ( + <View style={[styles.empty, pal.viewLight]}> + <Text type="lg" style={[pal.text, styles.emptyText]}> + <Trans> + You have not blocked any accounts yet. To block an account, go + to their profile and selected "Block account" from the menu on + their account. + </Trans> + </Text> + </View> + )} + </View> + ) : ( + <FlatList + style={[!isTabletOrDesktop && styles.flex1]} + data={profiles} + keyExtractor={(item: ActorDefs.ProfileView) => item.did} + refreshControl={ + <RefreshControl + refreshing={isPTRing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + // FIXME(dan) - ListFooterComponent={() => ( - <View style={styles.footer}> - {(isFetching || isFetchingNextPage) && <ActivityIndicator />} - </View> - )} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> - )} - </CenteredView> - ) - }, -) + ListFooterComponent={() => ( + <View style={styles.footer}> + {(isFetching || isFetchingNextPage) && <ActivityIndicator />} + </View> + )} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + )} + </CenteredView> + ) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx index be0eb3850..145b35a42 100644 --- a/src/view/screens/ModerationModlists.tsx +++ b/src/view/screens/ModerationModlists.tsx @@ -4,7 +4,6 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {AtUri} from '@atproto/api' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {MyLists} from '#/view/com/lists/MyLists' import {Text} from 'view/com/util/text/Text' import {Button} from 'view/com/util/forms/Button' @@ -17,70 +16,68 @@ import {useSetMinimalShellMode} from '#/state/shell' import {useModalControls} from '#/state/modals' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> -export const ModerationModlistsScreen = withAuthRequired( - function ModerationModlistsScreenImpl({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile} = useWebMediaQueries() - const navigation = useNavigation<NavigationProp>() - const {openModal} = useModalControls() +export function ModerationModlistsScreen({}: Props) { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const {isMobile} = useWebMediaQueries() + const navigation = useNavigation<NavigationProp>() + const {openModal} = useModalControls() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - const onPressNewList = React.useCallback(() => { - openModal({ - name: 'create-or-edit-list', - purpose: 'app.bsky.graph.defs#modlist', - onSave: (uri: string) => { - try { - const urip = new AtUri(uri) - navigation.navigate('ProfileList', { - name: urip.hostname, - rkey: urip.rkey, - }) - } catch {} - }, - }) - }, [openModal, navigation]) + const onPressNewList = React.useCallback(() => { + openModal({ + name: 'create-or-edit-list', + purpose: 'app.bsky.graph.defs#modlist', + onSave: (uri: string) => { + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} + }, + }) + }, [openModal, navigation]) - return ( - <View style={s.hContentRegion} testID="moderationModlistsScreen"> - <SimpleViewHeader - showBackButton={isMobile} - style={ - !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] - }> - <View style={{flex: 1}}> - <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> - Moderation Lists + return ( + <View style={s.hContentRegion} testID="moderationModlistsScreen"> + <SimpleViewHeader + showBackButton={isMobile} + style={ + !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] + }> + <View style={{flex: 1}}> + <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> + 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 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> - </Button> - </View> - </SimpleViewHeader> - <MyLists filter="mod" style={s.flexGrow1} /> - </View> - ) - }, -) + </Button> + </View> + </SimpleViewHeader> + <MyLists filter="mod" style={s.flexGrow1} /> + </View> + ) +} diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index c0ff17eb4..41aee9f2f 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -10,7 +10,6 @@ import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {Text} from '../com/util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from 'lib/routes/types' import {useAnalytics} from 'lib/analytics/analytics' @@ -30,145 +29,143 @@ type Props = NativeStackScreenProps< CommonNavigatorParams, 'ModerationMutedAccounts' > -export const ModerationMutedAccounts = withAuthRequired( - function ModerationMutedAccountsImpl({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrDesktop} = useWebMediaQueries() - const {screen} = useAnalytics() - const [isPTRing, setIsPTRing] = React.useState(false) - const { - data, - isFetching, - isError, - error, - refetch, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - } = useMyMutedAccountsQuery() - const isEmpty = !isFetching && !data?.pages[0]?.mutes.length - const profiles = React.useMemo(() => { - if (data?.pages) { - return data.pages.flatMap(page => page.mutes) - } - return [] - }, [data]) +export function ModerationMutedAccounts({}: Props) { + const pal = usePalette('default') + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const {isTabletOrDesktop} = useWebMediaQueries() + const {screen} = useAnalytics() + const [isPTRing, setIsPTRing] = React.useState(false) + const { + data, + isFetching, + isError, + error, + refetch, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = useMyMutedAccountsQuery() + const isEmpty = !isFetching && !data?.pages[0]?.mutes.length + const profiles = React.useMemo(() => { + if (data?.pages) { + return data.pages.flatMap(page => page.mutes) + } + return [] + }, [data]) - useFocusEffect( - React.useCallback(() => { - screen('MutedAccounts') - setMinimalShellMode(false) - }, [screen, setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + screen('MutedAccounts') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) - const onRefresh = React.useCallback(async () => { - setIsPTRing(true) - try { - await refetch() - } catch (err) { - logger.error('Failed to refresh my muted accounts', {error: err}) - } - setIsPTRing(false) - }, [refetch, setIsPTRing]) + const onRefresh = React.useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh my muted accounts', {error: err}) + } + setIsPTRing(false) + }, [refetch, setIsPTRing]) - const onEndReached = React.useCallback(async () => { - if (isFetching || !hasNextPage || isError) return + const onEndReached = React.useCallback(async () => { + if (isFetching || !hasNextPage || isError) return - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more of my muted accounts', {error: err}) - } - }, [isFetching, hasNextPage, isError, fetchNextPage]) + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more of my muted accounts', {error: err}) + } + }, [isFetching, hasNextPage, isError, fetchNextPage]) - const renderItem = ({ - item, - index, - }: { - item: ActorDefs.ProfileView - index: number - }) => ( - <ProfileCard - testID={`mutedAccount-${index}`} - key={item.did} - profile={item} - /> - ) - return ( - <CenteredView + const renderItem = ({ + item, + index, + }: { + item: ActorDefs.ProfileView + index: number + }) => ( + <ProfileCard + testID={`mutedAccount-${index}`} + key={item.did} + profile={item} + /> + ) + return ( + <CenteredView + style={[ + styles.container, + isTabletOrDesktop && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="mutedAccountsScreen"> + <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> + <Text + type="sm" style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="mutedAccountsScreen"> - <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> - <Text - type="sm" - style={[ - styles.description, - pal.text, - isTabletOrDesktop && styles.descriptionDesktop, - ]}> - <Trans> - Muted accounts have their posts removed from your feed and from your - notifications. Mutes are completely private. - </Trans> - </Text> - {isEmpty ? ( - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> - {isError ? ( - <ErrorScreen - title="Oops!" - message={cleanError(error)} - onPressTryAgain={refetch} - /> - ) : ( - <View style={[styles.empty, pal.viewLight]}> - <Text type="lg" style={[pal.text, styles.emptyText]}> - <Trans> - You have not muted any accounts yet. To mute an account, go - to their profile and selected "Mute account" from the menu - on their account. - </Trans> - </Text> - </View> - )} - </View> - ) : ( - <FlatList - style={[!isTabletOrDesktop && styles.flex1]} - data={profiles} - keyExtractor={item => item.did} - refreshControl={ - <RefreshControl - refreshing={isPTRing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - onEndReached={onEndReached} - renderItem={renderItem} - initialNumToRender={15} - // FIXME(dan) + styles.description, + pal.text, + isTabletOrDesktop && styles.descriptionDesktop, + ]}> + <Trans> + Muted accounts have their posts removed from your feed and from your + notifications. Mutes are completely private. + </Trans> + </Text> + {isEmpty ? ( + <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> + {isError ? ( + <ErrorScreen + title="Oops!" + message={cleanError(error)} + onPressTryAgain={refetch} + /> + ) : ( + <View style={[styles.empty, pal.viewLight]}> + <Text type="lg" style={[pal.text, styles.emptyText]}> + <Trans> + You have not muted any accounts yet. To mute an account, go to + their profile and selected "Mute account" from the menu on + their account. + </Trans> + </Text> + </View> + )} + </View> + ) : ( + <FlatList + style={[!isTabletOrDesktop && styles.flex1]} + data={profiles} + keyExtractor={item => item.did} + refreshControl={ + <RefreshControl + refreshing={isPTRing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + // FIXME(dan) - ListFooterComponent={() => ( - <View style={styles.footer}> - {(isFetching || isFetchingNextPage) && <ActivityIndicator />} - </View> - )} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> - )} - </CenteredView> - ) - }, -) + ListFooterComponent={() => ( + <View style={styles.footer}> + {(isFetching || isFetchingNextPage) && <ActivityIndicator />} + </View> + )} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + )} + </CenteredView> + ) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 4ed9c7f74..8516d1667 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -6,7 +6,6 @@ import { NativeStackScreenProps, NotificationsTabNavigatorParams, } from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/notifications/Feed' import {TextLink} from 'view/com/util/Link' @@ -28,102 +27,100 @@ type Props = NativeStackScreenProps< NotificationsTabNavigatorParams, 'Notifications' > -export const NotificationsScreen = withAuthRequired( - function NotificationsScreenImpl({}: Props) { - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() - const scrollElRef = React.useRef<FlatList>(null) - const {screen} = useAnalytics() - const pal = usePalette('default') - const {isDesktop} = useWebMediaQueries() - const unreadNotifs = useUnreadNotifications() - const queryClient = useQueryClient() - const hasNew = !!unreadNotifs +export function NotificationsScreen({}: Props) { + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() + const scrollElRef = React.useRef<FlatList>(null) + const {screen} = useAnalytics() + const pal = usePalette('default') + const {isDesktop} = useWebMediaQueries() + const unreadNotifs = useUnreadNotifications() + const queryClient = useQueryClient() + const hasNew = !!unreadNotifs - // event handlers - // = - const scrollToTop = React.useCallback(() => { - scrollElRef.current?.scrollToOffset({offset: 0}) - resetMainScroll() - }, [scrollElRef, resetMainScroll]) + // event handlers + // = + const scrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0}) + resetMainScroll() + }, [scrollElRef, resetMainScroll]) - const onPressLoadLatest = React.useCallback(() => { - scrollToTop() - queryClient.invalidateQueries({ - queryKey: NOTIFS_RQKEY(), - }) - }, [scrollToTop, queryClient]) + const onPressLoadLatest = React.useCallback(() => { + scrollToTop() + queryClient.invalidateQueries({ + queryKey: NOTIFS_RQKEY(), + }) + }, [scrollToTop, queryClient]) - // on-visible setup - // = - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - logger.debug('NotificationsScreen: Updating feed') - screen('Notifications') - return listenSoftReset(onPressLoadLatest) - }, [screen, onPressLoadLatest, setMinimalShellMode]), - ) + // on-visible setup + // = + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + logger.debug('NotificationsScreen: Updating feed') + screen('Notifications') + return listenSoftReset(onPressLoadLatest) + }, [screen, onPressLoadLatest, setMinimalShellMode]), + ) - const ListHeaderComponent = React.useCallback(() => { - if (isDesktop) { - return ( - <View - style={[ - pal.view, - { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 18, - paddingVertical: 12, - }, - ]}> - <TextLink - type="title-lg" - href="/notifications" - style={[pal.text, {fontWeight: 'bold'}]} - text={ - <> - <Trans>Notifications</Trans>{' '} - {hasNew && ( - <View - style={{ - top: -8, - backgroundColor: colors.blue3, - width: 8, - height: 8, - borderRadius: 4, - }} - /> - )} - </> - } - onPress={emitSoftReset} - /> - </View> - ) - } - return <></> - }, [isDesktop, pal, hasNew]) + const ListHeaderComponent = React.useCallback(() => { + if (isDesktop) { + return ( + <View + style={[ + pal.view, + { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 18, + paddingVertical: 12, + }, + ]}> + <TextLink + type="title-lg" + href="/notifications" + style={[pal.text, {fontWeight: 'bold'}]} + text={ + <> + <Trans>Notifications</Trans>{' '} + {hasNew && ( + <View + style={{ + top: -8, + backgroundColor: colors.blue3, + width: 8, + height: 8, + borderRadius: 4, + }} + /> + )} + </> + } + 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} + 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} /> - {(isScrolledDown || hasNew) && ( - <LoadLatestBtn - onPress={onPressLoadLatest} - label={_(msg`Load new notifications`)} - showIndicator={hasNew} - /> - )} - </View> - ) - }, -) + )} + </View> + ) +} diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx index 2209310d0..7cbb81102 100644 --- a/src/view/screens/PostLikedBy.tsx +++ b/src/view/screens/PostLikedBy.tsx @@ -2,7 +2,6 @@ import React from 'react' import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' import {makeRecordUri} from 'lib/strings/url-helpers' @@ -11,25 +10,22 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> -export const PostLikedByScreen = withAuthRequired( - ({route}: Props) => { - const setMinimalShellMode = useSetMinimalShellMode() - const {name, rkey} = route.params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {_} = useLingui() +export const PostLikedByScreen = ({route}: Props) => { + const setMinimalShellMode = useSetMinimalShellMode() + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Liked by`)} /> - <PostLikedByComponent uri={uri} /> - </View> - ) - }, - {isPublic: true}, -) + return ( + <View> + <ViewHeader title={_(msg`Liked by`)} /> + <PostLikedByComponent uri={uri} /> + </View> + ) +} diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx index 5b3b5f8fa..de95f33bf 100644 --- a/src/view/screens/PostRepostedBy.tsx +++ b/src/view/screens/PostRepostedBy.tsx @@ -1,7 +1,6 @@ import React from 'react' import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {ViewHeader} from '../com/util/ViewHeader' import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy' @@ -11,25 +10,22 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> -export const PostRepostedByScreen = withAuthRequired( - ({route}: Props) => { - const {name, rkey} = route.params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() +export const PostRepostedByScreen = ({route}: Props) => { + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Reposted by`)} /> - <PostRepostedByComponent uri={uri} /> - </View> - ) - }, - {isPublic: true}, -) + return ( + <View> + <ViewHeader title={_(msg`Reposted by`)} /> + <PostRepostedByComponent uri={uri} /> + </View> + ) +} diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 11574e283..4b1f51748 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -5,7 +5,6 @@ import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {makeRecordUri} from 'lib/strings/url-helpers' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {ComposePrompt} from 'view/com/composer/Prompt' @@ -27,85 +26,82 @@ import {CenteredView} from '../com/util/Views' import {useComposerControls} from '#/state/shell/composer' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> -export const PostThreadScreen = withAuthRequired( - function PostThreadScreenImpl({route}: Props) { - const queryClient = useQueryClient() - const {_} = useLingui() - const {fabMinimalShellTransform} = useMinimalShellMode() - const setMinimalShellMode = useSetMinimalShellMode() - const {openComposer} = useComposerControls() - const safeAreaInsets = useSafeAreaInsets() - const {name, rkey} = route.params - const {isMobile} = useWebMediaQueries() - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) +export function PostThreadScreen({route}: Props) { + const queryClient = useQueryClient() + const {_} = useLingui() + const {fabMinimalShellTransform} = useMinimalShellMode() + const setMinimalShellMode = useSetMinimalShellMode() + const {openComposer} = useComposerControls() + const safeAreaInsets = useSafeAreaInsets() + const {name, rkey} = route.params + const {isMobile} = useWebMediaQueries() + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - const onPressReply = React.useCallback(() => { - if (!resolvedUri) { - return - } - const thread = queryClient.getQueryData<ThreadNode>( - POST_THREAD_RQKEY(resolvedUri.uri), - ) - if (thread?.type !== 'post') { - return - } - openComposer({ - 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, - }, + const onPressReply = React.useCallback(() => { + if (!resolvedUri) { + return + } + const thread = queryClient.getQueryData<ThreadNode>( + POST_THREAD_RQKEY(resolvedUri.uri), + ) + if (thread?.type !== 'post') { + return + } + openComposer({ + 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({ - queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), - }), - }) - }, [openComposer, queryClient, resolvedUri]) + }, + onPost: () => + queryClient.invalidateQueries({ + queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), + }), + }) + }, [openComposer, queryClient, resolvedUri]) - return ( - <View style={s.hContentRegion}> - {isMobile && <ViewHeader title={_(msg`Post`)} />} - <View style={s.flex1}> - {uriError ? ( - <CenteredView> - <ErrorMessage message={String(uriError)} /> - </CenteredView> - ) : ( - <PostThreadComponent - uri={resolvedUri?.uri} - onPressReply={onPressReply} - /> - )} - </View> - {isMobile && ( - <Animated.View - style={[ - styles.prompt, - fabMinimalShellTransform, - { - bottom: clamp(safeAreaInsets.bottom, 15, 30), - }, - ]}> - <ComposePrompt onPressCompose={onPressReply} /> - </Animated.View> + return ( + <View style={s.hContentRegion}> + {isMobile && <ViewHeader title={_(msg`Post`)} />} + <View style={s.flex1}> + {uriError ? ( + <CenteredView> + <ErrorMessage message={String(uriError)} /> + </CenteredView> + ) : ( + <PostThreadComponent + uri={resolvedUri?.uri} + onPressReply={onPressReply} + /> )} </View> - ) - }, - {isPublic: true}, -) + {isMobile && ( + <Animated.View + style={[ + styles.prompt, + fabMinimalShellTransform, + { + bottom: clamp(safeAreaInsets.bottom, 15, 30), + }, + ]}> + <ComposePrompt onPressCompose={onPressReply} /> + </Animated.View> + )} + </View> + ) +} const styles = StyleSheet.create({ prompt: { diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 48c914054..8282b2105 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -5,7 +5,6 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewSelectorHandle} from '../com/util/ViewSelector' import {CenteredView, FlatList} from '../com/util/Views' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' @@ -43,83 +42,78 @@ interface SectionRef { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> -export const ProfileScreen = withAuthRequired( - function ProfileScreenImpl({route}: Props) { - const {currentAccount} = useSession() - const name = - route.params.name === 'me' ? currentAccount?.did : route.params.name - const moderationOpts = useModerationOpts() - const { - data: resolvedDid, - error: resolveError, - refetch: refetchDid, - isFetching: isFetchingDid, - } = useResolveDidQuery(name) - const { - data: profile, - error: profileError, - refetch: refetchProfile, - isFetching: isFetchingProfile, - } = useProfileQuery({ - did: resolvedDid?.did, - }) +export function ProfileScreen({route}: Props) { + const {currentAccount} = useSession() + const name = + route.params.name === 'me' ? currentAccount?.did : route.params.name + const moderationOpts = useModerationOpts() + const { + data: resolvedDid, + error: resolveError, + refetch: refetchDid, + isFetching: isFetchingDid, + } = useResolveDidQuery(name) + const { + data: profile, + error: profileError, + refetch: refetchProfile, + isFetching: isFetchingProfile, + } = useProfileQuery({ + did: resolvedDid?.did, + }) - const onPressTryAgain = React.useCallback(() => { - if (resolveError) { - refetchDid() - } else { - refetchProfile() - } - }, [resolveError, refetchDid, refetchProfile]) - - if (isFetchingDid || isFetchingProfile || !moderationOpts) { - return ( - <CenteredView> - <ProfileHeader - profile={null} - moderation={null} - isProfilePreview={true} - /> - </CenteredView> - ) + const onPressTryAgain = React.useCallback(() => { + if (resolveError) { + refetchDid() + } else { + refetchProfile() } - 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} + }, [resolveError, refetchDid, refetchProfile]) + + if (isFetchingDid || isFetchingProfile || !moderationOpts) { + return ( + <CenteredView> + <ProfileHeader + profile={null} + moderation={null} + isProfilePreview={true} /> - ) - } - // should never happen + </CenteredView> + ) + } + if (resolveError || profileError) { return ( <CenteredView> <ErrorScreen testID="profileErrorScreen" title="Oops!" - message="Something went wrong and we're not sure what." + message={cleanError(resolveError || profileError)} onPressTryAgain={onPressTryAgain} /> </CenteredView> ) - }, - { - isPublic: true, - }, -) + } + if (profile && moderationOpts) { + 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({ profile: profileUnshadowed, diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 642793eda..da01cfca6 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -16,7 +16,6 @@ import {CommonNavigatorParams} from 'lib/routes/types' import {makeRecordUri} from 'lib/strings/url-helpers' import {colors, s} from 'lib/styles' import {FeedDescriptor} from '#/state/queries/post-feed' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {Feed} from 'view/com/posts/Feed' @@ -69,70 +68,65 @@ interface SectionRef { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> -export const ProfileFeedScreen = withAuthRequired( - function ProfileFeedScreenImpl(props: Props) { - const {rkey, name: handleOrDid} = props.route.params +export function ProfileFeedScreen(props: Props) { + const {rkey, name: handleOrDid} = props.route.params - const pal = usePalette('default') - const {_} = useLingui() - const navigation = useNavigation<NavigationProp>() + const pal = usePalette('default') + const {_} = useLingui() + const navigation = useNavigation<NavigationProp>() - const uri = useMemo( - () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), - [rkey, handleOrDid], - ) - const {error, data: resolvedUri} = useResolveUriQuery(uri) + const uri = useMemo( + () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), + [rkey, handleOrDid], + ) + const {error, data: resolvedUri} = useResolveUriQuery(uri) - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - 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> - ) + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') } + }, [navigation]) - return resolvedUri ? ( - <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} /> - ) : ( + if (error) { + return ( <CenteredView> - <View style={s.p20}> - <ActivityIndicator size="large" /> + <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> ) - }, - { - isPublic: true, - }, -) + } + + return resolvedUri ? ( + <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} /> + ) : ( + <CenteredView> + <View style={s.p20}> + <ActivityIndicator size="large" /> + </View> + </CenteredView> + ) +} function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { const {data: preferences} = usePreferencesQuery() diff --git a/src/view/screens/ProfileFeedLikedBy.tsx b/src/view/screens/ProfileFeedLikedBy.tsx index 6399c8a0b..0460670e1 100644 --- a/src/view/screens/ProfileFeedLikedBy.tsx +++ b/src/view/screens/ProfileFeedLikedBy.tsx @@ -2,7 +2,6 @@ import React from 'react' import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' import {makeRecordUri} from 'lib/strings/url-helpers' @@ -11,25 +10,22 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'> -export const ProfileFeedLikedByScreen = withAuthRequired( - ({route}: Props) => { - const setMinimalShellMode = useSetMinimalShellMode() - const {name, rkey} = route.params - const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) - const {_} = useLingui() +export const ProfileFeedLikedByScreen = ({route}: Props) => { + const setMinimalShellMode = useSetMinimalShellMode() + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Liked by`)} /> - <PostLikedByComponent uri={uri} /> - </View> - ) - }, - {isPublic: true}, -) + return ( + <View> + <ViewHeader title={_(msg`Liked by`)} /> + <PostLikedByComponent uri={uri} /> + </View> + ) +} diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index 71c0e4a9c..2cad08cb5 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -2,7 +2,6 @@ import React from 'react' import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' import {useSetMinimalShellMode} from '#/state/shell' @@ -10,24 +9,21 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> -export const ProfileFollowersScreen = withAuthRequired( - ({route}: Props) => { - const {name} = route.params - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() +export const ProfileFollowersScreen = ({route}: Props) => { + const {name} = route.params + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Followers`)} /> - <ProfileFollowersComponent name={name} /> - </View> - ) - }, - {isPublic: true}, -) + return ( + <View> + <ViewHeader title={_(msg`Followers`)} /> + <ProfileFollowersComponent name={name} /> + </View> + ) +} diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index bb3f2040f..80502b98b 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -2,7 +2,6 @@ import React from 'react' import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {useSetMinimalShellMode} from '#/state/shell' @@ -10,24 +9,21 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> -export const ProfileFollowsScreen = withAuthRequired( - ({route}: Props) => { - const {name} = route.params - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() +export const ProfileFollowsScreen = ({route}: Props) => { + const {name} = route.params + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Following`)} /> - <ProfileFollowsComponent name={name} /> - </View> - ) - }, - {isPublic: true}, -) + return ( + <View> + <ViewHeader title={_(msg`Following`)} /> + <ProfileFollowsComponent name={name} /> + </View> + ) +} diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index a8c55250f..cc6d85e6f 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -12,7 +12,6 @@ import {useNavigation} from '@react-navigation/native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' import {useQueryClient} from '@tanstack/react-query' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {Feed} from 'view/com/posts/Feed' @@ -64,42 +63,40 @@ interface SectionRef { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> -export const ProfileListScreen = withAuthRequired( - function ProfileListScreenImpl(props: Props) { - const {name: handleOrDid, rkey} = props.route.params - const {data: resolvedUri, error: resolveError} = useResolveUriQuery( - AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), - ) - const {data: list, error: listError} = useListQuery(resolvedUri?.uri) - - if (resolveError) { - 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> - ) - } +export function ProfileListScreen(props: Props) { + const {name: handleOrDid, rkey} = props.route.params + const {data: resolvedUri, error: resolveError} = useResolveUriQuery( + AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), + ) + const {data: list, error: listError} = useListQuery(resolvedUri?.uri) - return resolvedUri && list ? ( - <ProfileListScreenLoaded {...props} uri={resolvedUri.uri} list={list} /> - ) : ( + if (resolveError) { + return ( <CenteredView> - <View style={s.p20}> - <ActivityIndicator size="large" /> - </View> + <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> + <View style={s.p20}> + <ActivityIndicator size="large" /> + </View> + </CenteredView> + ) +} function ProfileListScreenLoaded({ route, diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 4c13a2be1..640d76a5c 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -14,7 +14,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {CommonNavigatorParams} from 'lib/routes/types' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from 'view/com/util/ViewHeader' import {ScrollView, CenteredView} from 'view/com/util/Views' import {Text} from 'view/com/util/text/Text' @@ -51,7 +50,7 @@ const HITSLOP_BOTTOM = { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> -export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) { +export function SavedFeeds({}: Props) { const pal = usePalette('default') const {_} = useLingui() const {isMobile, isTabletOrDesktop} = useWebMediaQueries() @@ -147,7 +146,7 @@ export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) { </ScrollView> </CenteredView> ) -}) +} function ListItem({ feedUri, diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx index 69130ecfd..a65149bf7 100644 --- a/src/view/screens/Search/index.tsx +++ b/src/view/screens/Search/index.tsx @@ -1,6 +1,3 @@ -import {withAuthRequired} from '#/view/com/auth/withAuthRequired' import {SearchScreenMobile} from '#/view/screens/Search/Search' -export const SearchScreen = withAuthRequired(SearchScreenMobile, { - isPublic: true, -}) +export const SearchScreen = SearchScreenMobile diff --git a/src/view/screens/Search/index.web.tsx b/src/view/screens/Search/index.web.tsx index 03fb7589c..8e039e3cd 100644 --- a/src/view/screens/Search/index.web.tsx +++ b/src/view/screens/Search/index.web.tsx @@ -1,6 +1,3 @@ -import {withAuthRequired} from '#/view/com/auth/withAuthRequired' import {SearchScreenDesktop} from '#/view/screens/Search/Search' -export const SearchScreen = withAuthRequired(SearchScreenDesktop, { - isPublic: true, -}) +export const SearchScreen = SearchScreenDesktop diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index cc4348fff..88cc2d532 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -20,7 +20,6 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' import * as AppInfo from 'lib/app-info' import {s, colors} from 'lib/styles' import {ScrollView} from '../com/util/Views' @@ -141,7 +140,7 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> -export const SettingsScreen = withAuthRequired(function Settings({}: Props) { +export function SettingsScreen({}: Props) { const queryClient = useQueryClient() const colorMode = useColorMode() const setColorMode = useSetColorMode() @@ -731,7 +730,7 @@ export const SettingsScreen = withAuthRequired(function Settings({}: Props) { </ScrollView> </View> ) -}) +} function EmailConfirmationNotice() { const pal = usePalette('default') diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx new file mode 100644 index 000000000..c7b5d1d2e --- /dev/null +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -0,0 +1,150 @@ +import * as React from 'react' +import {View} from 'react-native' + +// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts +// MIT License +// Copyright (c) 2017 React Navigation Contributors + +import { + createNavigatorFactory, + EventArg, + ParamListBase, + StackActionHelpers, + StackActions, + StackNavigationState, + StackRouter, + StackRouterOptions, + useNavigationBuilder, +} from '@react-navigation/native' +import type { + NativeStackNavigationEventMap, + NativeStackNavigationOptions, +} from '@react-navigation/native-stack' +import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types' +import {NativeStackView} from '@react-navigation/native-stack' + +import {BottomBarWeb} from './bottom-bar/BottomBarWeb' +import {DesktopLeftNav} from './desktop/LeftNav' +import {DesktopRightNav} from './desktop/RightNav' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {useOnboardingState} from '#/state/shell' +import { + useLoggedOutView, + useLoggedOutViewControls, +} from '#/state/shell/logged-out' +import {useSession} from '#/state/session' +import {isWeb} from 'platform/detection' +import {LoggedOut} from '../com/auth/LoggedOut' +import {Onboarding} from '../com/auth/Onboarding' + +type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { + requireAuth?: boolean +} + +function NativeStackNavigator({ + id, + initialRouteName, + children, + screenListeners, + screenOptions, + ...rest +}: NativeStackNavigatorProps) { + // --- this is copy and pasted from the original native stack navigator --- + const {state, descriptors, navigation, NavigationContent} = + useNavigationBuilder< + StackNavigationState<ParamListBase>, + StackRouterOptions, + StackActionHelpers<ParamListBase>, + NativeStackNavigationOptionsWithAuth, + NativeStackNavigationEventMap + >(StackRouter, { + id, + initialRouteName, + children, + screenListeners, + screenOptions, + }) + React.useEffect( + () => + // @ts-expect-error: there may not be a tab navigator in parent + navigation?.addListener?.('tabPress', (e: any) => { + const isFocused = navigation.isFocused() + + // Run the operation in the next frame so we're sure all listeners have been run + // This is necessary to know if preventDefault() has been called + requestAnimationFrame(() => { + if ( + state.index > 0 && + isFocused && + !(e as EventArg<'tabPress', true>).defaultPrevented + ) { + // When user taps on already focused tab and we're inside the tab, + // reset the stack to replicate native behaviour + navigation.dispatch({ + ...StackActions.popToTop(), + target: state.key, + }) + } + }) + }), + [navigation, state.index, state.key], + ) + + // --- our custom logic starts here --- + const {hasSession} = useSession() + const activeRoute = state.routes[state.index] + const activeDescriptor = descriptors[activeRoute.key] + const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false + const onboardingState = useOnboardingState() + const {showLoggedOut} = useLoggedOutView() + const {setShowLoggedOut} = useLoggedOutViewControls() + const {isMobile} = useWebMediaQueries() + if (activeRouteRequiresAuth && !hasSession) { + return <LoggedOut /> + } + if (showLoggedOut) { + return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> + } + if (onboardingState.isActive) { + return <Onboarding /> + } + const newDescriptors: typeof descriptors = {} + for (let key in descriptors) { + const descriptor = descriptors[key] + const requireAuth = descriptor.options.requireAuth ?? false + newDescriptors[key] = { + ...descriptor, + render() { + if (requireAuth && !hasSession) { + return <View /> + } else { + return descriptor.render() + } + }, + } + } + return ( + <NavigationContent> + <NativeStackView + {...rest} + state={state} + navigation={navigation} + descriptors={newDescriptors} + /> + {isWeb && isMobile && <BottomBarWeb />} + {isWeb && !isMobile && ( + <> + <DesktopLeftNav /> + <DesktopRightNav /> + </> + )} + </NavigationContent> + ) +} + +export const createNativeStackNavigatorWithAuth = createNavigatorFactory< + StackNavigationState<ParamListBase>, + NativeStackNavigationOptionsWithAuth, + NativeStackNavigationEventMap, + typeof NativeStackNavigator +>(NativeStackNavigator) diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 74477243d..38da860bd 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -1,7 +1,5 @@ import React, {useEffect} from 'react' import {View, StyleSheet, TouchableOpacity} from 'react-native' -import {DesktopLeftNav} from './desktop/LeftNav' -import {DesktopRightNav} from './desktop/RightNav' import {ErrorBoundary} from '../com/util/ErrorBoundary' import {Lightbox} from '../com/lightbox/Lightbox' import {ModalsContainer} from '../com/modals/Modal' @@ -11,27 +9,19 @@ import {s, colors} from 'lib/styles' import {RoutesContainer, FlatNavigator} from '../../Navigation' import {DrawerContent} from './Drawer' import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' -import {BottomBarWeb} from './bottom-bar/BottomBarWeb' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {useAuxClick} from 'lib/hooks/useAuxClick' import {t} from '@lingui/macro' -import { - useIsDrawerOpen, - useSetDrawerOpen, - useOnboardingState, -} from '#/state/shell' +import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useCloseAllActiveElements} from '#/state/util' -import {useLoggedOutView} from '#/state/shell/logged-out' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() const setDrawerOpen = useSetDrawerOpen() - const onboardingState = useOnboardingState() - const {isDesktop, isMobile} = useWebMediaQueries() + const {isDesktop} = useWebMediaQueries() const navigator = useNavigation<NavigationProp>() const closeAllActiveElements = useCloseAllActiveElements() - const {showLoggedOut} = useLoggedOutView() useAuxClick() @@ -42,8 +32,6 @@ function ShellInner() { return unsubscribe }, [navigator, closeAllActiveElements]) - const showBottomBar = isMobile && !onboardingState.isActive - const showSideNavs = !isMobile && !onboardingState.isActive && !showLoggedOut return ( <View style={[s.hContentRegion, {overflow: 'hidden'}]}> <View style={s.hContentRegion}> @@ -51,22 +39,9 @@ function ShellInner() { <FlatNavigator /> </ErrorBoundary> </View> - - {showSideNavs && ( - <> - <DesktopLeftNav /> - <DesktopRightNav /> - </> - )} - <Composer winHeight={0} /> - - {showBottomBar && <BottomBarWeb />} - <ModalsContainer /> - <Lightbox /> - {!isDesktop && isDrawerOpen && ( <TouchableOpacity onPress={() => setDrawerOpen(false)} |