Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Paul Frazee
dan
and committed by
GitHub
ec5c4929 44039c68

+518 -477
+5 -13
src/Navigation.tsx
··· 193 193 <Stack.Screen 194 194 name="ProfileFeed" 195 195 getComponent={() => ProfileFeedScreen} 196 - options={{title: title(msg`Feed`), requireAuth: true}} 196 + options={{title: title(msg`Feed`)}} 197 197 /> 198 198 <Stack.Screen 199 199 name="ProfileFeedLikedBy" ··· 331 331 animationDuration: 250, 332 332 contentStyle: pal.view, 333 333 }}> 334 - <HomeTab.Screen 335 - name="Home" 336 - getComponent={() => HomeScreen} 337 - options={{requireAuth: true}} 338 - /> 334 + <HomeTab.Screen name="Home" getComponent={() => HomeScreen} /> 339 335 {commonScreens(HomeTab)} 340 336 </HomeTab.Navigator> 341 337 ) ··· 371 367 animationDuration: 250, 372 368 contentStyle: pal.view, 373 369 }}> 374 - <FeedsTab.Screen 375 - name="Feeds" 376 - getComponent={() => FeedsScreen} 377 - options={{requireAuth: true}} 378 - /> 370 + <FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} /> 379 371 {commonScreens(FeedsTab as typeof HomeTab)} 380 372 </FeedsTab.Navigator> 381 373 ) ··· 451 443 <Flat.Screen 452 444 name="Home" 453 445 getComponent={() => HomeScreen} 454 - options={{title: title(msg`Home`), requireAuth: true}} 446 + options={{title: title(msg`Home`)}} 455 447 /> 456 448 <Flat.Screen 457 449 name="Search" ··· 461 453 <Flat.Screen 462 454 name="Feeds" 463 455 getComponent={() => FeedsScreen} 464 - options={{title: title(msg`Feeds`), requireAuth: true}} 456 + options={{title: title(msg`Feeds`)}} 465 457 /> 466 458 <Flat.Screen 467 459 name="Notifications"
+67
src/components/AppLanguageDropdown.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' 4 + 5 + import {sanitizeAppLanguageSetting} from '#/locale/helpers' 6 + import {APP_LANGUAGES} from '#/locale/languages' 7 + import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 8 + import {atoms as a, useTheme} from '#/alf' 9 + import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 10 + 11 + export function AppLanguageDropdown() { 12 + const t = useTheme() 13 + 14 + const langPrefs = useLanguagePrefs() 15 + const setLangPrefs = useLanguagePrefsApi() 16 + const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) 17 + 18 + const onChangeAppLanguage = React.useCallback( 19 + (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 20 + if (!value) return 21 + if (sanitizedLang !== value) { 22 + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 23 + } 24 + }, 25 + [sanitizedLang, setLangPrefs], 26 + ) 27 + 28 + return ( 29 + <View style={a.relative}> 30 + <RNPickerSelect 31 + placeholder={{}} 32 + value={sanitizedLang} 33 + onValueChange={onChangeAppLanguage} 34 + items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 35 + label: l.name, 36 + value: l.code2, 37 + key: l.code2, 38 + }))} 39 + useNativeAndroidPickerStyle={false} 40 + style={{ 41 + inputAndroid: { 42 + color: t.atoms.text_contrast_medium.color, 43 + fontSize: 16, 44 + paddingRight: 12 + 4, 45 + }, 46 + inputIOS: { 47 + color: t.atoms.text.color, 48 + fontSize: 16, 49 + paddingRight: 12 + 4, 50 + }, 51 + }} 52 + /> 53 + 54 + <View 55 + style={[ 56 + a.absolute, 57 + a.inset_0, 58 + {left: 'auto'}, 59 + {pointerEvents: 'none'}, 60 + a.align_center, 61 + a.justify_center, 62 + ]}> 63 + <ChevronDown fill={t.atoms.text.color} size="xs" /> 64 + </View> 65 + </View> 66 + ) 67 + }
+62
src/components/AppLanguageDropdown.web.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {sanitizeAppLanguageSetting} from '#/locale/helpers' 5 + import {APP_LANGUAGES} from '#/locale/languages' 6 + import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 7 + import {atoms as a, useTheme} from '#/alf' 8 + import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 9 + import {Text} from '#/components/Typography' 10 + 11 + export function AppLanguageDropdown() { 12 + const t = useTheme() 13 + 14 + const langPrefs = useLanguagePrefs() 15 + const setLangPrefs = useLanguagePrefsApi() 16 + 17 + const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) 18 + 19 + const onChangeAppLanguage = React.useCallback( 20 + (ev: React.ChangeEvent<HTMLSelectElement>) => { 21 + const value = ev.target.value 22 + 23 + if (!value) return 24 + if (sanitizedLang !== value) { 25 + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 26 + } 27 + }, 28 + [sanitizedLang, setLangPrefs], 29 + ) 30 + 31 + return ( 32 + <View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}> 33 + <Text aria-hidden={true} style={t.atoms.text_contrast_medium}> 34 + {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name} 35 + </Text> 36 + <ChevronDown fill={t.atoms.text.color} size="xs" style={a.flex_shrink} /> 37 + 38 + <select 39 + value={sanitizedLang} 40 + onChange={onChangeAppLanguage} 41 + style={{ 42 + cursor: 'pointer', 43 + MozAppearance: 'none', 44 + WebkitAppearance: 'none', 45 + appearance: 'none', 46 + position: 'absolute', 47 + inset: 0, 48 + width: '100%', 49 + color: 'transparent', 50 + background: 'transparent', 51 + border: 0, 52 + padding: 0, 53 + }}> 54 + {APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ( 55 + <option key={l.code2} value={l.code2}> 56 + {l.name} 57 + </option> 58 + ))} 59 + </select> 60 + </View> 61 + ) 62 + }
+5 -2
src/components/dialogs/Context.tsx
··· 6 6 7 7 type ControlsContext = { 8 8 mutedWordsDialogControl: Control 9 + signinDialogControl: Control 9 10 } 10 11 11 12 const ControlsContext = React.createContext({ 12 13 mutedWordsDialogControl: {} as Control, 14 + signinDialogControl: {} as Control, 13 15 }) 14 16 15 17 export function useGlobalDialogsControlContext() { ··· 18 20 19 21 export function Provider({children}: React.PropsWithChildren<{}>) { 20 22 const mutedWordsDialogControl = Dialog.useDialogControl() 23 + const signinDialogControl = Dialog.useDialogControl() 21 24 const ctx = React.useMemo<ControlsContext>( 22 - () => ({mutedWordsDialogControl}), 23 - [mutedWordsDialogControl], 25 + () => ({mutedWordsDialogControl, signinDialogControl}), 26 + [mutedWordsDialogControl, signinDialogControl], 24 27 ) 25 28 26 29 return (
+99
src/components/dialogs/Signin.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {isNative} from '#/platform/detection' 7 + import {useLoggedOutViewControls} from '#/state/shell/logged-out' 8 + import {useCloseAllActiveElements} from '#/state/util' 9 + import {Logo} from '#/view/icons/Logo' 10 + import {Logotype} from '#/view/icons/Logotype' 11 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 12 + import {Button, ButtonText} from '#/components/Button' 13 + import * as Dialog from '#/components/Dialog' 14 + import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 15 + import {Text} from '#/components/Typography' 16 + 17 + export function SigninDialog() { 18 + const {signinDialogControl: control} = useGlobalDialogsControlContext() 19 + return ( 20 + <Dialog.Outer control={control}> 21 + <Dialog.Handle /> 22 + <SigninDialogInner control={control} /> 23 + </Dialog.Outer> 24 + ) 25 + } 26 + 27 + function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) { 28 + const t = useTheme() 29 + const {_} = useLingui() 30 + const {gtMobile} = useBreakpoints() 31 + const {requestSwitchToAccount} = useLoggedOutViewControls() 32 + const closeAllActiveElements = useCloseAllActiveElements() 33 + 34 + const showSignIn = React.useCallback(() => { 35 + closeAllActiveElements() 36 + requestSwitchToAccount({requestedAccount: 'none'}) 37 + }, [requestSwitchToAccount, closeAllActiveElements]) 38 + 39 + const showCreateAccount = React.useCallback(() => { 40 + closeAllActiveElements() 41 + requestSwitchToAccount({requestedAccount: 'new'}) 42 + }, [requestSwitchToAccount, closeAllActiveElements]) 43 + 44 + return ( 45 + <Dialog.ScrollableInner 46 + label={_(msg`Sign into Bluesky or create a new account`)} 47 + style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}> 48 + <View> 49 + <View 50 + style={[ 51 + a.flex_row, 52 + a.align_center, 53 + a.justify_center, 54 + a.gap_sm, 55 + a.pb_lg, 56 + ]}> 57 + <Logo width={36} /> 58 + <View style={{paddingTop: 6}}> 59 + <Logotype width={120} fill={t.atoms.text.color} /> 60 + </View> 61 + </View> 62 + 63 + <Text style={[a.text_lg, a.text_center, t.atoms.text, a.pb_2xl]}> 64 + <Trans> 65 + Sign in or create your account to join the conversation! 66 + </Trans> 67 + </Text> 68 + 69 + <View style={[a.flex_col, a.gap_md]}> 70 + <Button 71 + variant="solid" 72 + color="primary" 73 + size="large" 74 + onPress={showCreateAccount} 75 + label={_(msg`Create an account`)}> 76 + <ButtonText> 77 + <Trans>Create an account</Trans> 78 + </ButtonText> 79 + </Button> 80 + 81 + <Button 82 + variant="solid" 83 + color="secondary" 84 + size="large" 85 + onPress={showSignIn} 86 + label={_(msg`Sign in`)}> 87 + <ButtonText> 88 + <Trans>Sign in</Trans> 89 + </ButtonText> 90 + </Button> 91 + </View> 92 + 93 + {isNative && <View style={{height: 10}} />} 94 + </View> 95 + 96 + <Dialog.Close /> 97 + </Dialog.ScrollableInner> 98 + ) 99 + }
+28 -23
src/screens/Profile/Header/ProfileHeaderLabeler.tsx
··· 18 18 import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' 19 19 import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' 20 20 import {usePreferencesQuery} from '#/state/queries/preferences' 21 - import {useSession} from '#/state/session' 21 + import {useRequireAuth, useSession} from '#/state/session' 22 22 import {useAnalytics} from 'lib/analytics/analytics' 23 23 import {useHaptics} from 'lib/haptics' 24 24 import {useProfileShadow} from 'state/cache/profile-shadow' ··· 64 64 const {currentAccount, hasSession} = useSession() 65 65 const {openModal} = useModalControls() 66 66 const {track} = useAnalytics() 67 + const requireAuth = useRequireAuth() 67 68 const playHaptic = useHaptics() 68 69 const cantSubscribePrompt = Prompt.usePromptControl() 69 70 const isSelf = currentAccount?.did === profile.did ··· 125 126 }) 126 127 }, [track, openModal, profile]) 127 128 128 - const onPressSubscribe = React.useCallback(async () => { 129 - if (!canSubscribe) { 130 - cantSubscribePrompt.open() 131 - return 132 - } 133 - try { 134 - await toggleSubscription({ 135 - did: profile.did, 136 - subscribe: !isSubscribed, 137 - }) 138 - } catch (e: any) { 139 - // setSubscriptionError(e.message) 140 - logger.error(`Failed to subscribe to labeler`, {message: e.message}) 141 - } 142 - }, [ 143 - toggleSubscription, 144 - isSubscribed, 145 - profile, 146 - canSubscribe, 147 - cantSubscribePrompt, 148 - ]) 129 + const onPressSubscribe = React.useCallback( 130 + () => 131 + requireAuth(async () => { 132 + if (!canSubscribe) { 133 + cantSubscribePrompt.open() 134 + return 135 + } 136 + try { 137 + await toggleSubscription({ 138 + did: profile.did, 139 + subscribe: !isSubscribed, 140 + }) 141 + } catch (e: any) { 142 + // setSubscriptionError(e.message) 143 + logger.error(`Failed to subscribe to labeler`, {message: e.message}) 144 + } 145 + }), 146 + [ 147 + requireAuth, 148 + toggleSubscription, 149 + isSubscribed, 150 + profile, 151 + canSubscribe, 152 + cantSubscribePrompt, 153 + ], 154 + ) 149 155 150 156 const isMe = React.useMemo( 151 157 () => currentAccount?.did === profile.did, ··· 184 190 ? _(msg`Unsubscribe from this labeler`) 185 191 : _(msg`Subscribe to this labeler`) 186 192 } 187 - disabled={!hasSession} 188 193 onPress={onPressSubscribe}> 189 194 {state => ( 190 195 <View
-1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 220 220 ? _(msg`Unfollow ${profile.handle}`) 221 221 : _(msg`Follow ${profile.handle}`) 222 222 } 223 - disabled={!hasSession} 224 223 onPress={ 225 224 profile.viewer?.following ? onPressUnfollow : onPressFollow 226 225 }
+8 -3
src/screens/Signup/index.tsx
··· 22 22 import {StepHandle} from '#/screens/Signup/StepHandle' 23 23 import {StepInfo} from '#/screens/Signup/StepInfo' 24 24 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 25 + import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 25 26 import {Button, ButtonText} from '#/components/Button' 26 27 import {Divider} from '#/components/Divider' 27 28 import {InlineLinkText} from '#/components/Link' ··· 212 213 213 214 <Divider /> 214 215 215 - <View style={[a.w_full, a.py_lg]}> 216 - <Text style={[t.atoms.text_contrast_medium]}> 216 + <View 217 + style={[a.w_full, a.py_lg, a.flex_row, a.gap_lg, a.align_center]}> 218 + <AppLanguageDropdown /> 219 + <Text style={[t.atoms.text, !gtMobile && a.text_md]}> 217 220 <Trans>Having trouble?</Trans>{' '} 218 - <InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}> 221 + <InlineLinkText 222 + to={FEEDBACK_FORM_URL({email: state.email})} 223 + style={[!gtMobile && a.text_md]}> 219 224 <Trans>Contact support</Trans> 220 225 </InlineLinkText> 221 226 </Text>
+24 -3
src/state/queries/feed.ts
··· 17 17 import {sanitizeHandle} from '#/lib/strings/handles' 18 18 import {STALE} from '#/state/queries' 19 19 import {usePreferencesQuery} from '#/state/queries/preferences' 20 - import {getAgent} from '#/state/session' 20 + import {getAgent, useSession} from '#/state/session' 21 21 import {router} from '#/routes' 22 22 23 23 export type FeedSourceFeedInfo = { ··· 216 216 likeCount: 0, 217 217 likeUri: '', 218 218 } 219 + const DISCOVER_FEED_STUB: FeedSourceInfo = { 220 + type: 'feed', 221 + displayName: 'Discover', 222 + uri: '', 223 + route: { 224 + href: '/', 225 + name: 'Home', 226 + params: {}, 227 + }, 228 + cid: '', 229 + avatar: '', 230 + description: new RichText({text: ''}), 231 + creatorDid: '', 232 + creatorHandle: '', 233 + likeCount: 0, 234 + likeUri: '', 235 + } 219 236 220 237 const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos' 221 238 222 239 export function usePinnedFeedsInfos() { 240 + const {hasSession} = useSession() 223 241 const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() 224 242 const pinnedUris = preferences?.feeds?.pinned ?? [] 225 243 226 244 return useQuery({ 227 245 staleTime: STALE.INFINITY, 228 246 enabled: !isLoadingPrefs, 229 - queryKey: [pinnedFeedInfosQueryKeyRoot, pinnedUris.join(',')], 247 + queryKey: [ 248 + pinnedFeedInfosQueryKeyRoot, 249 + (hasSession ? 'authed:' : 'unauthed:') + pinnedUris.join(','), 250 + ], 230 251 queryFn: async () => { 231 252 let resolved = new Map() 232 253 ··· 264 285 ) 265 286 266 287 // The returned result will have the original order. 267 - const result = [FOLLOWING_FEED_STUB] 288 + const result = [hasSession ? FOLLOWING_FEED_STUB : DISCOVER_FEED_STUB] 268 289 await Promise.allSettled([feedsPromise, ...listsPromises]) 269 290 for (let pinnedUri of pinnedUris) { 270 291 if (resolved.has(pinnedUri)) {
+4 -4
src/state/session/index.tsx
··· 15 15 import {isWeb} from '#/platform/detection' 16 16 import * as persisted from '#/state/persisted' 17 17 import {PUBLIC_BSKY_AGENT} from '#/state/queries' 18 - import {useLoggedOutViewControls} from '#/state/shell/logged-out' 19 18 import {useCloseAllActiveElements} from '#/state/util' 19 + import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 20 20 import {IS_DEV} from '#/env' 21 21 import {emitSessionDropped} from '../events' 22 22 import {readLabelers} from './agent-config' ··· 702 702 703 703 export function useRequireAuth() { 704 704 const {hasSession} = useSession() 705 - const {setShowLoggedOut} = useLoggedOutViewControls() 706 705 const closeAll = useCloseAllActiveElements() 706 + const {signinDialogControl} = useGlobalDialogsControlContext() 707 707 708 708 return React.useCallback( 709 709 (fn: () => void) => { ··· 711 711 fn() 712 712 } else { 713 713 closeAll() 714 - setShowLoggedOut(true) 714 + signinDialogControl.open() 715 715 } 716 716 }, 717 - [hasSession, setShowLoggedOut, closeAll], 717 + [hasSession, signinDialogControl, closeAll], 718 718 ) 719 719 } 720 720
-170
src/view/com/auth/HomeLoggedOutCTA.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {usePalette} from '#/lib/hooks/usePalette' 7 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 - import {colors, s} from '#/lib/styles' 9 - import {useLoggedOutViewControls} from '#/state/shell/logged-out' 10 - import {TextLink} from '../util/Link' 11 - import {Text} from '../util/text/Text' 12 - import {ScrollView} from '../util/Views' 13 - 14 - export function HomeLoggedOutCTA() { 15 - const pal = usePalette('default') 16 - const {_} = useLingui() 17 - const {isMobile} = useWebMediaQueries() 18 - const {requestSwitchToAccount} = useLoggedOutViewControls() 19 - 20 - const showCreateAccount = React.useCallback(() => { 21 - requestSwitchToAccount({requestedAccount: 'new'}) 22 - }, [requestSwitchToAccount]) 23 - 24 - const showSignIn = React.useCallback(() => { 25 - requestSwitchToAccount({requestedAccount: 'none'}) 26 - }, [requestSwitchToAccount]) 27 - 28 - return ( 29 - <ScrollView style={styles.container} testID="loggedOutCTA"> 30 - <View style={[styles.hero, isMobile && styles.heroMobile]}> 31 - <Text style={[styles.title, pal.link]}> 32 - <Trans>Bluesky</Trans> 33 - </Text> 34 - <Text 35 - style={[ 36 - styles.subtitle, 37 - isMobile && styles.subtitleMobile, 38 - pal.textLight, 39 - ]}> 40 - <Trans>See what's next</Trans> 41 - </Text> 42 - </View> 43 - <View 44 - testID="signinOrCreateAccount" 45 - style={isMobile ? undefined : styles.btnsDesktop}> 46 - <TouchableOpacity 47 - testID="createAccountButton" 48 - style={[ 49 - styles.btn, 50 - isMobile && styles.btnMobile, 51 - {backgroundColor: colors.blue3}, 52 - ]} 53 - onPress={showCreateAccount} 54 - accessibilityRole="button" 55 - accessibilityLabel={_(msg`Create new account`)} 56 - accessibilityHint={_( 57 - msg`Opens flow to create a new Bluesky account`, 58 - )}> 59 - <Text 60 - style={[ 61 - s.white, 62 - styles.btnLabel, 63 - isMobile && styles.btnLabelMobile, 64 - ]}> 65 - <Trans>Create a new account</Trans> 66 - </Text> 67 - </TouchableOpacity> 68 - <TouchableOpacity 69 - testID="signInButton" 70 - style={[styles.btn, isMobile && styles.btnMobile, pal.btn]} 71 - onPress={showSignIn} 72 - accessibilityRole="button" 73 - accessibilityLabel={_(msg`Sign in`)} 74 - accessibilityHint={_( 75 - msg`Opens flow to sign into your existing Bluesky account`, 76 - )}> 77 - <Text 78 - style={[ 79 - pal.text, 80 - styles.btnLabel, 81 - isMobile && styles.btnLabelMobile, 82 - ]}> 83 - <Trans>Sign in</Trans> 84 - </Text> 85 - </TouchableOpacity> 86 - </View> 87 - 88 - <View style={[styles.footer, pal.view, pal.border]}> 89 - <TextLink 90 - type="2xl" 91 - href="https://bsky.social" 92 - text={_(msg`Business`)} 93 - style={[styles.footerLink, pal.link]} 94 - /> 95 - <TextLink 96 - type="2xl" 97 - href="https://bsky.social/about/blog" 98 - text={_(msg`Blog`)} 99 - style={[styles.footerLink, pal.link]} 100 - /> 101 - <TextLink 102 - type="2xl" 103 - href="https://bsky.social/about/join" 104 - text={_(msg`Jobs`)} 105 - style={[styles.footerLink, pal.link]} 106 - /> 107 - </View> 108 - </ScrollView> 109 - ) 110 - } 111 - 112 - const styles = StyleSheet.create({ 113 - container: { 114 - height: '100%', 115 - }, 116 - hero: { 117 - justifyContent: 'center', 118 - paddingTop: 100, 119 - paddingBottom: 30, 120 - }, 121 - heroMobile: { 122 - paddingBottom: 50, 123 - }, 124 - title: { 125 - textAlign: 'center', 126 - fontSize: 68, 127 - fontWeight: 'bold', 128 - }, 129 - subtitle: { 130 - textAlign: 'center', 131 - fontSize: 48, 132 - fontWeight: 'bold', 133 - }, 134 - subtitleMobile: { 135 - fontSize: 42, 136 - }, 137 - btnsDesktop: { 138 - flexDirection: 'row', 139 - justifyContent: 'center', 140 - gap: 20, 141 - marginHorizontal: 20, 142 - }, 143 - btn: { 144 - borderRadius: 32, 145 - width: 230, 146 - paddingVertical: 12, 147 - marginBottom: 20, 148 - }, 149 - btnMobile: { 150 - flex: 1, 151 - width: 'auto', 152 - marginHorizontal: 20, 153 - paddingVertical: 16, 154 - }, 155 - btnLabel: { 156 - textAlign: 'center', 157 - fontSize: 18, 158 - }, 159 - btnLabelMobile: { 160 - textAlign: 'center', 161 - fontSize: 21, 162 - }, 163 - 164 - footer: { 165 - flexDirection: 'row', 166 - gap: 20, 167 - justifyContent: 'center', 168 - }, 169 - footerLink: {}, 170 - })
+2 -56
src/view/com/auth/SplashScreen.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 - import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' 4 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 4 import {msg, Trans} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 6 8 - import {sanitizeAppLanguageSetting} from '#/locale/helpers' 9 - import {APP_LANGUAGES} from '#/locale/languages' 10 - import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 11 7 import {Logo} from '#/view/icons/Logo' 12 8 import {Logotype} from '#/view/icons/Logotype' 13 9 import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 14 10 import {atoms as a, useTheme} from '#/alf' 11 + import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 15 12 import {Button, ButtonText} from '#/components/Button' 16 - import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 17 13 import {Text} from '#/components/Typography' 18 14 import {CenteredView} from '../util/Views' 19 15 ··· 27 23 const t = useTheme() 28 24 const {_} = useLingui() 29 25 30 - const langPrefs = useLanguagePrefs() 31 - const setLangPrefs = useLanguagePrefsApi() 32 26 const insets = useSafeAreaInsets() 33 - 34 - const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) 35 - 36 - const onChangeAppLanguage = React.useCallback( 37 - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 38 - if (!value) return 39 - if (sanitizedLang !== value) { 40 - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 41 - } 42 - }, 43 - [sanitizedLang, setLangPrefs], 44 - ) 45 27 46 28 return ( 47 29 <CenteredView style={[a.h_full, a.flex_1]}> ··· 99 81 a.justify_center, 100 82 a.align_center, 101 83 ]}> 102 - <View style={a.relative}> 103 - <RNPickerSelect 104 - placeholder={{}} 105 - value={sanitizedLang} 106 - onValueChange={onChangeAppLanguage} 107 - items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 108 - label: l.name, 109 - value: l.code2, 110 - key: l.code2, 111 - }))} 112 - useNativeAndroidPickerStyle={false} 113 - style={{ 114 - inputAndroid: { 115 - color: t.atoms.text_contrast_medium.color, 116 - fontSize: 16, 117 - paddingRight: 12 + 4, 118 - }, 119 - inputIOS: { 120 - color: t.atoms.text.color, 121 - fontSize: 16, 122 - paddingRight: 12 + 4, 123 - }, 124 - }} 125 - /> 126 - 127 - <View 128 - style={[ 129 - a.absolute, 130 - a.inset_0, 131 - {left: 'auto'}, 132 - {pointerEvents: 'none'}, 133 - a.align_center, 134 - a.justify_center, 135 - ]}> 136 - <ChevronDown fill={t.atoms.text.color} size="xs" /> 137 - </View> 138 - </View> 84 + <AppLanguageDropdown /> 139 85 </View> 140 86 <View style={{height: insets.bottom}} /> 141 87 </ErrorBoundary>
+2 -54
src/view/com/auth/SplashScreen.web.tsx
··· 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 - import {sanitizeAppLanguageSetting} from '#/locale/helpers' 8 - import {APP_LANGUAGES} from '#/locale/languages' 9 - import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 10 7 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 11 8 import {Logo} from '#/view/icons/Logo' 12 9 import {Logotype} from '#/view/icons/Logotype' 13 10 import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 14 11 import {atoms as a, useTheme} from '#/alf' 12 + import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 15 13 import {Button, ButtonText} from '#/components/Button' 16 - import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 17 14 import {InlineLinkText} from '#/components/Link' 18 15 import {Text} from '#/components/Typography' 19 16 import {CenteredView} from '../util/Views' ··· 131 128 function Footer() { 132 129 const t = useTheme() 133 130 134 - const langPrefs = useLanguagePrefs() 135 - const setLangPrefs = useLanguagePrefsApi() 136 - 137 - const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) 138 - 139 - const onChangeAppLanguage = React.useCallback( 140 - (ev: React.ChangeEvent<HTMLSelectElement>) => { 141 - const value = ev.target.value 142 - 143 - if (!value) return 144 - if (sanitizedLang !== value) { 145 - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 146 - } 147 - }, 148 - [sanitizedLang, setLangPrefs], 149 - ) 150 - 151 131 return ( 152 132 <View 153 133 style={[ ··· 174 154 175 155 <View style={a.flex_1} /> 176 156 177 - <View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}> 178 - <Text aria-hidden={true} style={t.atoms.text_contrast_medium}> 179 - {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name} 180 - </Text> 181 - <ChevronDown 182 - fill={t.atoms.text.color} 183 - size="xs" 184 - style={a.flex_shrink} 185 - /> 186 - 187 - <select 188 - value={sanitizedLang} 189 - onChange={onChangeAppLanguage} 190 - style={{ 191 - cursor: 'pointer', 192 - MozAppearance: 'none', 193 - WebkitAppearance: 'none', 194 - appearance: 'none', 195 - position: 'absolute', 196 - inset: 0, 197 - width: '100%', 198 - color: 'transparent', 199 - background: 'transparent', 200 - border: 0, 201 - padding: 0, 202 - }}> 203 - {APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ( 204 - <option key={l.code2} value={l.code2}> 205 - {l.name} 206 - </option> 207 - ))} 208 - </select> 209 - </View> 157 + <AppLanguageDropdown /> 210 158 </View> 211 159 ) 212 160 }
+34 -29
src/view/com/home/HomeHeaderLayout.web.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 3 import Animated from 'react-native-reanimated' 4 - import {usePalette} from 'lib/hooks/usePalette' 5 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 6 - import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' 7 - import {Logo} from '#/view/icons/Logo' 8 - import {Link} from '../util/Link' 9 4 import { 10 5 FontAwesomeIcon, 11 6 FontAwesomeIconStyle, 12 7 } from '@fortawesome/react-native-fontawesome' 8 + import {msg} from '@lingui/macro' 13 9 import {useLingui} from '@lingui/react' 14 - import {msg} from '@lingui/macro' 10 + 15 11 import {CogIcon} from '#/lib/icons' 16 - import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 12 + import {useSession} from '#/state/session' 17 13 import {useShellLayout} from '#/state/shell/shell-layout' 14 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 15 + import {usePalette} from 'lib/hooks/usePalette' 16 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 17 + import {Logo} from '#/view/icons/Logo' 18 + import {Link} from '../util/Link' 19 + import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' 18 20 19 21 export function HomeHeaderLayout(props: { 20 22 children: React.ReactNode ··· 38 40 const pal = usePalette('default') 39 41 const {headerMinimalShellTransform} = useMinimalShellMode() 40 42 const {headerHeight} = useShellLayout() 43 + const {hasSession} = useSession() 41 44 const {_} = useLingui() 42 45 43 46 return ( 44 47 <> 45 - <View style={[pal.view, pal.border, styles.bar, styles.topBar]}> 46 - <Link 47 - href="/settings/following-feed" 48 - hitSlop={10} 49 - accessibilityRole="button" 50 - accessibilityLabel={_(msg`Following Feed Preferences`)} 51 - accessibilityHint=""> 52 - <FontAwesomeIcon 53 - icon="sliders" 54 - style={pal.textLight as FontAwesomeIconStyle} 55 - /> 56 - </Link> 57 - <Logo width={28} /> 58 - <Link 59 - href="/settings/saved-feeds" 60 - hitSlop={10} 61 - accessibilityRole="button" 62 - accessibilityLabel={_(msg`Edit Saved Feeds`)} 63 - accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}> 64 - <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> 65 - </Link> 66 - </View> 48 + {hasSession && ( 49 + <View style={[pal.view, pal.border, styles.bar, styles.topBar]}> 50 + <Link 51 + href="/settings/following-feed" 52 + hitSlop={10} 53 + accessibilityRole="button" 54 + accessibilityLabel={_(msg`Following Feed Preferences`)} 55 + accessibilityHint=""> 56 + <FontAwesomeIcon 57 + icon="sliders" 58 + style={pal.textLight as FontAwesomeIconStyle} 59 + /> 60 + </Link> 61 + <Logo width={28} /> 62 + <Link 63 + href="/settings/saved-feeds" 64 + hitSlop={10} 65 + accessibilityRole="button" 66 + accessibilityLabel={_(msg`Edit Saved Feeds`)} 67 + accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}> 68 + <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> 69 + </Link> 70 + </View> 71 + )} 67 72 {tabBarAnchor} 68 73 <Animated.View 69 74 onLayout={e => {
+24 -20
src/view/com/home/HomeHeaderLayoutMobile.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {usePalette} from 'lib/hooks/usePalette' 4 - import {Link} from '../util/Link' 3 + import Animated from 'react-native-reanimated' 5 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 5 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 7 - import {HITSLOP_10} from 'lib/constants' 8 - import Animated from 'react-native-reanimated' 9 6 import {msg} from '@lingui/macro' 10 7 import {useLingui} from '@lingui/react' 11 - import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 8 + 9 + import {useSession} from '#/state/session' 12 10 import {useSetDrawerOpen} from '#/state/shell/drawer-open' 13 11 import {useShellLayout} from '#/state/shell/shell-layout' 12 + import {HITSLOP_10} from 'lib/constants' 13 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 14 + import {usePalette} from 'lib/hooks/usePalette' 14 15 import {isWeb} from 'platform/detection' 15 16 import {Logo} from '#/view/icons/Logo' 16 - 17 - import {IS_DEV} from '#/env' 18 17 import {atoms} from '#/alf' 19 - import {Link as Link2} from '#/components/Link' 20 18 import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' 19 + import {Link as Link2} from '#/components/Link' 20 + import {IS_DEV} from '#/env' 21 + import {Link} from '../util/Link' 21 22 22 23 export function HomeHeaderLayoutMobile({ 23 24 children, ··· 30 31 const setDrawerOpen = useSetDrawerOpen() 31 32 const {headerHeight} = useShellLayout() 32 33 const {headerMinimalShellTransform} = useMinimalShellMode() 34 + const {hasSession} = useSession() 33 35 34 36 const onPressAvi = React.useCallback(() => { 35 37 setDrawerOpen(true) ··· 76 78 <ColorPalette size="md" /> 77 79 </Link2> 78 80 )} 79 - <Link 80 - testID="viewHeaderHomeFeedPrefsBtn" 81 - href="/settings/following-feed" 82 - hitSlop={HITSLOP_10} 83 - accessibilityRole="button" 84 - accessibilityLabel={_(msg`Following Feed Preferences`)} 85 - accessibilityHint=""> 86 - <FontAwesomeIcon 87 - icon="sliders" 88 - style={pal.textLight as FontAwesomeIconStyle} 89 - /> 90 - </Link> 81 + {hasSession && ( 82 + <Link 83 + testID="viewHeaderHomeFeedPrefsBtn" 84 + href="/settings/following-feed" 85 + hitSlop={HITSLOP_10} 86 + accessibilityRole="button" 87 + accessibilityLabel={_(msg`Following Feed Preferences`)} 88 + accessibilityHint=""> 89 + <FontAwesomeIcon 90 + icon="sliders" 91 + style={pal.textLight as FontAwesomeIconStyle} 92 + /> 93 + </Link> 94 + )} 91 95 </View> 92 96 </View> 93 97 {children}
+57 -32
src/view/screens/Feeds.tsx
··· 1 1 import React from 'react' 2 2 import { 3 3 ActivityIndicator, 4 + type FlatList, 5 + Pressable, 4 6 StyleSheet, 5 7 View, 6 - type FlatList, 7 - Pressable, 8 8 } from 'react-native' 9 9 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 10 10 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 11 - import {ViewHeader} from 'view/com/util/ViewHeader' 12 - import {FAB} from 'view/com/util/fab/FAB' 13 - import {Link} from 'view/com/util/Link' 14 - import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' 11 + import {msg, Trans} from '@lingui/macro' 12 + import {useLingui} from '@lingui/react' 13 + import {useFocusEffect} from '@react-navigation/native' 14 + import debounce from 'lodash.debounce' 15 + 16 + import {isNative, isWeb} from '#/platform/detection' 17 + import { 18 + getAvatarTypeFromUri, 19 + useFeedSourceInfoQuery, 20 + useGetPopularFeedsQuery, 21 + useSearchPopularFeedsMutation, 22 + } from '#/state/queries/feed' 23 + import {usePreferencesQuery} from '#/state/queries/preferences' 24 + import {useSession} from '#/state/session' 25 + import {useSetMinimalShellMode} from '#/state/shell' 26 + import {useComposerControls} from '#/state/shell/composer' 27 + import {HITSLOP_10} from 'lib/constants' 15 28 import {usePalette} from 'lib/hooks/usePalette' 16 29 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 17 - import {ComposeIcon2, CogIcon, MagnifyingGlassIcon2} from 'lib/icons' 30 + import {CogIcon, ComposeIcon2, MagnifyingGlassIcon2} from 'lib/icons' 31 + import {FeedsTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' 32 + import {cleanError} from 'lib/strings/errors' 18 33 import {s} from 'lib/styles' 19 - import {atoms as a, useTheme} from '#/alf' 34 + import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 35 + import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 36 + import {FAB} from 'view/com/util/fab/FAB' 20 37 import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput' 21 - import {UserAvatar} from 'view/com/util/UserAvatar' 38 + import {Link} from 'view/com/util/Link' 39 + import {List} from 'view/com/util/List' 22 40 import { 41 + FeedFeedLoadingPlaceholder, 23 42 LoadingPlaceholder, 24 - FeedFeedLoadingPlaceholder, 25 43 } from 'view/com/util/LoadingPlaceholder' 26 - import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 27 - import debounce from 'lodash.debounce' 28 44 import {Text} from 'view/com/util/text/Text' 29 - import {List} from 'view/com/util/List' 30 - import {useFocusEffect} from '@react-navigation/native' 31 - import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 32 - import {Trans, msg} from '@lingui/macro' 33 - import {useLingui} from '@lingui/react' 34 - import {useSetMinimalShellMode} from '#/state/shell' 35 - import {usePreferencesQuery} from '#/state/queries/preferences' 36 - import { 37 - useFeedSourceInfoQuery, 38 - useGetPopularFeedsQuery, 39 - useSearchPopularFeedsMutation, 40 - getAvatarTypeFromUri, 41 - } from '#/state/queries/feed' 42 - import {cleanError} from 'lib/strings/errors' 43 - import {useComposerControls} from '#/state/shell/composer' 44 - import {useSession} from '#/state/session' 45 - import {isNative, isWeb} from '#/platform/detection' 46 - import {HITSLOP_10} from 'lib/constants' 45 + import {UserAvatar} from 'view/com/util/UserAvatar' 46 + import {ViewHeader} from 'view/com/util/ViewHeader' 47 + import {atoms as a, useTheme} from '#/alf' 47 48 import {IconCircle} from '#/components/IconCircle' 48 - import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' 49 49 import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' 50 + import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' 50 51 51 52 type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> 52 53 ··· 99 100 type: 'popularFeedsLoadingMore' 100 101 key: string 101 102 } 103 + 104 + // HACK 105 + // the protocol doesn't yet tell us which feeds are personalized 106 + // this list is used to filter out feed recommendations from logged out users 107 + // for the ones we know need it 108 + // -prf 109 + const KNOWN_AUTHED_ONLY_FEEDS = [ 110 + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app 111 + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed 112 + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed 113 + 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow 114 + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz 115 + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky 116 + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz 117 + 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why 118 + ] 102 119 103 120 export function FeedsScreen(_props: Props) { 104 121 const pal = usePalette('default') ··· 299 316 for (const page of popularFeeds.pages || []) { 300 317 slices = slices.concat( 301 318 page.feeds 302 - .filter(feed => !preferences?.feeds?.saved.includes(feed.uri)) 319 + .filter(feed => { 320 + if ( 321 + !hasSession && 322 + KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) 323 + ) { 324 + return false 325 + } 326 + return !preferences?.feeds?.saved.includes(feed.uri) 327 + }) 303 328 .map(feed => ({ 304 329 key: `popularFeed:${feed.uri}`, 305 330 type: 'popularFeed',
+7 -2
src/view/screens/Home.tsx
··· 2 2 import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native' 3 3 import {useFocusEffect} from '@react-navigation/native' 4 4 5 + import {PROD_DEFAULT_FEED} from '#/lib/constants' 5 6 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 6 7 import {useSetTitle} from '#/lib/hooks/useSetTitle' 7 8 import {logEvent, LogEvents, useGate} from '#/lib/statsig/statsig' ··· 19 20 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' 20 21 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' 21 22 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' 22 - import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' 23 23 import {HomeHeader} from '../com/home/HomeHeader' 24 24 25 25 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> ··· 231 231 onPageSelected={onPageSelected} 232 232 onPageScrollStateChanged={onPageScrollStateChanged} 233 233 renderTabBar={renderTabBar}> 234 - <HomeLoggedOutCTA /> 234 + <FeedPage 235 + testID="customFeedPage" 236 + isPageFocused 237 + feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`} 238 + renderEmptyState={renderCustomFeedEmptyState} 239 + /> 235 240 </Pager> 236 241 ) 237 242 }
+1 -2
src/view/screens/Profile.tsx
··· 184 184 const showRepliesTab = hasSession 185 185 const showMediaTab = !hasLabeler 186 186 const showLikesTab = isMe 187 - const showFeedsTab = 188 - hasSession && (isMe || (profile.associated?.feedgens || 0) > 0) 187 + const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0 189 188 const showListsTab = 190 189 hasSession && (isMe || (profile.associated?.lists || 0) > 0) 191 190
+9 -1
src/view/screens/Settings/index.tsx
··· 71 71 import {ScrollView} from 'view/com/util/Views' 72 72 import {useDialogControl} from '#/components/Dialog' 73 73 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 74 + import {navigate, resetToTab} from '#/Navigation' 74 75 import {ExportCarDialog} from './ExportCarDialog' 75 76 76 77 function SettingsAccountCard({account}: {account: SessionAccount}) { ··· 104 105 <TouchableOpacity 105 106 testID="signOutBtn" 106 107 onPress={() => { 107 - logout('Settings') 108 + if (isNative) { 109 + logout('Settings') 110 + resetToTab('HomeTab') 111 + } else { 112 + navigate('Home').then(() => { 113 + logout('Settings') 114 + }) 115 + } 108 116 }} 109 117 accessibilityRole="button" 110 118 accessibilityLabel={_(msg`Sign out`)}
+30 -26
src/view/shell/Drawer.tsx
··· 9 9 View, 10 10 ViewStyle, 11 11 } from 'react-native' 12 - import {useNavigation, StackActions} from '@react-navigation/native' 13 12 import { 14 13 FontAwesomeIcon, 15 14 FontAwesomeIconStyle, 16 15 } from '@fortawesome/react-native-fontawesome' 17 - import {s, colors} from 'lib/styles' 16 + import {msg, Trans} from '@lingui/macro' 17 + import {useLingui} from '@lingui/react' 18 + import {StackActions, useNavigation} from '@react-navigation/native' 19 + 20 + import {emitSoftReset} from '#/state/events' 21 + import {useUnreadNotifications} from '#/state/queries/notifications/unread' 22 + import {useProfileQuery} from '#/state/queries/profile' 23 + import {SessionAccount, useSession} from '#/state/session' 24 + import {useSetDrawerOpen} from '#/state/shell' 25 + import {useAnalytics} from 'lib/analytics/analytics' 18 26 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' 27 + import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' 28 + import {usePalette} from 'lib/hooks/usePalette' 19 29 import { 20 - HomeIcon, 21 - HomeIconSolid, 22 30 BellIcon, 23 31 BellIconSolid, 24 - UserIcon, 25 32 CogIcon, 33 + HandIcon, 34 + HashtagIcon, 35 + HomeIcon, 36 + HomeIconSolid, 37 + ListIcon, 26 38 MagnifyingGlassIcon2, 27 39 MagnifyingGlassIcon2Solid, 40 + UserIcon, 28 41 UserIconSolid, 29 - HashtagIcon, 30 - ListIcon, 31 - HandIcon, 32 42 } from 'lib/icons' 33 - import {UserAvatar} from 'view/com/util/UserAvatar' 34 - import {Text} from 'view/com/util/text/Text' 35 - import {useTheme} from 'lib/ThemeContext' 36 - import {usePalette} from 'lib/hooks/usePalette' 37 - import {useAnalytics} from 'lib/analytics/analytics' 38 - import {pluralize} from 'lib/strings/helpers' 39 43 import {getTabState, TabState} from 'lib/routes/helpers' 40 44 import {NavigationProp} from 'lib/routes/types' 41 - import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' 45 + import {pluralize} from 'lib/strings/helpers' 46 + import {colors, s} from 'lib/styles' 47 + import {useTheme} from 'lib/ThemeContext' 42 48 import {isWeb} from 'platform/detection' 49 + import {NavSignupCard} from '#/view/shell/NavSignupCard' 43 50 import {formatCountShortOnly} from 'view/com/util/numeric/format' 44 - import {Trans, msg} from '@lingui/macro' 45 - import {useLingui} from '@lingui/react' 46 - import {useSetDrawerOpen} from '#/state/shell' 47 - import {useSession, SessionAccount} from '#/state/session' 48 - import {useProfileQuery} from '#/state/queries/profile' 49 - import {useUnreadNotifications} from '#/state/queries/notifications/unread' 50 - import {emitSoftReset} from '#/state/events' 51 - import {NavSignupCard} from '#/view/shell/NavSignupCard' 52 - import {TextLink} from '../com/util/Link' 53 - 51 + import {Text} from 'view/com/util/text/Text' 52 + import {UserAvatar} from 'view/com/util/UserAvatar' 54 53 import {useTheme as useAlfTheme} from '#/alf' 54 + import {TextLink} from '../com/util/Link' 55 55 56 56 let DrawerProfileCard = ({ 57 57 account, ··· 246 246 <SettingsMenuItem onPress={onPressSettings} /> 247 247 </> 248 248 ) : ( 249 - <SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} /> 249 + <> 250 + <HomeMenuItem isActive={isAtHome} onPress={onPressHome} /> 251 + <FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} /> 252 + <SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} /> 253 + </> 250 254 )} 251 255 252 256 <View style={styles.smallSpacer} />
+14 -5
src/view/shell/NavSignupCard.tsx
··· 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 - import {s} from 'lib/styles' 7 - import {usePalette} from 'lib/hooks/usePalette' 8 - import {Text} from '#/view/com/util/text/Text' 9 - import {Button} from '#/view/com/util/forms/Button' 10 6 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 11 7 import {useCloseAllActiveElements} from '#/state/util' 8 + import {usePalette} from 'lib/hooks/usePalette' 9 + import {s} from 'lib/styles' 10 + import {Button} from '#/view/com/util/forms/Button' 11 + import {Text} from '#/view/com/util/text/Text' 12 12 import {Logo} from '#/view/icons/Logo' 13 + import {atoms as a} from '#/alf' 14 + import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 15 + import {Link} from '#/components/Link' 13 16 14 17 let NavSignupCard = ({}: {}): React.ReactNode => { 15 18 const {_} = useLingui() ··· 35 38 paddingTop: 6, 36 39 marginBottom: 24, 37 40 }}> 38 - <Logo width={48} /> 41 + <Link to="/" label="Bluesky - Home"> 42 + <Logo width={48} /> 43 + </Link> 39 44 40 45 <View style={{paddingTop: 18}}> 41 46 <Text type="md-bold" style={[pal.text]}> ··· 61 66 <Trans>Sign in</Trans> 62 67 </Text> 63 68 </Button> 69 + </View> 70 + 71 + <View style={[a.pt_2xl, a.w_full]}> 72 + <AppLanguageDropdown /> 64 73 </View> 65 74 </View> 66 75 )
+21 -18
src/view/shell/index.tsx
··· 1 1 import React from 'react' 2 - import {StatusBar} from 'expo-status-bar' 3 2 import { 3 + BackHandler, 4 4 DimensionValue, 5 5 StyleSheet, 6 6 useWindowDimensions, 7 7 View, 8 - BackHandler, 9 8 } from 'react-native' 10 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 11 9 import {Drawer} from 'react-native-drawer-layout' 10 + import Animated from 'react-native-reanimated' 11 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 12 + import {StatusBar} from 'expo-status-bar' 12 13 import {useNavigationState} from '@react-navigation/native' 13 - import {ModalsContainer} from 'view/com/modals/Modal' 14 - import {Lightbox} from 'view/com/lightbox/Lightbox' 15 - import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 16 - import {DrawerContent} from './Drawer' 17 - import {Composer} from './Composer' 18 - import {useTheme} from 'lib/ThemeContext' 19 - import {usePalette} from 'lib/hooks/usePalette' 20 - import {RoutesContainer, TabsNavigator} from '../../Navigation' 21 - import {isStateAtTabRoot} from 'lib/routes/helpers' 14 + 15 + import {useSession} from '#/state/session' 22 16 import { 23 17 useIsDrawerOpen, 24 - useSetDrawerOpen, 25 18 useIsDrawerSwipeDisabled, 19 + useSetDrawerOpen, 26 20 } from '#/state/shell' 27 - import {isAndroid} from 'platform/detection' 28 - import {useSession} from '#/state/session' 29 21 import {useCloseAnyActiveElement} from '#/state/util' 22 + import {usePalette} from 'lib/hooks/usePalette' 30 23 import * as notifications from 'lib/notifications/notifications' 24 + import {isStateAtTabRoot} from 'lib/routes/helpers' 25 + import {useTheme} from 'lib/ThemeContext' 26 + import {isAndroid} from 'platform/detection' 27 + import {useDialogStateContext} from 'state/dialogs' 28 + import {Lightbox} from 'view/com/lightbox/Lightbox' 29 + import {ModalsContainer} from 'view/com/modals/Modal' 30 + import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 31 + import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 32 + import {SigninDialog} from '#/components/dialogs/Signin' 31 33 import {Outlet as PortalOutlet} from '#/components/Portal' 32 - import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 33 - import {useDialogStateContext} from 'state/dialogs' 34 - import Animated from 'react-native-reanimated' 34 + import {RoutesContainer, TabsNavigator} from '../../Navigation' 35 + import {Composer} from './Composer' 36 + import {DrawerContent} from './Drawer' 35 37 36 38 function ShellInner() { 37 39 const isDrawerOpen = useIsDrawerOpen() ··· 101 103 <Composer winHeight={winDim.height} /> 102 104 <ModalsContainer /> 103 105 <MutedWordsDialog /> 106 + <SigninDialog /> 104 107 <Lightbox /> 105 108 <PortalOutlet /> 106 109 </>
+15 -13
src/view/shell/index.web.tsx
··· 1 1 import React, {useEffect} from 'react' 2 - import {View, StyleSheet, TouchableOpacity} from 'react-native' 3 - import {useNavigation} from '@react-navigation/native' 2 + import {StyleSheet, TouchableOpacity, View} from 'react-native' 4 3 import {msg} from '@lingui/macro' 5 4 import {useLingui} from '@lingui/react' 5 + import {useNavigation} from '@react-navigation/native' 6 6 7 - import {ErrorBoundary} from '../com/util/ErrorBoundary' 7 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 8 + import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' 9 + import {useCloseAllActiveElements} from '#/state/util' 10 + import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 11 + import {NavigationProp} from 'lib/routes/types' 12 + import {colors, s} from 'lib/styles' 13 + import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 14 + import {SigninDialog} from '#/components/dialogs/Signin' 15 + import {Outlet as PortalOutlet} from '#/components/Portal' 16 + import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' 17 + import {FlatNavigator, RoutesContainer} from '../../Navigation' 8 18 import {Lightbox} from '../com/lightbox/Lightbox' 9 19 import {ModalsContainer} from '../com/modals/Modal' 20 + import {ErrorBoundary} from '../com/util/ErrorBoundary' 10 21 import {Composer} from './Composer.web' 11 - import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 12 - import {s, colors} from 'lib/styles' 13 - import {RoutesContainer, FlatNavigator} from '../../Navigation' 14 22 import {DrawerContent} from './Drawer' 15 - import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' 16 - import {NavigationProp} from 'lib/routes/types' 17 - import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' 18 - import {useCloseAllActiveElements} from '#/state/util' 19 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 20 - import {Outlet as PortalOutlet} from '#/components/Portal' 21 - import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 22 23 23 24 function ShellInner() { 24 25 const isDrawerOpen = useIsDrawerOpen() ··· 45 46 <Composer winHeight={0} /> 46 47 <ModalsContainer /> 47 48 <MutedWordsDialog /> 49 + <SigninDialog /> 48 50 <Lightbox /> 49 51 <PortalOutlet /> 50 52