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
parent
44039c68d6
commit
ec5c4929c1
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: {},
|
|
||||||
})
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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`)}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue