From ec5c4929c1c5677d22c923193ce04f3d69b72711 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 12 Apr 2024 14:13:13 -0700 Subject: [PATCH] PWI improvements (#3489) * Enable home and feeds on the PWI * Add global SigninDialog to drive useRequireAuth() * Tweak desktop styles * Make the logo in leftnav PWI a clickable home link * Add label * Improve dialog on web * Fix query key * Go to home after signout from settings screen * Filter out feeds from the discover listing for logged out users which are known to break without auth * Update profile header follow/subscribe to give signin prompt * Show profile feeds tabs on pwi * Add language selector to account creation footer and pwi left nav desktop --------- Co-authored-by: dan --- src/Navigation.tsx | 18 +- src/components/AppLanguageDropdown.tsx | 67 +++++++ src/components/AppLanguageDropdown.web.tsx | 62 +++++++ src/components/dialogs/Context.tsx | 7 +- src/components/dialogs/Signin.tsx | 99 ++++++++++ .../Profile/Header/ProfileHeaderLabeler.tsx | 51 +++--- .../Profile/Header/ProfileHeaderStandard.tsx | 1 - src/screens/Signup/index.tsx | 11 +- src/state/queries/feed.ts | 27 ++- src/state/session/index.tsx | 8 +- src/view/com/auth/HomeLoggedOutCTA.tsx | 170 ------------------ src/view/com/auth/SplashScreen.tsx | 58 +----- src/view/com/auth/SplashScreen.web.tsx | 56 +----- src/view/com/home/HomeHeaderLayout.web.tsx | 63 ++++--- src/view/com/home/HomeHeaderLayoutMobile.tsx | 44 ++--- src/view/screens/Feeds.tsx | 89 +++++---- src/view/screens/Home.tsx | 9 +- src/view/screens/Profile.tsx | 3 +- src/view/screens/Settings/index.tsx | 10 +- src/view/shell/Drawer.tsx | 56 +++--- src/view/shell/NavSignupCard.tsx | 19 +- src/view/shell/index.tsx | 39 ++-- src/view/shell/index.web.tsx | 28 +-- 23 files changed, 518 insertions(+), 477 deletions(-) create mode 100644 src/components/AppLanguageDropdown.tsx create mode 100644 src/components/AppLanguageDropdown.web.tsx create mode 100644 src/components/dialogs/Signin.tsx delete mode 100644 src/view/com/auth/HomeLoggedOutCTA.tsx diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 070c57960d..99c0ebf3c3 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -193,7 +193,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { ProfileFeedScreen} - options={{title: title(msg`Feed`), requireAuth: true}} + options={{title: title(msg`Feed`)}} /> - HomeScreen} - options={{requireAuth: true}} - /> + HomeScreen} /> {commonScreens(HomeTab)} ) @@ -371,11 +367,7 @@ function FeedsTabNavigator() { animationDuration: 250, contentStyle: pal.view, }}> - FeedsScreen} - options={{requireAuth: true}} - /> + FeedsScreen} /> {commonScreens(FeedsTab as typeof HomeTab)} ) @@ -451,7 +443,7 @@ const FlatNavigator = () => { HomeScreen} - options={{title: title(msg`Home`), requireAuth: true}} + options={{title: title(msg`Home`)}} /> { FeedsScreen} - options={{title: title(msg`Feeds`), requireAuth: true}} + options={{title: title(msg`Feeds`)}} /> [0]) => { + if (!value) return + if (sanitizedLang !== value) { + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) + } + }, + [sanitizedLang, setLangPrefs], + ) + + return ( + + Boolean(l.code2)).map(l => ({ + label: l.name, + value: l.code2, + key: l.code2, + }))} + useNativeAndroidPickerStyle={false} + style={{ + inputAndroid: { + color: t.atoms.text_contrast_medium.color, + fontSize: 16, + paddingRight: 12 + 4, + }, + inputIOS: { + color: t.atoms.text.color, + fontSize: 16, + paddingRight: 12 + 4, + }, + }} + /> + + + + + + ) +} diff --git a/src/components/AppLanguageDropdown.web.tsx b/src/components/AppLanguageDropdown.web.tsx new file mode 100644 index 0000000000..302a30ca66 --- /dev/null +++ b/src/components/AppLanguageDropdown.web.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import {View} from 'react-native' + +import {sanitizeAppLanguageSetting} from '#/locale/helpers' +import {APP_LANGUAGES} from '#/locale/languages' +import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' +import {atoms as a, useTheme} from '#/alf' +import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' +import {Text} from '#/components/Typography' + +export function AppLanguageDropdown() { + const t = useTheme() + + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() + + const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) + + const onChangeAppLanguage = React.useCallback( + (ev: React.ChangeEvent) => { + const value = ev.target.value + + if (!value) return + if (sanitizedLang !== value) { + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) + } + }, + [sanitizedLang, setLangPrefs], + ) + + return ( + + + {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name} + + + + + + ) +} diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx index 87bd5c2ed7..c9dff9a999 100644 --- a/src/components/dialogs/Context.tsx +++ b/src/components/dialogs/Context.tsx @@ -6,10 +6,12 @@ type Control = Dialog.DialogOuterProps['control'] type ControlsContext = { mutedWordsDialogControl: Control + signinDialogControl: Control } const ControlsContext = React.createContext({ mutedWordsDialogControl: {} as Control, + signinDialogControl: {} as Control, }) export function useGlobalDialogsControlContext() { @@ -18,9 +20,10 @@ export function useGlobalDialogsControlContext() { export function Provider({children}: React.PropsWithChildren<{}>) { const mutedWordsDialogControl = Dialog.useDialogControl() + const signinDialogControl = Dialog.useDialogControl() const ctx = React.useMemo( - () => ({mutedWordsDialogControl}), - [mutedWordsDialogControl], + () => ({mutedWordsDialogControl, signinDialogControl}), + [mutedWordsDialogControl, signinDialogControl], ) return ( diff --git a/src/components/dialogs/Signin.tsx b/src/components/dialogs/Signin.tsx new file mode 100644 index 0000000000..488eb5c73a --- /dev/null +++ b/src/components/dialogs/Signin.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isNative} from '#/platform/detection' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import {Logo} from '#/view/icons/Logo' +import {Logotype} from '#/view/icons/Logotype' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {Text} from '#/components/Typography' + +export function SigninDialog() { + const {signinDialogControl: control} = useGlobalDialogsControlContext() + return ( + + + + + ) +} + +function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) { + const t = useTheme() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const {requestSwitchToAccount} = useLoggedOutViewControls() + const closeAllActiveElements = useCloseAllActiveElements() + + const showSignIn = React.useCallback(() => { + closeAllActiveElements() + requestSwitchToAccount({requestedAccount: 'none'}) + }, [requestSwitchToAccount, closeAllActiveElements]) + + const showCreateAccount = React.useCallback(() => { + closeAllActiveElements() + requestSwitchToAccount({requestedAccount: 'new'}) + }, [requestSwitchToAccount, closeAllActiveElements]) + + return ( + + + + + + + + + + + + Sign in or create your account to join the conversation! + + + + + + + + + + {isNative && } + + + + + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index d0fd5e20bd..b9145822c9 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -18,7 +18,7 @@ import {useModalControls} from '#/state/modals' import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useSession} from '#/state/session' +import {useRequireAuth, useSession} from '#/state/session' import {useAnalytics} from 'lib/analytics/analytics' import {useHaptics} from 'lib/haptics' import {useProfileShadow} from 'state/cache/profile-shadow' @@ -64,6 +64,7 @@ let ProfileHeaderLabeler = ({ const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const {track} = useAnalytics() + const requireAuth = useRequireAuth() const playHaptic = useHaptics() const cantSubscribePrompt = Prompt.usePromptControl() const isSelf = currentAccount?.did === profile.did @@ -125,27 +126,32 @@ let ProfileHeaderLabeler = ({ }) }, [track, openModal, profile]) - const onPressSubscribe = React.useCallback(async () => { - if (!canSubscribe) { - cantSubscribePrompt.open() - return - } - try { - await toggleSubscription({ - did: profile.did, - subscribe: !isSubscribed, - }) - } catch (e: any) { - // setSubscriptionError(e.message) - logger.error(`Failed to subscribe to labeler`, {message: e.message}) - } - }, [ - toggleSubscription, - isSubscribed, - profile, - canSubscribe, - cantSubscribePrompt, - ]) + const onPressSubscribe = React.useCallback( + () => + requireAuth(async () => { + if (!canSubscribe) { + cantSubscribePrompt.open() + return + } + try { + await toggleSubscription({ + did: profile.did, + subscribe: !isSubscribed, + }) + } catch (e: any) { + // setSubscriptionError(e.message) + logger.error(`Failed to subscribe to labeler`, {message: e.message}) + } + }), + [ + requireAuth, + toggleSubscription, + isSubscribed, + profile, + canSubscribe, + cantSubscribePrompt, + ], + ) const isMe = React.useMemo( () => currentAccount?.did === profile.did, @@ -184,7 +190,6 @@ let ProfileHeaderLabeler = ({ ? _(msg`Unsubscribe from this labeler`) : _(msg`Subscribe to this labeler`) } - disabled={!hasSession} onPress={onPressSubscribe}> {state => ( void}) { - - + + + Having trouble?{' '} - + Contact support diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index c56912491a..0d3de89697 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -17,7 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {getAgent} from '#/state/session' +import {getAgent, useSession} from '#/state/session' import {router} from '#/routes' export type FeedSourceFeedInfo = { @@ -216,17 +216,38 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = { likeCount: 0, likeUri: '', } +const DISCOVER_FEED_STUB: FeedSourceInfo = { + type: 'feed', + displayName: 'Discover', + uri: '', + route: { + href: '/', + name: 'Home', + params: {}, + }, + cid: '', + avatar: '', + description: new RichText({text: ''}), + creatorDid: '', + creatorHandle: '', + likeCount: 0, + likeUri: '', +} const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos' export function usePinnedFeedsInfos() { + const {hasSession} = useSession() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const pinnedUris = preferences?.feeds?.pinned ?? [] return useQuery({ staleTime: STALE.INFINITY, enabled: !isLoadingPrefs, - queryKey: [pinnedFeedInfosQueryKeyRoot, pinnedUris.join(',')], + queryKey: [ + pinnedFeedInfosQueryKeyRoot, + (hasSession ? 'authed:' : 'unauthed:') + pinnedUris.join(','), + ], queryFn: async () => { let resolved = new Map() @@ -264,7 +285,7 @@ export function usePinnedFeedsInfos() { ) // The returned result will have the original order. - const result = [FOLLOWING_FEED_STUB] + const result = [hasSession ? FOLLOWING_FEED_STUB : DISCOVER_FEED_STUB] await Promise.allSettled([feedsPromise, ...listsPromises]) for (let pinnedUri of pinnedUris) { if (resolved.has(pinnedUri)) { diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 5c7cc15916..b88181ebda 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -15,8 +15,8 @@ import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' import {PUBLIC_BSKY_AGENT} from '#/state/queries' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {IS_DEV} from '#/env' import {emitSessionDropped} from '../events' import {readLabelers} from './agent-config' @@ -702,8 +702,8 @@ export function useSessionApi() { export function useRequireAuth() { const {hasSession} = useSession() - const {setShowLoggedOut} = useLoggedOutViewControls() const closeAll = useCloseAllActiveElements() + const {signinDialogControl} = useGlobalDialogsControlContext() return React.useCallback( (fn: () => void) => { @@ -711,10 +711,10 @@ export function useRequireAuth() { fn() } else { closeAll() - setShowLoggedOut(true) + signinDialogControl.open() } }, - [hasSession, setShowLoggedOut, closeAll], + [hasSession, signinDialogControl, closeAll], ) } diff --git a/src/view/com/auth/HomeLoggedOutCTA.tsx b/src/view/com/auth/HomeLoggedOutCTA.tsx deleted file mode 100644 index 4c8c35da73..0000000000 --- a/src/view/com/auth/HomeLoggedOutCTA.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {colors, s} from '#/lib/styles' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import {TextLink} from '../util/Link' -import {Text} from '../util/text/Text' -import {ScrollView} from '../util/Views' - -export function HomeLoggedOutCTA() { - const pal = usePalette('default') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - const {requestSwitchToAccount} = useLoggedOutViewControls() - - const showCreateAccount = React.useCallback(() => { - requestSwitchToAccount({requestedAccount: 'new'}) - }, [requestSwitchToAccount]) - - const showSignIn = React.useCallback(() => { - requestSwitchToAccount({requestedAccount: 'none'}) - }, [requestSwitchToAccount]) - - return ( - - - - Bluesky - - - See what's next - - - - - - Create a new account - - - - - Sign in - - - - - - - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - height: '100%', - }, - hero: { - justifyContent: 'center', - paddingTop: 100, - paddingBottom: 30, - }, - heroMobile: { - paddingBottom: 50, - }, - title: { - textAlign: 'center', - fontSize: 68, - fontWeight: 'bold', - }, - subtitle: { - textAlign: 'center', - fontSize: 48, - fontWeight: 'bold', - }, - subtitleMobile: { - fontSize: 42, - }, - btnsDesktop: { - flexDirection: 'row', - justifyContent: 'center', - gap: 20, - marginHorizontal: 20, - }, - btn: { - borderRadius: 32, - width: 230, - paddingVertical: 12, - marginBottom: 20, - }, - btnMobile: { - flex: 1, - width: 'auto', - marginHorizontal: 20, - paddingVertical: 16, - }, - btnLabel: { - textAlign: 'center', - fontSize: 18, - }, - btnLabelMobile: { - textAlign: 'center', - fontSize: 21, - }, - - footer: { - flexDirection: 'row', - gap: 20, - justifyContent: 'center', - }, - footerLink: {}, -}) diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx index 763b01dfa1..8eac1ab82f 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -1,19 +1,15 @@ import React from 'react' import {View} from 'react-native' -import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {sanitizeAppLanguageSetting} from '#/locale/helpers' -import {APP_LANGUAGES} from '#/locale/languages' -import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {atoms as a, useTheme} from '#/alf' +import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {Button, ButtonText} from '#/components/Button' -import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {Text} from '#/components/Typography' import {CenteredView} from '../util/Views' @@ -27,22 +23,8 @@ export const SplashScreen = ({ const t = useTheme() const {_} = useLingui() - const langPrefs = useLanguagePrefs() - const setLangPrefs = useLanguagePrefsApi() const insets = useSafeAreaInsets() - const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) - - const onChangeAppLanguage = React.useCallback( - (value: Parameters[0]) => { - if (!value) return - if (sanitizedLang !== value) { - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) - } - }, - [sanitizedLang, setLangPrefs], - ) - return ( @@ -99,43 +81,7 @@ export const SplashScreen = ({ a.justify_center, a.align_center, ]}> - - Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2, - }))} - useNativeAndroidPickerStyle={false} - style={{ - inputAndroid: { - color: t.atoms.text_contrast_medium.color, - fontSize: 16, - paddingRight: 12 + 4, - }, - inputIOS: { - color: t.atoms.text.color, - fontSize: 16, - paddingRight: 12 + 4, - }, - }} - /> - - - - - + diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx index 7a2ee16cf3..f905e1e8d5 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -4,16 +4,13 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {sanitizeAppLanguageSetting} from '#/locale/helpers' -import {APP_LANGUAGES} from '#/locale/languages' -import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {atoms as a, useTheme} from '#/alf' +import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {Button, ButtonText} from '#/components/Button' -import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' import {CenteredView} from '../util/Views' @@ -131,23 +128,6 @@ export const SplashScreen = ({ function Footer() { const t = useTheme() - const langPrefs = useLanguagePrefs() - const setLangPrefs = useLanguagePrefsApi() - - const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) - - const onChangeAppLanguage = React.useCallback( - (ev: React.ChangeEvent) => { - const value = ev.target.value - - if (!value) return - if (sanitizedLang !== value) { - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) - } - }, - [sanitizedLang, setLangPrefs], - ) - return ( - - - {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name} - - - - - + ) } diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index 9818b56f6f..644d4cab6c 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -1,20 +1,22 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import Animated from 'react-native-reanimated' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' -import {Logo} from '#/view/icons/Logo' -import {Link} from '../util/Link' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + import {CogIcon} from '#/lib/icons' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {useSession} from '#/state/session' import {useShellLayout} from '#/state/shell/shell-layout' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {Logo} from '#/view/icons/Logo' +import {Link} from '../util/Link' +import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' export function HomeHeaderLayout(props: { children: React.ReactNode @@ -38,32 +40,35 @@ function HomeHeaderLayoutDesktopAndTablet({ const pal = usePalette('default') const {headerMinimalShellTransform} = useMinimalShellMode() const {headerHeight} = useShellLayout() + const {hasSession} = useSession() const {_} = useLingui() return ( <> - - - - - - - - - + {hasSession && ( + + + + + + + + + + )} {tabBarAnchor} { diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index d7b7231c60..78fa9af865 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -1,23 +1,24 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {usePalette} from 'lib/hooks/usePalette' -import {Link} from '../util/Link' +import Animated from 'react-native-reanimated' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' -import {HITSLOP_10} from 'lib/constants' -import Animated from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' + +import {useSession} from '#/state/session' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' +import {HITSLOP_10} from 'lib/constants' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {Logo} from '#/view/icons/Logo' - -import {IS_DEV} from '#/env' import {atoms} from '#/alf' -import {Link as Link2} from '#/components/Link' import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' +import {Link as Link2} from '#/components/Link' +import {IS_DEV} from '#/env' +import {Link} from '../util/Link' export function HomeHeaderLayoutMobile({ children, @@ -30,6 +31,7 @@ export function HomeHeaderLayoutMobile({ const setDrawerOpen = useSetDrawerOpen() const {headerHeight} = useShellLayout() const {headerMinimalShellTransform} = useMinimalShellMode() + const {hasSession} = useSession() const onPressAvi = React.useCallback(() => { setDrawerOpen(true) @@ -76,18 +78,20 @@ export function HomeHeaderLayoutMobile({ )} - - - + {hasSession && ( + + + + )} {children} diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 2e3bf08db5..e64ab08df2 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -1,52 +1,53 @@ import React from 'react' import { ActivityIndicator, - StyleSheet, - View, type FlatList, Pressable, + StyleSheet, + View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {FAB} from 'view/com/util/fab/FAB' -import {Link} from 'view/com/util/Link' -import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {ComposeIcon2, CogIcon, MagnifyingGlassIcon2} from 'lib/icons' -import {s} from 'lib/styles' -import {atoms as a, useTheme} from '#/alf' -import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput' -import {UserAvatar} from 'view/com/util/UserAvatar' -import { - LoadingPlaceholder, - FeedFeedLoadingPlaceholder, -} from 'view/com/util/LoadingPlaceholder' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import debounce from 'lodash.debounce' -import {Text} from 'view/com/util/text/Text' -import {List} from 'view/com/util/List' -import {useFocusEffect} from '@react-navigation/native' -import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' -import {Trans, msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useSetMinimalShellMode} from '#/state/shell' -import {usePreferencesQuery} from '#/state/queries/preferences' +import {useFocusEffect} from '@react-navigation/native' +import debounce from 'lodash.debounce' + +import {isNative, isWeb} from '#/platform/detection' import { + getAvatarTypeFromUri, useFeedSourceInfoQuery, useGetPopularFeedsQuery, useSearchPopularFeedsMutation, - getAvatarTypeFromUri, } from '#/state/queries/feed' -import {cleanError} from 'lib/strings/errors' -import {useComposerControls} from '#/state/shell/composer' +import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' -import {isNative, isWeb} from '#/platform/detection' +import {useSetMinimalShellMode} from '#/state/shell' +import {useComposerControls} from '#/state/shell/composer' import {HITSLOP_10} from 'lib/constants' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {CogIcon, ComposeIcon2, MagnifyingGlassIcon2} from 'lib/icons' +import {FeedsTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' +import {cleanError} from 'lib/strings/errors' +import {s} from 'lib/styles' +import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {ErrorMessage} from 'view/com/util/error/ErrorMessage' +import {FAB} from 'view/com/util/fab/FAB' +import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput' +import {Link} from 'view/com/util/Link' +import {List} from 'view/com/util/List' +import { + FeedFeedLoadingPlaceholder, + LoadingPlaceholder, +} from 'view/com/util/LoadingPlaceholder' +import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {atoms as a, useTheme} from '#/alf' import {IconCircle} from '#/components/IconCircle' -import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' +import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' type Props = NativeStackScreenProps @@ -100,6 +101,22 @@ type FlatlistSlice = key: string } +// HACK +// the protocol doesn't yet tell us which feeds are personalized +// this list is used to filter out feed recommendations from logged out users +// for the ones we know need it +// -prf +const KNOWN_AUTHED_ONLY_FEEDS = [ + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed + 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz + 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why +] + export function FeedsScreen(_props: Props) { const pal = usePalette('default') const {openComposer} = useComposerControls() @@ -299,7 +316,15 @@ export function FeedsScreen(_props: Props) { for (const page of popularFeeds.pages || []) { slices = slices.concat( page.feeds - .filter(feed => !preferences?.feeds?.saved.includes(feed.uri)) + .filter(feed => { + if ( + !hasSession && + KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) + ) { + return false + } + return !preferences?.feeds?.saved.includes(feed.uri) + }) .map(feed => ({ key: `popularFeed:${feed.uri}`, type: 'popularFeed', diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 39bdac669c..7a2a88265b 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -2,6 +2,7 @@ import React from 'react' import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' +import {PROD_DEFAULT_FEED} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useSetTitle} from '#/lib/hooks/useSetTitle' import {logEvent, LogEvents, useGate} from '#/lib/statsig/statsig' @@ -19,7 +20,6 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' -import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeHeader} from '../com/home/HomeHeader' type Props = NativeStackScreenProps @@ -231,7 +231,12 @@ function HomeScreenReady({ onPageSelected={onPageSelected} onPageScrollStateChanged={onPageScrollStateChanged} renderTabBar={renderTabBar}> - + ) } diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index c391f80508..f71e1330ef 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -184,8 +184,7 @@ function ProfileScreenLoaded({ const showRepliesTab = hasSession const showMediaTab = !hasLabeler const showLikesTab = isMe - const showFeedsTab = - hasSession && (isMe || (profile.associated?.feedgens || 0) > 0) + const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0 const showListsTab = hasSession && (isMe || (profile.associated?.lists || 0) > 0) diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 8a7fa5e714..b97faafad1 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -71,6 +71,7 @@ import {UserAvatar} from 'view/com/util/UserAvatar' import {ScrollView} from 'view/com/util/Views' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {navigate, resetToTab} from '#/Navigation' import {ExportCarDialog} from './ExportCarDialog' function SettingsAccountCard({account}: {account: SessionAccount}) { @@ -104,7 +105,14 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { { - logout('Settings') + if (isNative) { + logout('Settings') + resetToTab('HomeTab') + } else { + navigate('Home').then(() => { + logout('Settings') + }) + } }} accessibilityRole="button" accessibilityLabel={_(msg`Sign out`)} diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 1bf5647f66..a7342179d4 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -9,49 +9,49 @@ import { View, ViewStyle, } from 'react-native' -import {useNavigation, StackActions} from '@react-navigation/native' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {s, colors} from 'lib/styles' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {StackActions, useNavigation} from '@react-navigation/native' + +import {emitSoftReset} from '#/state/events' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {useProfileQuery} from '#/state/queries/profile' +import {SessionAccount, useSession} from '#/state/session' +import {useSetDrawerOpen} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' +import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' +import {usePalette} from 'lib/hooks/usePalette' import { - HomeIcon, - HomeIconSolid, BellIcon, BellIconSolid, - UserIcon, CogIcon, + HandIcon, + HashtagIcon, + HomeIcon, + HomeIconSolid, + ListIcon, MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, + UserIcon, UserIconSolid, - HashtagIcon, - ListIcon, - HandIcon, } from 'lib/icons' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {Text} from 'view/com/util/text/Text' -import {useTheme} from 'lib/ThemeContext' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {pluralize} from 'lib/strings/helpers' import {getTabState, TabState} from 'lib/routes/helpers' import {NavigationProp} from 'lib/routes/types' -import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' +import {pluralize} from 'lib/strings/helpers' +import {colors, s} from 'lib/styles' +import {useTheme} from 'lib/ThemeContext' import {isWeb} from 'platform/detection' -import {formatCountShortOnly} from 'view/com/util/numeric/format' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useSetDrawerOpen} from '#/state/shell' -import {useSession, SessionAccount} from '#/state/session' -import {useProfileQuery} from '#/state/queries/profile' -import {useUnreadNotifications} from '#/state/queries/notifications/unread' -import {emitSoftReset} from '#/state/events' import {NavSignupCard} from '#/view/shell/NavSignupCard' -import {TextLink} from '../com/util/Link' - +import {formatCountShortOnly} from 'view/com/util/numeric/format' +import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' import {useTheme as useAlfTheme} from '#/alf' +import {TextLink} from '../com/util/Link' let DrawerProfileCard = ({ account, @@ -246,7 +246,11 @@ let DrawerContent = ({}: {}): React.ReactNode => { ) : ( - + <> + + + + )} diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx index 83d1414984..aa807f0cc6 100644 --- a/src/view/shell/NavSignupCard.tsx +++ b/src/view/shell/NavSignupCard.tsx @@ -3,13 +3,16 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from '#/view/com/util/text/Text' -import {Button} from '#/view/com/util/forms/Button' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' +import {Button} from '#/view/com/util/forms/Button' +import {Text} from '#/view/com/util/text/Text' import {Logo} from '#/view/icons/Logo' +import {atoms as a} from '#/alf' +import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' +import {Link} from '#/components/Link' let NavSignupCard = ({}: {}): React.ReactNode => { const {_} = useLingui() @@ -35,7 +38,9 @@ let NavSignupCard = ({}: {}): React.ReactNode => { paddingTop: 6, marginBottom: 24, }}> - + + + @@ -62,6 +67,10 @@ let NavSignupCard = ({}: {}): React.ReactNode => { + + + + ) } diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index f29183095a..c554112ed5 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -1,37 +1,39 @@ import React from 'react' -import {StatusBar} from 'expo-status-bar' import { + BackHandler, DimensionValue, StyleSheet, useWindowDimensions, View, - BackHandler, } from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Drawer} from 'react-native-drawer-layout' +import Animated from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {StatusBar} from 'expo-status-bar' import {useNavigationState} from '@react-navigation/native' -import {ModalsContainer} from 'view/com/modals/Modal' -import {Lightbox} from 'view/com/lightbox/Lightbox' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' -import {DrawerContent} from './Drawer' -import {Composer} from './Composer' -import {useTheme} from 'lib/ThemeContext' -import {usePalette} from 'lib/hooks/usePalette' -import {RoutesContainer, TabsNavigator} from '../../Navigation' -import {isStateAtTabRoot} from 'lib/routes/helpers' + +import {useSession} from '#/state/session' import { useIsDrawerOpen, - useSetDrawerOpen, useIsDrawerSwipeDisabled, + useSetDrawerOpen, } from '#/state/shell' -import {isAndroid} from 'platform/detection' -import {useSession} from '#/state/session' import {useCloseAnyActiveElement} from '#/state/util' +import {usePalette} from 'lib/hooks/usePalette' import * as notifications from 'lib/notifications/notifications' -import {Outlet as PortalOutlet} from '#/components/Portal' -import {MutedWordsDialog} from '#/components/dialogs/MutedWords' +import {isStateAtTabRoot} from 'lib/routes/helpers' +import {useTheme} from 'lib/ThemeContext' +import {isAndroid} from 'platform/detection' import {useDialogStateContext} from 'state/dialogs' -import Animated from 'react-native-reanimated' +import {Lightbox} from 'view/com/lightbox/Lightbox' +import {ModalsContainer} from 'view/com/modals/Modal' +import {ErrorBoundary} from 'view/com/util/ErrorBoundary' +import {MutedWordsDialog} from '#/components/dialogs/MutedWords' +import {SigninDialog} from '#/components/dialogs/Signin' +import {Outlet as PortalOutlet} from '#/components/Portal' +import {RoutesContainer, TabsNavigator} from '../../Navigation' +import {Composer} from './Composer' +import {DrawerContent} from './Drawer' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -101,6 +103,7 @@ function ShellInner() { + diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 02993ac462..51fb4a0a11 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -1,24 +1,25 @@ import React, {useEffect} from 'react' -import {View, StyleSheet, TouchableOpacity} from 'react-native' -import {useNavigation} from '@react-navigation/native' +import {StyleSheet, TouchableOpacity, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' -import {ErrorBoundary} from '../com/util/ErrorBoundary' +import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' +import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' +import {useCloseAllActiveElements} from '#/state/util' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {NavigationProp} from 'lib/routes/types' +import {colors, s} from 'lib/styles' +import {MutedWordsDialog} from '#/components/dialogs/MutedWords' +import {SigninDialog} from '#/components/dialogs/Signin' +import {Outlet as PortalOutlet} from '#/components/Portal' +import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' +import {FlatNavigator, RoutesContainer} from '../../Navigation' import {Lightbox} from '../com/lightbox/Lightbox' import {ModalsContainer} from '../com/modals/Modal' +import {ErrorBoundary} from '../com/util/ErrorBoundary' import {Composer} from './Composer.web' -import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' -import {s, colors} from 'lib/styles' -import {RoutesContainer, FlatNavigator} from '../../Navigation' import {DrawerContent} from './Drawer' -import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' -import {NavigationProp} from 'lib/routes/types' -import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' -import {useCloseAllActiveElements} from '#/state/util' -import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' -import {Outlet as PortalOutlet} from '#/components/Portal' -import {MutedWordsDialog} from '#/components/dialogs/MutedWords' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -45,6 +46,7 @@ function ShellInner() { +