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 <dan.abramov@gmail.com>
zio/stable
Paul Frazee 2024-04-12 14:13:13 -07:00 committed by GitHub
parent 44039c68d6
commit ec5c4929c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 519 additions and 478 deletions

View File

@ -193,7 +193,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen <Stack.Screen
name="ProfileFeed" name="ProfileFeed"
getComponent={() => ProfileFeedScreen} getComponent={() => ProfileFeedScreen}
options={{title: title(msg`Feed`), requireAuth: true}} options={{title: title(msg`Feed`)}}
/> />
<Stack.Screen <Stack.Screen
name="ProfileFeedLikedBy" name="ProfileFeedLikedBy"
@ -331,11 +331,7 @@ function HomeTabNavigator() {
animationDuration: 250, animationDuration: 250,
contentStyle: pal.view, contentStyle: pal.view,
}}> }}>
<HomeTab.Screen <HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
name="Home"
getComponent={() => HomeScreen}
options={{requireAuth: true}}
/>
{commonScreens(HomeTab)} {commonScreens(HomeTab)}
</HomeTab.Navigator> </HomeTab.Navigator>
) )
@ -371,11 +367,7 @@ function FeedsTabNavigator() {
animationDuration: 250, animationDuration: 250,
contentStyle: pal.view, contentStyle: pal.view,
}}> }}>
<FeedsTab.Screen <FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
name="Feeds"
getComponent={() => FeedsScreen}
options={{requireAuth: true}}
/>
{commonScreens(FeedsTab as typeof HomeTab)} {commonScreens(FeedsTab as typeof HomeTab)}
</FeedsTab.Navigator> </FeedsTab.Navigator>
) )
@ -451,7 +443,7 @@ const FlatNavigator = () => {
<Flat.Screen <Flat.Screen
name="Home" name="Home"
getComponent={() => HomeScreen} getComponent={() => HomeScreen}
options={{title: title(msg`Home`), requireAuth: true}} options={{title: title(msg`Home`)}}
/> />
<Flat.Screen <Flat.Screen
name="Search" name="Search"
@ -461,7 +453,7 @@ const FlatNavigator = () => {
<Flat.Screen <Flat.Screen
name="Feeds" name="Feeds"
getComponent={() => FeedsScreen} getComponent={() => FeedsScreen}
options={{title: title(msg`Feeds`), requireAuth: true}} options={{title: title(msg`Feeds`)}}
/> />
<Flat.Screen <Flat.Screen
name="Notifications" name="Notifications"

View File

@ -0,0 +1,67 @@
import React from 'react'
import {View} from 'react-native'
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
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'
export function AppLanguageDropdown() {
const t = useTheme()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
const onChangeAppLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
if (!value) return
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
},
[sanitizedLang, setLangPrefs],
)
return (
<View style={a.relative}>
<RNPickerSelect
placeholder={{}}
value={sanitizedLang}
onValueChange={onChangeAppLanguage}
items={APP_LANGUAGES.filter(l => 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,
},
}}
/>
<View
style={[
a.absolute,
a.inset_0,
{left: 'auto'},
{pointerEvents: 'none'},
a.align_center,
a.justify_center,
]}>
<ChevronDown fill={t.atoms.text.color} size="xs" />
</View>
</View>
)
}

View File

@ -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<HTMLSelectElement>) => {
const value = ev.target.value
if (!value) return
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
},
[sanitizedLang, setLangPrefs],
)
return (
<View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}>
<Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
{APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
</Text>
<ChevronDown fill={t.atoms.text.color} size="xs" style={a.flex_shrink} />
<select
value={sanitizedLang}
onChange={onChangeAppLanguage}
style={{
cursor: 'pointer',
MozAppearance: 'none',
WebkitAppearance: 'none',
appearance: 'none',
position: 'absolute',
inset: 0,
width: '100%',
color: 'transparent',
background: 'transparent',
border: 0,
padding: 0,
}}>
{APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
<option key={l.code2} value={l.code2}>
{l.name}
</option>
))}
</select>
</View>
)
}

View File

@ -6,10 +6,12 @@ type Control = Dialog.DialogOuterProps['control']
type ControlsContext = { type ControlsContext = {
mutedWordsDialogControl: Control mutedWordsDialogControl: Control
signinDialogControl: Control
} }
const ControlsContext = React.createContext({ const ControlsContext = React.createContext({
mutedWordsDialogControl: {} as Control, mutedWordsDialogControl: {} as Control,
signinDialogControl: {} as Control,
}) })
export function useGlobalDialogsControlContext() { export function useGlobalDialogsControlContext() {
@ -18,9 +20,10 @@ export function useGlobalDialogsControlContext() {
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const mutedWordsDialogControl = Dialog.useDialogControl() const mutedWordsDialogControl = Dialog.useDialogControl()
const signinDialogControl = Dialog.useDialogControl()
const ctx = React.useMemo<ControlsContext>( const ctx = React.useMemo<ControlsContext>(
() => ({mutedWordsDialogControl}), () => ({mutedWordsDialogControl, signinDialogControl}),
[mutedWordsDialogControl], [mutedWordsDialogControl, signinDialogControl],
) )
return ( return (

View File

@ -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 (
<Dialog.Outer control={control}>
<Dialog.Handle />
<SigninDialogInner control={control} />
</Dialog.Outer>
)
}
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 (
<Dialog.ScrollableInner
label={_(msg`Sign into Bluesky or create a new account`)}
style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}>
<View>
<View
style={[
a.flex_row,
a.align_center,
a.justify_center,
a.gap_sm,
a.pb_lg,
]}>
<Logo width={36} />
<View style={{paddingTop: 6}}>
<Logotype width={120} fill={t.atoms.text.color} />
</View>
</View>
<Text style={[a.text_lg, a.text_center, t.atoms.text, a.pb_2xl]}>
<Trans>
Sign in or create your account to join the conversation!
</Trans>
</Text>
<View style={[a.flex_col, a.gap_md]}>
<Button
variant="solid"
color="primary"
size="large"
onPress={showCreateAccount}
label={_(msg`Create an account`)}>
<ButtonText>
<Trans>Create an account</Trans>
</ButtonText>
</Button>
<Button
variant="solid"
color="secondary"
size="large"
onPress={showSignIn}
label={_(msg`Sign in`)}>
<ButtonText>
<Trans>Sign in</Trans>
</ButtonText>
</Button>
</View>
{isNative && <View style={{height: 10}} />}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}

View File

@ -18,7 +18,7 @@ import {useModalControls} from '#/state/modals'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session' import {useRequireAuth, useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useHaptics} from 'lib/haptics' import {useHaptics} from 'lib/haptics'
import {useProfileShadow} from 'state/cache/profile-shadow' import {useProfileShadow} from 'state/cache/profile-shadow'
@ -64,6 +64,7 @@ let ProfileHeaderLabeler = ({
const {currentAccount, hasSession} = useSession() const {currentAccount, hasSession} = useSession()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {track} = useAnalytics() const {track} = useAnalytics()
const requireAuth = useRequireAuth()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const cantSubscribePrompt = Prompt.usePromptControl() const cantSubscribePrompt = Prompt.usePromptControl()
const isSelf = currentAccount?.did === profile.did const isSelf = currentAccount?.did === profile.did
@ -125,7 +126,9 @@ let ProfileHeaderLabeler = ({
}) })
}, [track, openModal, profile]) }, [track, openModal, profile])
const onPressSubscribe = React.useCallback(async () => { const onPressSubscribe = React.useCallback(
() =>
requireAuth(async () => {
if (!canSubscribe) { if (!canSubscribe) {
cantSubscribePrompt.open() cantSubscribePrompt.open()
return return
@ -139,13 +142,16 @@ let ProfileHeaderLabeler = ({
// setSubscriptionError(e.message) // setSubscriptionError(e.message)
logger.error(`Failed to subscribe to labeler`, {message: e.message}) logger.error(`Failed to subscribe to labeler`, {message: e.message})
} }
}, [ }),
[
requireAuth,
toggleSubscription, toggleSubscription,
isSubscribed, isSubscribed,
profile, profile,
canSubscribe, canSubscribe,
cantSubscribePrompt, cantSubscribePrompt,
]) ],
)
const isMe = React.useMemo( const isMe = React.useMemo(
() => currentAccount?.did === profile.did, () => currentAccount?.did === profile.did,
@ -184,7 +190,6 @@ let ProfileHeaderLabeler = ({
? _(msg`Unsubscribe from this labeler`) ? _(msg`Unsubscribe from this labeler`)
: _(msg`Subscribe to this labeler`) : _(msg`Subscribe to this labeler`)
} }
disabled={!hasSession}
onPress={onPressSubscribe}> onPress={onPressSubscribe}>
{state => ( {state => (
<View <View

View File

@ -220,7 +220,6 @@ let ProfileHeaderStandard = ({
? _(msg`Unfollow ${profile.handle}`) ? _(msg`Unfollow ${profile.handle}`)
: _(msg`Follow ${profile.handle}`) : _(msg`Follow ${profile.handle}`)
} }
disabled={!hasSession}
onPress={ onPress={
profile.viewer?.following ? onPressUnfollow : onPressFollow profile.viewer?.following ? onPressUnfollow : onPressFollow
} }

View File

@ -22,6 +22,7 @@ import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
import {StepHandle} from '#/screens/Signup/StepHandle' import {StepHandle} from '#/screens/Signup/StepHandle'
import {StepInfo} from '#/screens/Signup/StepInfo' import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
@ -212,10 +213,14 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<Divider /> <Divider />
<View style={[a.w_full, a.py_lg]}> <View
<Text style={[t.atoms.text_contrast_medium]}> style={[a.w_full, a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
<AppLanguageDropdown />
<Text style={[t.atoms.text, !gtMobile && a.text_md]}>
<Trans>Having trouble?</Trans>{' '} <Trans>Having trouble?</Trans>{' '}
<InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}> <InlineLinkText
to={FEEDBACK_FORM_URL({email: state.email})}
style={[!gtMobile && a.text_md]}>
<Trans>Contact support</Trans> <Trans>Contact support</Trans>
</InlineLinkText> </InlineLinkText>
</Text> </Text>

View File

@ -17,7 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {getAgent} from '#/state/session' import {getAgent, useSession} from '#/state/session'
import {router} from '#/routes' import {router} from '#/routes'
export type FeedSourceFeedInfo = { export type FeedSourceFeedInfo = {
@ -216,17 +216,38 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeCount: 0, likeCount: 0,
likeUri: '', 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' const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
export function usePinnedFeedsInfos() { export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedUris = preferences?.feeds?.pinned ?? [] const pinnedUris = preferences?.feeds?.pinned ?? []
return useQuery({ return useQuery({
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs, enabled: !isLoadingPrefs,
queryKey: [pinnedFeedInfosQueryKeyRoot, pinnedUris.join(',')], queryKey: [
pinnedFeedInfosQueryKeyRoot,
(hasSession ? 'authed:' : 'unauthed:') + pinnedUris.join(','),
],
queryFn: async () => { queryFn: async () => {
let resolved = new Map() let resolved = new Map()
@ -264,7 +285,7 @@ export function usePinnedFeedsInfos() {
) )
// The returned result will have the original order. // 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]) await Promise.allSettled([feedsPromise, ...listsPromises])
for (let pinnedUri of pinnedUris) { for (let pinnedUri of pinnedUris) {
if (resolved.has(pinnedUri)) { if (resolved.has(pinnedUri)) {

View File

@ -15,8 +15,8 @@ import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {PUBLIC_BSKY_AGENT} from '#/state/queries' import {PUBLIC_BSKY_AGENT} from '#/state/queries'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {IS_DEV} from '#/env' import {IS_DEV} from '#/env'
import {emitSessionDropped} from '../events' import {emitSessionDropped} from '../events'
import {readLabelers} from './agent-config' import {readLabelers} from './agent-config'
@ -702,8 +702,8 @@ export function useSessionApi() {
export function useRequireAuth() { export function useRequireAuth() {
const {hasSession} = useSession() const {hasSession} = useSession()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAll = useCloseAllActiveElements() const closeAll = useCloseAllActiveElements()
const {signinDialogControl} = useGlobalDialogsControlContext()
return React.useCallback( return React.useCallback(
(fn: () => void) => { (fn: () => void) => {
@ -711,10 +711,10 @@ export function useRequireAuth() {
fn() fn()
} else { } else {
closeAll() closeAll()
setShowLoggedOut(true) signinDialogControl.open()
} }
}, },
[hasSession, setShowLoggedOut, closeAll], [hasSession, signinDialogControl, closeAll],
) )
} }

View File

@ -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 (
<ScrollView style={styles.container} testID="loggedOutCTA">
<View style={[styles.hero, isMobile && styles.heroMobile]}>
<Text style={[styles.title, pal.link]}>
<Trans>Bluesky</Trans>
</Text>
<Text
style={[
styles.subtitle,
isMobile && styles.subtitleMobile,
pal.textLight,
]}>
<Trans>See what's next</Trans>
</Text>
</View>
<View
testID="signinOrCreateAccount"
style={isMobile ? undefined : styles.btnsDesktop}>
<TouchableOpacity
testID="createAccountButton"
style={[
styles.btn,
isMobile && styles.btnMobile,
{backgroundColor: colors.blue3},
]}
onPress={showCreateAccount}
accessibilityRole="button"
accessibilityLabel={_(msg`Create new account`)}
accessibilityHint={_(
msg`Opens flow to create a new Bluesky account`,
)}>
<Text
style={[
s.white,
styles.btnLabel,
isMobile && styles.btnLabelMobile,
]}>
<Trans>Create a new account</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="signInButton"
style={[styles.btn, isMobile && styles.btnMobile, pal.btn]}
onPress={showSignIn}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign in`)}
accessibilityHint={_(
msg`Opens flow to sign into your existing Bluesky account`,
)}>
<Text
style={[
pal.text,
styles.btnLabel,
isMobile && styles.btnLabelMobile,
]}>
<Trans>Sign in</Trans>
</Text>
</TouchableOpacity>
</View>
<View style={[styles.footer, pal.view, pal.border]}>
<TextLink
type="2xl"
href="https://bsky.social"
text={_(msg`Business`)}
style={[styles.footerLink, pal.link]}
/>
<TextLink
type="2xl"
href="https://bsky.social/about/blog"
text={_(msg`Blog`)}
style={[styles.footerLink, pal.link]}
/>
<TextLink
type="2xl"
href="https://bsky.social/about/join"
text={_(msg`Jobs`)}
style={[styles.footerLink, pal.link]}
/>
</View>
</ScrollView>
)
}
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: {},
})

View File

@ -1,19 +1,15 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
@ -27,22 +23,8 @@ export const SplashScreen = ({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
const onChangeAppLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
if (!value) return
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
},
[sanitizedLang, setLangPrefs],
)
return ( return (
<CenteredView style={[a.h_full, a.flex_1]}> <CenteredView style={[a.h_full, a.flex_1]}>
<ErrorBoundary> <ErrorBoundary>
@ -99,43 +81,7 @@ export const SplashScreen = ({
a.justify_center, a.justify_center,
a.align_center, a.align_center,
]}> ]}>
<View style={a.relative}> <AppLanguageDropdown />
<RNPickerSelect
placeholder={{}}
value={sanitizedLang}
onValueChange={onChangeAppLanguage}
items={APP_LANGUAGES.filter(l => 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,
},
}}
/>
<View
style={[
a.absolute,
a.inset_0,
{left: 'auto'},
{pointerEvents: 'none'},
a.align_center,
a.justify_center,
]}>
<ChevronDown fill={t.atoms.text.color} size="xs" />
</View>
</View>
</View> </View>
<View style={{height: insets.bottom}} /> <View style={{height: insets.bottom}} />
</ErrorBoundary> </ErrorBoundary>

View File

@ -4,16 +4,13 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
@ -131,23 +128,6 @@ export const SplashScreen = ({
function Footer() { function Footer() {
const t = useTheme() const t = useTheme()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
const onChangeAppLanguage = React.useCallback(
(ev: React.ChangeEvent<HTMLSelectElement>) => {
const value = ev.target.value
if (!value) return
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
},
[sanitizedLang, setLangPrefs],
)
return ( return (
<View <View
style={[ style={[
@ -174,39 +154,7 @@ function Footer() {
<View style={a.flex_1} /> <View style={a.flex_1} />
<View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}> <AppLanguageDropdown />
<Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
{APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
</Text>
<ChevronDown
fill={t.atoms.text.color}
size="xs"
style={a.flex_shrink}
/>
<select
value={sanitizedLang}
onChange={onChangeAppLanguage}
style={{
cursor: 'pointer',
MozAppearance: 'none',
WebkitAppearance: 'none',
appearance: 'none',
position: 'absolute',
inset: 0,
width: '100%',
color: 'transparent',
background: 'transparent',
border: 0,
padding: 0,
}}>
{APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
<option key={l.code2} value={l.code2}>
{l.name}
</option>
))}
</select>
</View>
</View> </View>
) )
} }

View File

@ -1,20 +1,22 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated' 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 { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {CogIcon} from '#/lib/icons' import {CogIcon} from '#/lib/icons'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout' 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: { export function HomeHeaderLayout(props: {
children: React.ReactNode children: React.ReactNode
@ -38,10 +40,12 @@ function HomeHeaderLayoutDesktopAndTablet({
const pal = usePalette('default') const pal = usePalette('default')
const {headerMinimalShellTransform} = useMinimalShellMode() const {headerMinimalShellTransform} = useMinimalShellMode()
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
return ( return (
<> <>
{hasSession && (
<View style={[pal.view, pal.border, styles.bar, styles.topBar]}> <View style={[pal.view, pal.border, styles.bar, styles.topBar]}>
<Link <Link
href="/settings/following-feed" href="/settings/following-feed"
@ -64,6 +68,7 @@ function HomeHeaderLayoutDesktopAndTablet({
<CogIcon size={22} strokeWidth={2} style={pal.textLight} /> <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
</Link> </Link>
</View> </View>
)}
{tabBarAnchor} {tabBarAnchor}
<Animated.View <Animated.View
onLayout={e => { onLayout={e => {

View File

@ -1,23 +1,24 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette' import Animated from 'react-native-reanimated'
import {Link} from '../util/Link'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {HITSLOP_10} from 'lib/constants'
import Animated from 'react-native-reanimated'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useShellLayout} from '#/state/shell/shell-layout' 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 {isWeb} from 'platform/detection'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {IS_DEV} from '#/env'
import {atoms} from '#/alf' import {atoms} from '#/alf'
import {Link as Link2} from '#/components/Link'
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' 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({ export function HomeHeaderLayoutMobile({
children, children,
@ -30,6 +31,7 @@ export function HomeHeaderLayoutMobile({
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {headerMinimalShellTransform} = useMinimalShellMode() const {headerMinimalShellTransform} = useMinimalShellMode()
const {hasSession} = useSession()
const onPressAvi = React.useCallback(() => { const onPressAvi = React.useCallback(() => {
setDrawerOpen(true) setDrawerOpen(true)
@ -76,6 +78,7 @@ export function HomeHeaderLayoutMobile({
<ColorPalette size="md" /> <ColorPalette size="md" />
</Link2> </Link2>
)} )}
{hasSession && (
<Link <Link
testID="viewHeaderHomeFeedPrefsBtn" testID="viewHeaderHomeFeedPrefsBtn"
href="/settings/following-feed" href="/settings/following-feed"
@ -88,6 +91,7 @@ export function HomeHeaderLayoutMobile({
style={pal.textLight as FontAwesomeIconStyle} style={pal.textLight as FontAwesomeIconStyle}
/> />
</Link> </Link>
)}
</View> </View>
</View> </View>
{children} {children}

View File

@ -1,52 +1,53 @@
import React from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
StyleSheet,
View,
type FlatList, type FlatList,
Pressable, Pressable,
StyleSheet,
View,
} from 'react-native' } from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {ViewHeader} from 'view/com/util/ViewHeader' import {msg, Trans} from '@lingui/macro'
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 {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell' import {useFocusEffect} from '@react-navigation/native'
import {usePreferencesQuery} from '#/state/queries/preferences' import debounce from 'lodash.debounce'
import {isNative, isWeb} from '#/platform/detection'
import { import {
getAvatarTypeFromUri,
useFeedSourceInfoQuery, useFeedSourceInfoQuery,
useGetPopularFeedsQuery, useGetPopularFeedsQuery,
useSearchPopularFeedsMutation, useSearchPopularFeedsMutation,
getAvatarTypeFromUri,
} from '#/state/queries/feed' } from '#/state/queries/feed'
import {cleanError} from 'lib/strings/errors' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useComposerControls} from '#/state/shell/composer'
import {useSession} from '#/state/session' 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 {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 {IconCircle} from '#/components/IconCircle'
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
@ -100,6 +101,22 @@ type FlatlistSlice =
key: string 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) { export function FeedsScreen(_props: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
@ -299,7 +316,15 @@ export function FeedsScreen(_props: Props) {
for (const page of popularFeeds.pages || []) { for (const page of popularFeeds.pages || []) {
slices = slices.concat( slices = slices.concat(
page.feeds 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 => ({ .map(feed => ({
key: `popularFeed:${feed.uri}`, key: `popularFeed:${feed.uri}`,
type: 'popularFeed', type: 'popularFeed',

View File

@ -2,6 +2,7 @@ import React from 'react'
import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native' import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {logEvent, LogEvents, useGate} from '#/lib/statsig/statsig' 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 {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
import {HomeHeader} from '../com/home/HomeHeader' import {HomeHeader} from '../com/home/HomeHeader'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
@ -231,7 +231,12 @@ function HomeScreenReady({
onPageSelected={onPageSelected} onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged} onPageScrollStateChanged={onPageScrollStateChanged}
renderTabBar={renderTabBar}> renderTabBar={renderTabBar}>
<HomeLoggedOutCTA /> <FeedPage
testID="customFeedPage"
isPageFocused
feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
renderEmptyState={renderCustomFeedEmptyState}
/>
</Pager> </Pager>
) )
} }

View File

@ -184,8 +184,7 @@ function ProfileScreenLoaded({
const showRepliesTab = hasSession const showRepliesTab = hasSession
const showMediaTab = !hasLabeler const showMediaTab = !hasLabeler
const showLikesTab = isMe const showLikesTab = isMe
const showFeedsTab = const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0
hasSession && (isMe || (profile.associated?.feedgens || 0) > 0)
const showListsTab = const showListsTab =
hasSession && (isMe || (profile.associated?.lists || 0) > 0) hasSession && (isMe || (profile.associated?.lists || 0) > 0)

View File

@ -71,6 +71,7 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
import {ScrollView} from 'view/com/util/Views' import {ScrollView} from 'view/com/util/Views'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {navigate, resetToTab} from '#/Navigation'
import {ExportCarDialog} from './ExportCarDialog' import {ExportCarDialog} from './ExportCarDialog'
function SettingsAccountCard({account}: {account: SessionAccount}) { function SettingsAccountCard({account}: {account: SessionAccount}) {
@ -104,7 +105,14 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
<TouchableOpacity <TouchableOpacity
testID="signOutBtn" testID="signOutBtn"
onPress={() => { onPress={() => {
if (isNative) {
logout('Settings') logout('Settings')
resetToTab('HomeTab')
} else {
navigate('Home').then(() => {
logout('Settings')
})
}
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Sign out`)} accessibilityLabel={_(msg`Sign out`)}

View File

@ -9,49 +9,49 @@ import {
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {useNavigation, StackActions} from '@react-navigation/native'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } 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 {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {usePalette} from 'lib/hooks/usePalette'
import { import {
HomeIcon,
HomeIconSolid,
BellIcon, BellIcon,
BellIconSolid, BellIconSolid,
UserIcon,
CogIcon, CogIcon,
HandIcon,
HashtagIcon,
HomeIcon,
HomeIconSolid,
ListIcon,
MagnifyingGlassIcon2, MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid, MagnifyingGlassIcon2Solid,
UserIcon,
UserIconSolid, UserIconSolid,
HashtagIcon,
ListIcon,
HandIcon,
} from 'lib/icons' } 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 {getTabState, TabState} from 'lib/routes/helpers'
import {NavigationProp} from 'lib/routes/types' 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 {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 {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 {useTheme as useAlfTheme} from '#/alf'
import {TextLink} from '../com/util/Link'
let DrawerProfileCard = ({ let DrawerProfileCard = ({
account, account,
@ -246,7 +246,11 @@ let DrawerContent = ({}: {}): React.ReactNode => {
<SettingsMenuItem onPress={onPressSettings} /> <SettingsMenuItem onPress={onPressSettings} />
</> </>
) : ( ) : (
<>
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
<FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} /> <SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
</>
)} )}
<View style={styles.smallSpacer} /> <View style={styles.smallSpacer} />

View File

@ -3,13 +3,16 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util' 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 {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 => { let NavSignupCard = ({}: {}): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
@ -35,7 +38,9 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
paddingTop: 6, paddingTop: 6,
marginBottom: 24, marginBottom: 24,
}}> }}>
<Link to="/" label="Bluesky - Home">
<Logo width={48} /> <Logo width={48} />
</Link>
<View style={{paddingTop: 18}}> <View style={{paddingTop: 18}}>
<Text type="md-bold" style={[pal.text]}> <Text type="md-bold" style={[pal.text]}>
@ -62,6 +67,10 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
</Text> </Text>
</Button> </Button>
</View> </View>
<View style={[a.pt_2xl, a.w_full]}>
<AppLanguageDropdown />
</View>
</View> </View>
) )
} }

View File

@ -1,37 +1,39 @@
import React from 'react' import React from 'react'
import {StatusBar} from 'expo-status-bar'
import { import {
BackHandler,
DimensionValue, DimensionValue,
StyleSheet, StyleSheet,
useWindowDimensions, useWindowDimensions,
View, View,
BackHandler,
} from 'react-native' } from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Drawer} from 'react-native-drawer-layout' 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 {useNavigationState} from '@react-navigation/native'
import {ModalsContainer} from 'view/com/modals/Modal'
import {Lightbox} from 'view/com/lightbox/Lightbox' import {useSession} from '#/state/session'
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 { import {
useIsDrawerOpen, useIsDrawerOpen,
useSetDrawerOpen,
useIsDrawerSwipeDisabled, useIsDrawerSwipeDisabled,
useSetDrawerOpen,
} from '#/state/shell' } from '#/state/shell'
import {isAndroid} from 'platform/detection'
import {useSession} from '#/state/session'
import {useCloseAnyActiveElement} from '#/state/util' import {useCloseAnyActiveElement} from '#/state/util'
import {usePalette} from 'lib/hooks/usePalette'
import * as notifications from 'lib/notifications/notifications' import * as notifications from 'lib/notifications/notifications'
import {Outlet as PortalOutlet} from '#/components/Portal' import {isStateAtTabRoot} from 'lib/routes/helpers'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {useTheme} from 'lib/ThemeContext'
import {isAndroid} from 'platform/detection'
import {useDialogStateContext} from 'state/dialogs' 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() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
@ -101,6 +103,7 @@ function ShellInner() {
<Composer winHeight={winDim.height} /> <Composer winHeight={winDim.height} />
<ModalsContainer /> <ModalsContainer />
<MutedWordsDialog /> <MutedWordsDialog />
<SigninDialog />
<Lightbox /> <Lightbox />
<PortalOutlet /> <PortalOutlet />
</> </>

View File

@ -1,24 +1,25 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {View, StyleSheet, TouchableOpacity} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {ErrorBoundary} from '../com/util/ErrorBoundary' import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {Lightbox} from '../com/lightbox/Lightbox'
import {ModalsContainer} from '../com/modals/Modal'
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 {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Outlet as PortalOutlet} from '#/components/Portal' import {NavigationProp} from 'lib/routes/types'
import {colors, s} from 'lib/styles'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 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 {DrawerContent} from './Drawer'
function ShellInner() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
@ -45,6 +46,7 @@ function ShellInner() {
<Composer winHeight={0} /> <Composer winHeight={0} />
<ModalsContainer /> <ModalsContainer />
<MutedWordsDialog /> <MutedWordsDialog />
<SigninDialog />
<Lightbox /> <Lightbox />
<PortalOutlet /> <PortalOutlet />