Rework logged out state to preserve routing and work for web

zio/stable
Paul Frazee 2023-03-13 23:30:12 -05:00
parent b5c64a03b6
commit 774fb83719
26 changed files with 1063 additions and 1078 deletions

View File

@ -122,7 +122,9 @@ export class SessionModel {
try { try {
return await this.resumeSession(sess) return await this.resumeSession(sess)
} finally { } finally {
this.isResumingSession = false runInAction(() => {
this.isResumingSession = false
})
} }
} else { } else {
this.rootStore.log.debug( this.rootStore.log.debug(

View File

@ -0,0 +1,67 @@
import React from 'react'
import {SafeAreaView} from 'react-native'
import {observer} from 'mobx-react-lite'
import {Signin} from 'view/com/auth/Signin'
import {CreateAccount} from 'view/com/auth/CreateAccount'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics'
import {SplashScreen} from './SplashScreen'
import {CenteredView} from '../util/Views'
enum ScreenState {
S_SigninOrCreateAccount,
S_Signin,
S_CreateAccount,
}
export const LoggedOut = observer(() => {
const pal = usePalette('default')
const store = useStores()
const {screen} = useAnalytics()
const [screenState, setScreenState] = React.useState<ScreenState>(
ScreenState.S_SigninOrCreateAccount,
)
React.useEffect(() => {
screen('Login')
store.shell.setMinimalShellMode(true)
}, [store, screen])
if (
store.session.isResumingSession ||
screenState === ScreenState.S_SigninOrCreateAccount
) {
return (
<SplashScreen
onPressSignin={() => setScreenState(ScreenState.S_Signin)}
onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
/>
)
}
return (
<CenteredView style={[s.hContentRegion, pal.view]}>
<SafeAreaView testID="noSessionView" style={s.hContentRegion}>
<ErrorBoundary>
{screenState === ScreenState.S_Signin ? (
<Signin
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.S_CreateAccount ? (
<CreateAccount
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
</ErrorBoundary>
</SafeAreaView>
</CenteredView>
)
})

View File

@ -1,28 +1,19 @@
import React from 'react' import React from 'react'
import {StyleSheet} from 'react-native' import {StyleSheet, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {s, colors} from 'lib/styles'
import {s, gradients} from 'lib/styles'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
export const LogoTextHero = () => { export const LogoTextHero = () => {
return ( return (
<LinearGradient <View style={[styles.textHero]}>
colors={[gradients.blue.start, gradients.blue.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.textHero]}>
<Text type="title-lg" style={[s.white, s.bold]}> <Text type="title-lg" style={[s.white, s.bold]}>
Bluesky Bluesky
</Text> </Text>
</LinearGradient> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
logo: {
flexDirection: 'row',
justifyContent: 'center',
},
textHero: { textHero: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -30,5 +21,6 @@ const styles = StyleSheet.create({
paddingRight: 20, paddingRight: 20,
paddingVertical: 15, paddingVertical: 15,
marginBottom: 20, marginBottom: 20,
backgroundColor: colors.blue3,
}, },
}) })

View File

@ -0,0 +1,92 @@
import React from 'react'
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
import Image, {Source as ImageSource} from 'view/com/util/images/Image'
import {Text} from 'view/com/util/text/Text'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {CLOUD_SPLASH} from 'lib/assets'
import {CenteredView} from '../util/Views'
export const SplashScreen = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const pal = usePalette('default')
return (
<CenteredView style={styles.container}>
<Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
<View style={styles.hero}>
<View style={styles.heroText}>
<Text style={styles.title}>Bluesky</Text>
</View>
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
<TouchableOpacity
testID="createAccountButton"
style={[pal.view, styles.btn]}
onPress={onPressCreateAccount}>
<Text style={[pal.link, styles.btnLabel]}>
Create a new account
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="signInButton"
style={[pal.view, styles.btn]}
onPress={onPressSignin}>
<Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
</TouchableOpacity>
</View>
</ErrorBoundary>
</SafeAreaView>
</CenteredView>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
},
hero: {
flex: 2,
justifyContent: 'center',
},
bgImg: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
},
heroText: {
backgroundColor: colors.white,
paddingTop: 10,
paddingBottom: 20,
},
btns: {
paddingBottom: 40,
},
title: {
textAlign: 'center',
color: colors.blue3,
fontSize: 68,
fontWeight: 'bold',
},
btn: {
borderRadius: 4,
paddingVertical: 16,
marginBottom: 20,
marginHorizontal: 20,
backgroundColor: colors.blue3,
},
btnLabel: {
textAlign: 'center',
fontSize: 21,
color: colors.white,
},
})

View File

@ -0,0 +1,102 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {Text} from 'view/com/util/text/Text'
import {TextLink} from '../util/Link'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {CenteredView} from '../util/Views'
export const SplashScreen = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const pal = usePalette('default')
return (
<CenteredView style={styles.container}>
<View testID="noSessionView" style={styles.containerInner}>
<ErrorBoundary>
<Text style={styles.title}>Bluesky</Text>
<Text style={styles.subtitle}>See what's next</Text>
<View testID="signinOrCreateAccount" style={styles.btns}>
<TouchableOpacity
testID="createAccountButton"
style={[styles.btn, {backgroundColor: colors.blue3}]}
onPress={onPressCreateAccount}>
<Text style={[s.white, styles.btnLabel]}>
Create a new account
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="signInButton"
style={[styles.btn, pal.btn]}
onPress={onPressSignin}>
<Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
</TouchableOpacity>
</View>
<Text
type="xl"
style={[styles.notice, pal.textLight]}
lineHeight={1.3}>
Bluesky will launch soon.{' '}
<TextLink
type="xl"
text="Join the waitlist"
href="#"
style={pal.link}
/>{' '}
to try the beta before it's publicly available.
</Text>
</ErrorBoundary>
</View>
</CenteredView>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
backgroundColor: colors.gray1,
},
containerInner: {
backgroundColor: colors.white,
paddingVertical: 40,
paddingBottom: 50,
paddingHorizontal: 20,
},
title: {
textAlign: 'center',
color: colors.blue3,
fontSize: 68,
fontWeight: 'bold',
paddingBottom: 10,
},
subtitle: {
textAlign: 'center',
color: colors.gray5,
fontSize: 52,
fontWeight: 'bold',
paddingBottom: 30,
},
btns: {
flexDirection: 'row',
paddingBottom: 40,
},
btn: {
flex: 1,
borderRadius: 30,
paddingVertical: 12,
marginHorizontal: 10,
},
btnLabel: {
textAlign: 'center',
fontSize: 18,
},
notice: {
paddingHorizontal: 40,
textAlign: 'center',
},
})

View File

@ -0,0 +1,47 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index'
import {LoggedOut} from './LoggedOut'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
export const withAuthRequired = <P extends object>(
Component: React.ComponentType<P>,
): React.FC<P> =>
observer((props: P) => {
const store = useStores()
if (store.session.isResumingSession) {
return <Loading />
}
if (!store.session.hasSession) {
return <LoggedOut />
}
return <Component {...props} />
})
function Loading() {
const pal = usePalette('default')
return (
<View style={[styles.loading, pal.view]}>
<ActivityIndicator size="large" />
<Text type="2xl" style={[styles.loadingText, pal.textLight]}>
Firing up the grill...
</Text>
</View>
)
}
const styles = StyleSheet.create({
loading: {
height: '100%',
alignContent: 'center',
justifyContent: 'center',
paddingBottom: 100,
},
loadingText: {
paddingVertical: 20,
paddingHorizontal: 20,
textAlign: 'center',
},
})

View File

@ -4,6 +4,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import useAppState from 'react-native-appstate-hook' import useAppState from 'react-native-appstate-hook'
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/posts/Feed' import {Feed} from '../com/posts/Feed'
import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
@ -18,95 +19,97 @@ import {ComposeIcon2} from 'lib/icons'
const HEADER_HEIGHT = 42 const HEADER_HEIGHT = 42
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = observer(function Home(_opts: Props) { export const HomeScreen = withAuthRequired(
const store = useStores() observer(function Home(_opts: Props) {
const onMainScroll = useOnMainScroll(store) const store = useStores()
const {screen, track} = useAnalytics() const onMainScroll = useOnMainScroll(store)
const scrollElRef = React.useRef<FlatList>(null) const {screen, track} = useAnalytics()
const {appState} = useAppState({ const scrollElRef = React.useRef<FlatList>(null)
onForeground: () => doPoll(true), const {appState} = useAppState({
}) onForeground: () => doPoll(true),
const isFocused = useIsFocused() })
const isFocused = useIsFocused()
const doPoll = React.useCallback( const doPoll = React.useCallback(
(knownActive = false) => { (knownActive = false) => {
if ((!knownActive && appState !== 'active') || !isFocused) { if ((!knownActive && appState !== 'active') || !isFocused) {
return return
} }
if (store.me.mainFeed.isLoading) { if (store.me.mainFeed.isLoading) {
return return
} }
store.log.debug('HomeScreen: Polling for new posts') store.log.debug('HomeScreen: Polling for new posts')
store.me.mainFeed.checkForLatest() store.me.mainFeed.checkForLatest()
}, },
[appState, isFocused, store], [appState, isFocused, store],
) )
const scrollToTop = React.useCallback(() => { const scrollToTop = React.useCallback(() => {
// NOTE: the feed is offset by the height of the collapsing header, // NOTE: the feed is offset by the height of the collapsing header,
// so we scroll to the negative of that height -prf // so we scroll to the negative of that height -prf
scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT}) scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
}, [scrollElRef]) }, [scrollElRef])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(scrollToTop) const softResetSub = store.onScreenSoftReset(scrollToTop)
const feedCleanup = store.me.mainFeed.registerListeners() const feedCleanup = store.me.mainFeed.registerListeners()
const pollInterval = setInterval(doPoll, 15e3) const pollInterval = setInterval(doPoll, 15e3)
screen('Feed') screen('Feed')
store.log.debug('HomeScreen: Updating feed') store.log.debug('HomeScreen: Updating feed')
if (store.me.mainFeed.hasContent) { if (store.me.mainFeed.hasContent) {
store.me.mainFeed.update() store.me.mainFeed.update()
} }
return () => { return () => {
clearInterval(pollInterval) clearInterval(pollInterval)
softResetSub.remove() softResetSub.remove()
feedCleanup() feedCleanup()
} }
}, [store, doPoll, scrollToTop, screen]), }, [store, doPoll, scrollToTop, screen]),
) )
const onPressCompose = React.useCallback(() => { const onPressCompose = React.useCallback(() => {
track('HomeScreen:PressCompose') track('HomeScreen:PressCompose')
store.shell.openComposer({}) store.shell.openComposer({})
}, [store, track]) }, [store, track])
const onPressTryAgain = React.useCallback(() => { const onPressTryAgain = React.useCallback(() => {
store.me.mainFeed.refresh() store.me.mainFeed.refresh()
}, [store]) }, [store])
const onPressLoadLatest = React.useCallback(() => { const onPressLoadLatest = React.useCallback(() => {
store.me.mainFeed.refresh() store.me.mainFeed.refresh()
scrollToTop() scrollToTop()
}, [store, scrollToTop]) }, [store, scrollToTop])
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
{store.shell.isOnboarding && <WelcomeBanner />} {store.shell.isOnboarding && <WelcomeBanner />}
<Feed <Feed
testID="homeFeed" testID="homeFeed"
key="default" key="default"
feed={store.me.mainFeed} feed={store.me.mainFeed}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
style={s.hContentRegion} style={s.hContentRegion}
showPostFollowBtn showPostFollowBtn
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll} onScroll={onMainScroll}
headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT} headerOffset={store.shell.isOnboarding ? 0 : HEADER_HEIGHT}
/> />
{!store.shell.isOnboarding && ( {!store.shell.isOnboarding && (
<ViewHeader title="Bluesky" canGoBack={false} hideOnScroll /> <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
)} )}
{store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
<LoadLatestBtn onPress={onPressLoadLatest} /> <LoadLatestBtn onPress={onPressLoadLatest} />
)} )}
<FAB <FAB
testID="composeFAB" testID="composeFAB"
onPress={onPressCompose} onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
/> />
</View> </View>
) )
}) }),
)

View File

@ -1,164 +0,0 @@
import React, {useEffect, useState} from 'react'
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
import Image, {Source as ImageSource} from 'view/com/util/images/Image'
import {observer} from 'mobx-react-lite'
import {Signin} from '../com/login/Signin'
import {CreateAccount} from '../com/login/CreateAccount'
import {Text} from '../com/util/text/Text'
import {ErrorBoundary} from '../com/util/ErrorBoundary'
import {colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {CLOUD_SPLASH} from 'lib/assets'
import {useAnalytics} from 'lib/analytics'
enum ScreenState {
S_SigninOrCreateAccount,
S_Signin,
S_CreateAccount,
}
const SigninOrCreateAccount = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const {screen} = useAnalytics()
useEffect(() => {
screen('Login')
}, [screen])
const pal = usePalette('default')
return (
<>
<View style={styles.hero}>
<View style={styles.heroText}>
<Text style={styles.title}>Bluesky</Text>
</View>
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
<TouchableOpacity
testID="createAccountButton"
style={[pal.view, styles.btn]}
onPress={onPressCreateAccount}>
<Text style={[pal.link, styles.btnLabel]}>Create a new account</Text>
</TouchableOpacity>
<TouchableOpacity
testID="signInButton"
style={[pal.view, styles.btn]}
onPress={onPressSignin}>
<Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
</TouchableOpacity>
</View>
</>
)
}
export const Login = observer(() => {
const pal = usePalette('default')
const store = useStores()
const [screenState, setScreenState] = useState<ScreenState>(
ScreenState.S_SigninOrCreateAccount,
)
if (
store.session.isResumingSession ||
screenState === ScreenState.S_SigninOrCreateAccount
) {
return (
<View style={styles.container}>
<Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
{!store.session.isResumingSession && (
<SigninOrCreateAccount
onPressSignin={() => setScreenState(ScreenState.S_Signin)}
onPressCreateAccount={() =>
setScreenState(ScreenState.S_CreateAccount)
}
/>
)}
</ErrorBoundary>
</SafeAreaView>
</View>
)
}
return (
<View style={[styles.container, pal.view]}>
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
{screenState === ScreenState.S_Signin ? (
<Signin
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.S_CreateAccount ? (
<CreateAccount
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
</ErrorBoundary>
</SafeAreaView>
</View>
)
})
const styles = StyleSheet.create({
container: {
height: '100%',
},
outer: {
flex: 1,
},
hero: {
flex: 2,
justifyContent: 'center',
},
bgImg: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
},
heroText: {
backgroundColor: colors.white,
paddingTop: 10,
paddingBottom: 20,
},
btns: {
paddingBottom: 40,
},
title: {
textAlign: 'center',
color: colors.blue3,
fontSize: 68,
fontWeight: 'bold',
},
subtitle: {
textAlign: 'center',
color: colors.blue3,
fontSize: 18,
},
btn: {
borderRadius: 4,
paddingVertical: 16,
marginBottom: 20,
marginHorizontal: 20,
backgroundColor: colors.blue3,
},
btnLabel: {
textAlign: 'center',
fontSize: 21,
// fontWeight: '500',
color: colors.white,
},
})

View File

@ -1,156 +0,0 @@
import React, {useState} from 'react'
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {CenteredView} from '../com/util/Views'
import {Signin} from '../com/login/Signin'
import {CreateAccount} from '../com/login/CreateAccount'
import {Text} from '../com/util/text/Text'
import {ErrorBoundary} from '../com/util/ErrorBoundary'
import {colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
enum ScreenState {
S_SigninOrCreateAccount,
S_Signin,
S_CreateAccount,
}
const SigninOrCreateAccount = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const pal = usePalette('default')
return (
<>
<View style={styles.hero}>
<View style={styles.heroText}>
<Text style={styles.title}>Bluesky</Text>
</View>
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
<TouchableOpacity
testID="createAccountButton"
style={[pal.view, styles.btn]}
onPress={onPressCreateAccount}>
<Text style={[pal.link, styles.btnLabel]}>New account</Text>
</TouchableOpacity>
<TouchableOpacity
testID="signInButton"
style={[pal.view, styles.btn]}
onPress={onPressSignin}>
<Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
</TouchableOpacity>
</View>
</>
)
}
export const Login = observer(() => {
const pal = usePalette('default')
const [screenState, setScreenState] = useState<ScreenState>(
ScreenState.S_SigninOrCreateAccount,
)
if (screenState === ScreenState.S_SigninOrCreateAccount) {
return (
<CenteredView style={[styles.container, styles.vertCenter]}>
<ErrorBoundary>
<SigninOrCreateAccount
onPressSignin={() => setScreenState(ScreenState.S_Signin)}
onPressCreateAccount={() =>
setScreenState(ScreenState.S_CreateAccount)
}
/>
</ErrorBoundary>
</CenteredView>
)
}
return (
<CenteredView
style={[
styles.container,
styles.containerBorder,
pal.view,
pal.borderDark,
]}>
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
{screenState === ScreenState.S_Signin ? (
<Signin
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.S_CreateAccount ? (
<CreateAccount
onPressBack={() =>
setScreenState(ScreenState.S_SigninOrCreateAccount)
}
/>
) : undefined}
</ErrorBoundary>
</SafeAreaView>
</CenteredView>
)
})
const styles = StyleSheet.create({
container: {
height: '100%',
},
containerBorder: {
borderLeftWidth: 1,
borderRightWidth: 1,
},
vertCenter: {
justifyContent: 'center',
},
bgImg: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
},
hero: {},
heroText: {
backgroundColor: colors.white,
paddingTop: 10,
paddingBottom: 20,
},
btns: {
flexDirection: 'row',
paddingTop: 40,
},
title: {
textAlign: 'center',
color: colors.blue3,
fontSize: 68,
fontWeight: 'bold',
},
subtitle: {
textAlign: 'center',
color: colors.blue3,
fontSize: 18,
},
btn: {
flex: 1,
borderRadius: 4,
paddingVertical: 16,
marginBottom: 20,
marginHorizontal: 20,
borderWidth: 1,
borderColor: colors.blue3,
},
btnLabel: {
textAlign: 'center',
fontSize: 21,
fontWeight: '500',
color: colors.blue3,
},
})

View File

@ -1,11 +1,13 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {FlatList, View} from 'react-native' import {FlatList, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {observer} from 'mobx-react-lite'
import useAppState from 'react-native-appstate-hook' import useAppState from 'react-native-appstate-hook'
import { import {
NativeStackScreenProps, NativeStackScreenProps,
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {useStores} from 'state/index' import {useStores} from 'state/index'
@ -19,77 +21,79 @@ type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
'Notifications' 'Notifications'
> >
export const NotificationsScreen = ({}: Props) => { export const NotificationsScreen = withAuthRequired(
const store = useStores() observer(({}: Props) => {
const onMainScroll = useOnMainScroll(store) const store = useStores()
const scrollElRef = React.useRef<FlatList>(null) const onMainScroll = useOnMainScroll(store)
const {screen} = useAnalytics() const scrollElRef = React.useRef<FlatList>(null)
const {appState} = useAppState({ const {screen} = useAnalytics()
onForeground: () => doPoll(true), const {appState} = useAppState({
}) onForeground: () => doPoll(true),
})
// event handlers // event handlers
// = // =
const onPressTryAgain = () => { const onPressTryAgain = () => {
store.me.notifications.refresh() store.me.notifications.refresh()
} }
const scrollToTop = React.useCallback(() => { const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: 0}) scrollElRef.current?.scrollToOffset({offset: 0})
}, [scrollElRef]) }, [scrollElRef])
// periodic polling // periodic polling
// = // =
const doPoll = React.useCallback( const doPoll = React.useCallback(
async (isForegrounding = false) => { async (isForegrounding = false) => {
if (isForegrounding) { if (isForegrounding) {
// app is foregrounding, refresh optimistically // app is foregrounding, refresh optimistically
store.log.debug('NotificationsScreen: Refreshing on app foreground') store.log.debug('NotificationsScreen: Refreshing on app foreground')
await Promise.all([ await Promise.all([
store.me.notifications.loadUnreadCount(), store.me.notifications.loadUnreadCount(),
store.me.notifications.refresh(), store.me.notifications.refresh(),
]) ])
} else if (appState === 'active') { } else if (appState === 'active') {
// periodic poll, refresh if there are new notifs // periodic poll, refresh if there are new notifs
store.log.debug('NotificationsScreen: Polling for new notifications') store.log.debug('NotificationsScreen: Polling for new notifications')
const didChange = await store.me.notifications.loadUnreadCount() const didChange = await store.me.notifications.loadUnreadCount()
if (didChange) { if (didChange) {
store.log.debug('NotificationsScreen: Loading new notifications') store.log.debug('NotificationsScreen: Loading new notifications')
await store.me.notifications.loadLatest() await store.me.notifications.loadLatest()
}
} }
} },
}, [appState, store],
[appState, store], )
) useEffect(() => {
useEffect(() => { const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL)
const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL) return () => clearInterval(pollInterval)
return () => clearInterval(pollInterval) }, [doPoll])
}, [doPoll])
// on-visible setup // on-visible setup
// = // =
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
store.log.debug('NotificationsScreen: Updating feed') store.log.debug('NotificationsScreen: Updating feed')
const softResetSub = store.onScreenSoftReset(scrollToTop) const softResetSub = store.onScreenSoftReset(scrollToTop)
store.me.notifications.update() store.me.notifications.update()
screen('Notifications') screen('Notifications')
return () => { return () => {
softResetSub.remove() softResetSub.remove()
store.me.notifications.markAllRead() store.me.notifications.markAllRead()
} }
}, [store, screen, scrollToTop]), }, [store, screen, scrollToTop]),
) )
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
<ViewHeader title="Notifications" canGoBack={false} /> <ViewHeader title="Notifications" canGoBack={false} />
<Feed <Feed
view={store.me.notifications} view={store.me.notifications}
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll} onScroll={onMainScroll}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
/> />
</View> </View>
) )
} }),
)

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy' import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
@ -8,7 +9,7 @@ import {useStores} from 'state/index'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
export const PostRepostedByScreen = ({route}: Props) => { export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
const store = useStores() const store = useStores()
const {name, rkey} = route.params const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@ -25,4 +26,4 @@ export const PostRepostedByScreen = ({route}: Props) => {
<PostRepostedByComponent uri={uri} /> <PostRepostedByComponent uri={uri} />
</View> </View>
) )
} })

View File

@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ComposePrompt} from 'view/com/composer/Prompt' import {ComposePrompt} from 'view/com/composer/Prompt'
@ -16,7 +17,7 @@ import {isDesktopWeb} from 'platform/detection'
const SHELL_FOOTER_HEIGHT = 44 const SHELL_FOOTER_HEIGHT = 44
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export const PostThreadScreen = ({route}: Props) => { export const PostThreadScreen = withAuthRequired(({route}: Props) => {
const store = useStores() const store = useStores()
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const {name, rkey} = route.params const {name, rkey} = route.params
@ -84,7 +85,7 @@ export const PostThreadScreen = ({route}: Props) => {
)} )}
</View> </View>
) )
} })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
prompt: { prompt: {

View File

@ -2,13 +2,14 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy' import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostUpvotedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostUpvotedBy'>
export const PostUpvotedByScreen = ({route}: Props) => { export const PostUpvotedByScreen = withAuthRequired(({route}: Props) => {
const store = useStores() const store = useStores()
const {name, rkey} = route.params const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@ -25,4 +26,4 @@ export const PostUpvotedByScreen = ({route}: Props) => {
<PostLikedByComponent uri={uri} direction="up" /> <PostLikedByComponent uri={uri} direction="up" />
</View> </View>
) )
} })

View File

@ -26,7 +26,7 @@ export const PrivacyPolicyScreen = (_props: Props) => {
<ViewHeader title="Privacy Policy" /> <ViewHeader title="Privacy Policy" />
<ScrollView style={[s.hContentRegion, pal.view]}> <ScrollView style={[s.hContentRegion, pal.view]}>
<View style={[s.p20]}> <View style={[s.p20]}>
<Text type="title-xl" style={[pal.text, s.pb20]}> <Text type="title-xl" style={[pal.text, s.bold, s.pb20]}>
Privacy Policy Privacy Policy
</Text> </Text>
<PrivacyPolicyHtml /> <PrivacyPolicyHtml />

View File

@ -3,6 +3,7 @@ import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewSelector} from '../com/util/ViewSelector' import {ViewSelector} from '../com/util/ViewSelector'
import {CenteredView} from '../com/util/Views' import {CenteredView} from '../com/util/Views'
import {ProfileUiModel, Sections} from 'state/models/profile-ui' import {ProfileUiModel, Sections} from 'state/models/profile-ui'
@ -25,178 +26,182 @@ const END_ITEM = {_reactKey: '__end__'}
const EMPTY_ITEM = {_reactKey: '__empty__'} const EMPTY_ITEM = {_reactKey: '__empty__'}
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
export const ProfileScreen = observer(({route}: Props) => { export const ProfileScreen = withAuthRequired(
const store = useStores() observer(({route}: Props) => {
const {screen, track} = useAnalytics() const store = useStores()
const {screen, track} = useAnalytics()
useEffect(() => { useEffect(() => {
screen('Profile') screen('Profile')
}, [screen]) }, [screen])
const onMainScroll = useOnMainScroll(store) const onMainScroll = useOnMainScroll(store)
const [hasSetup, setHasSetup] = useState<boolean>(false) const [hasSetup, setHasSetup] = useState<boolean>(false)
const uiState = React.useMemo( const uiState = React.useMemo(
() => new ProfileUiModel(store, {user: route.params.name}), () => new ProfileUiModel(store, {user: route.params.name}),
[route.params.name, store], [route.params.name, store],
) )
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
let aborted = false let aborted = false
const feedCleanup = uiState.feed.registerListeners() const feedCleanup = uiState.feed.registerListeners()
if (hasSetup) { if (hasSetup) {
uiState.update() uiState.update()
} else { } else {
uiState.setup().then(() => { uiState.setup().then(() => {
if (aborted) { if (aborted) {
return return
} }
setHasSetup(true) setHasSetup(true)
}) })
} }
return () => { return () => {
aborted = true aborted = true
feedCleanup() feedCleanup()
} }
}, [hasSetup, uiState]), }, [hasSetup, uiState]),
) )
// events // events
// = // =
const onPressCompose = React.useCallback(() => { const onPressCompose = React.useCallback(() => {
track('ProfileScreen:PressCompose') track('ProfileScreen:PressCompose')
store.shell.openComposer({}) store.shell.openComposer({})
}, [store, track]) }, [store, track])
const onSelectView = (index: number) => { const onSelectView = (index: number) => {
uiState.setSelectedViewIndex(index) uiState.setSelectedViewIndex(index)
}
const onRefresh = () => {
uiState
.refresh()
.catch((err: any) =>
store.log.error('Failed to refresh user profile', err),
)
}
const onEndReached = () => {
uiState
.loadMore()
.catch((err: any) =>
store.log.error('Failed to load more entries in user profile', err),
)
}
const onPressTryAgain = () => {
uiState.setup()
}
// rendering
// =
const renderHeader = () => {
if (!uiState) {
return <View />
} }
return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} /> const onRefresh = () => {
} uiState
let renderItem .refresh()
let Footer .catch((err: any) =>
let items: any[] = [] store.log.error('Failed to refresh user profile', err),
if (uiState) { )
if (uiState.isInitialLoading) { }
items = items.concat([LOADING_ITEM]) const onEndReached = () => {
renderItem = () => <PostFeedLoadingPlaceholder /> uiState
} else if (uiState.currentView.hasError) { .loadMore()
items = items.concat([ .catch((err: any) =>
{ store.log.error('Failed to load more entries in user profile', err),
_reactKey: '__error__', )
error: uiState.currentView.error, }
}, const onPressTryAgain = () => {
]) uiState.setup()
renderItem = (item: any) => ( }
<View style={s.p5}>
<ErrorMessage // rendering
message={item.error} // =
const renderHeader = () => {
if (!uiState) {
return <View />
}
return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
}
let renderItem
let Footer
let items: any[] = []
if (uiState) {
if (uiState.isInitialLoading) {
items = items.concat([LOADING_ITEM])
renderItem = () => <PostFeedLoadingPlaceholder />
} else if (uiState.currentView.hasError) {
items = items.concat([
{
_reactKey: '__error__',
error: uiState.currentView.error,
},
])
renderItem = (item: any) => (
<View style={s.p5}>
<ErrorMessage
message={item.error}
onPressTryAgain={onPressTryAgain}
/>
</View>
)
} else {
if (
uiState.selectedView === Sections.Posts ||
uiState.selectedView === Sections.PostsWithReplies
) {
if (uiState.feed.hasContent) {
if (uiState.selectedView === Sections.Posts) {
items = uiState.feed.nonReplyFeed
} else {
items = uiState.feed.feed.slice()
}
if (!uiState.feed.hasMore) {
items = items.concat([END_ITEM])
} else if (uiState.feed.isLoading) {
Footer = LoadingMoreFooter
}
renderItem = (item: any) => {
if (item === END_ITEM) {
return <Text style={styles.endItem}>- end of feed -</Text>
}
return (
<FeedItem item={item} ignoreMuteFor={uiState.profile.did} />
)
}
} else if (uiState.feed.isEmpty) {
items = items.concat([EMPTY_ITEM])
renderItem = () => (
<EmptyState
icon={['far', 'message']}
message="No posts yet!"
style={styles.emptyState}
/>
)
}
} else {
items = items.concat([EMPTY_ITEM])
renderItem = () => <Text>TODO</Text>
}
}
}
if (!renderItem) {
renderItem = () => <View />
}
return (
<View testID="profileView" style={styles.container}>
{uiState.profile.hasError ? (
<ErrorScreen
testID="profileErrorScreen"
title="Failed to load profile"
message={`There was an issue when attempting to load ${route.params.name}`}
details={uiState.profile.error}
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
/> />
</View> ) : uiState.profile.hasLoaded ? (
) <ViewSelector
} else { swipeEnabled
if ( sections={uiState.selectorItems}
uiState.selectedView === Sections.Posts || items={items}
uiState.selectedView === Sections.PostsWithReplies renderHeader={renderHeader}
) { renderItem={renderItem}
if (uiState.feed.hasContent) { ListFooterComponent={Footer}
if (uiState.selectedView === Sections.Posts) { refreshing={uiState.isRefreshing || false}
items = uiState.feed.nonReplyFeed onSelectView={onSelectView}
} else { onScroll={onMainScroll}
items = uiState.feed.feed.slice() onRefresh={onRefresh}
} onEndReached={onEndReached}
if (!uiState.feed.hasMore) { />
items = items.concat([END_ITEM]) ) : (
} else if (uiState.feed.isLoading) { <CenteredView>{renderHeader()}</CenteredView>
Footer = LoadingMoreFooter )}
} <FAB
renderItem = (item: any) => { testID="composeFAB"
if (item === END_ITEM) { onPress={onPressCompose}
return <Text style={styles.endItem}>- end of feed -</Text> icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
}
return <FeedItem item={item} ignoreMuteFor={uiState.profile.did} />
}
} else if (uiState.feed.isEmpty) {
items = items.concat([EMPTY_ITEM])
renderItem = () => (
<EmptyState
icon={['far', 'message']}
message="No posts yet!"
style={styles.emptyState}
/>
)
}
} else {
items = items.concat([EMPTY_ITEM])
renderItem = () => <Text>TODO</Text>
}
}
}
if (!renderItem) {
renderItem = () => <View />
}
return (
<View testID="profileView" style={styles.container}>
{uiState.profile.hasError ? (
<ErrorScreen
testID="profileErrorScreen"
title="Failed to load profile"
message={`There was an issue when attempting to load ${route.params.name}`}
details={uiState.profile.error}
onPressTryAgain={onPressTryAgain}
/> />
) : uiState.profile.hasLoaded ? ( </View>
<ViewSelector )
swipeEnabled }),
sections={uiState.selectorItems} )
items={items}
renderHeader={renderHeader}
renderItem={renderItem}
ListFooterComponent={Footer}
refreshing={uiState.isRefreshing || false}
onSelectView={onSelectView}
onScroll={onMainScroll}
onRefresh={onRefresh}
onEndReached={onEndReached}
/>
) : (
<CenteredView>{renderHeader()}</CenteredView>
)}
<FAB
testID="composeFAB"
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
/>
</View>
)
})
function LoadingMoreFooter() { function LoadingMoreFooter() {
return ( return (

View File

@ -2,12 +2,13 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
import {useStores} from 'state/index' import {useStores} from 'state/index'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
export const ProfileFollowersScreen = ({route}: Props) => { export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
const store = useStores() const store = useStores()
const {name} = route.params const {name} = route.params
@ -23,4 +24,4 @@ export const ProfileFollowersScreen = ({route}: Props) => {
<ProfileFollowersComponent name={name} /> <ProfileFollowersComponent name={name} />
</View> </View>
) )
} })

View File

@ -2,12 +2,13 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
import {useStores} from 'state/index' import {useStores} from 'state/index'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
export const ProfileFollowsScreen = ({route}: Props) => { export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
const store = useStores() const store = useStores()
const {name} = route.params const {name} = route.params
@ -23,4 +24,4 @@ export const ProfileFollowsScreen = ({route}: Props) => {
<ProfileFollowsComponent name={name} /> <ProfileFollowsComponent name={name} />
</View> </View>
) )
} })

View File

@ -12,6 +12,7 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ScrollView} from '../com/util/Views' import {ScrollView} from '../com/util/Views'
import { import {
NativeStackScreenProps, NativeStackScreenProps,
@ -36,159 +37,161 @@ const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
const FIVE_MIN = 5 * 60 * 1e3 const FIVE_MIN = 5 * 60 * 1e3
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = observer<Props>(({}: Props) => { export const SearchScreen = withAuthRequired(
const pal = usePalette('default') observer<Props>(({}: Props) => {
const theme = useTheme() const pal = usePalette('default')
const store = useStores() const theme = useTheme()
const {track} = useAnalytics() const store = useStores()
const scrollElRef = React.useRef<ScrollView>(null) const {track} = useAnalytics()
const onMainScroll = useOnMainScroll(store) const scrollElRef = React.useRef<ScrollView>(null)
const textInput = React.useRef<TextInput>(null) const onMainScroll = useOnMainScroll(store)
const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads const textInput = React.useRef<TextInput>(null)
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
const [query, setQuery] = React.useState<string>('') const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const autocompleteView = React.useMemo<UserAutocompleteViewModel>( const [query, setQuery] = React.useState<string>('')
() => new UserAutocompleteViewModel(store), const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
[store], () => new UserAutocompleteViewModel(store),
) [store],
)
const onSoftReset = () => { const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0}) scrollElRef.current?.scrollTo({x: 0, y: 0})
}
useFocusEffect(
React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
const now = Date.now()
if (now - lastRenderTime > FIVE_MIN) {
setRenderTime(Date.now()) // trigger reload of suggestions
}
store.shell.setMinimalShellMode(false)
autocompleteView.setup()
return cleanup
}, [store, autocompleteView, lastRenderTime, setRenderTime]),
)
const onPressMenu = () => {
track('ViewHeader:MenuButtonClicked')
store.shell.openDrawer()
}
const onChangeQuery = (text: string) => {
setQuery(text)
if (text.length > 0) {
autocompleteView.setActive(true)
autocompleteView.setPrefix(text)
} else {
autocompleteView.setActive(false)
} }
}
const onPressClearQuery = () => {
setQuery('')
}
const onPressCancelSearch = () => {
setQuery('')
autocompleteView.setActive(false)
textInput.current?.blur()
}
return ( useFocusEffect(
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> React.useCallback(() => {
<ScrollView const softResetSub = store.onScreenSoftReset(onSoftReset)
ref={scrollElRef} const cleanup = () => {
testID="searchScrollView" softResetSub.remove()
style={[pal.view, styles.container]} }
onScroll={onMainScroll}
scrollEventThrottle={100}> const now = Date.now()
<View style={[pal.view, pal.border, styles.header]}> if (now - lastRenderTime > FIVE_MIN) {
<TouchableOpacity setRenderTime(Date.now()) // trigger reload of suggestions
testID="viewHeaderBackOrMenuBtn" }
onPress={onPressMenu} store.shell.setMinimalShellMode(false)
hitSlop={MENU_HITSLOP} autocompleteView.setup()
style={styles.headerMenuBtn}>
<UserAvatar size={30} avatar={store.me.avatar} /> return cleanup
</TouchableOpacity> }, [store, autocompleteView, lastRenderTime, setRenderTime]),
<View )
style={[
{backgroundColor: pal.colors.backgroundLight}, const onPressMenu = () => {
styles.headerSearchContainer, track('ViewHeader:MenuButtonClicked')
]}> store.shell.openDrawer()
<MagnifyingGlassIcon }
style={[pal.icon, styles.headerSearchIcon]}
size={21} const onChangeQuery = (text: string) => {
/> setQuery(text)
<TextInput if (text.length > 0) {
testID="searchTextInput" autocompleteView.setActive(true)
ref={textInput} autocompleteView.setPrefix(text)
placeholder="Search" } else {
placeholderTextColor={pal.colors.textLight} autocompleteView.setActive(false)
selectTextOnFocus }
returnKeyType="search" }
value={query} const onPressClearQuery = () => {
style={[pal.text, styles.headerSearchInput]} setQuery('')
keyboardAppearance={theme.colorScheme} }
onFocus={() => setIsInputFocused(true)} const onPressCancelSearch = () => {
onBlur={() => setIsInputFocused(false)} setQuery('')
onChangeText={onChangeQuery} autocompleteView.setActive(false)
/> textInput.current?.blur()
{query ? ( }
<TouchableOpacity onPress={onPressClearQuery}>
<FontAwesomeIcon return (
icon="xmark" <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
size={16} <ScrollView
style={pal.textLight as FontAwesomeIconStyle} ref={scrollElRef}
/> testID="searchScrollView"
</TouchableOpacity> style={[pal.view, styles.container]}
onScroll={onMainScroll}
scrollEventThrottle={100}>
<View style={[pal.view, pal.border, styles.header]}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={onPressMenu}
hitSlop={MENU_HITSLOP}
style={styles.headerMenuBtn}>
<UserAvatar size={30} avatar={store.me.avatar} />
</TouchableOpacity>
<View
style={[
{backgroundColor: pal.colors.backgroundLight},
styles.headerSearchContainer,
]}>
<MagnifyingGlassIcon
style={[pal.icon, styles.headerSearchIcon]}
size={21}
/>
<TextInput
testID="searchTextInput"
ref={textInput}
placeholder="Search"
placeholderTextColor={pal.colors.textLight}
selectTextOnFocus
returnKeyType="search"
value={query}
style={[pal.text, styles.headerSearchInput]}
keyboardAppearance={theme.colorScheme}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onChangeText={onChangeQuery}
/>
{query ? (
<TouchableOpacity onPress={onPressClearQuery}>
<FontAwesomeIcon
icon="xmark"
size={16}
style={pal.textLight as FontAwesomeIconStyle}
/>
</TouchableOpacity>
) : undefined}
</View>
{query || isInputFocused ? (
<View style={styles.headerCancelBtn}>
<TouchableOpacity onPress={onPressCancelSearch}>
<Text style={pal.text}>Cancel</Text>
</TouchableOpacity>
</View>
) : undefined} ) : undefined}
</View> </View>
{query || isInputFocused ? ( {query && autocompleteView.searchRes.length ? (
<View style={styles.headerCancelBtn}> <>
<TouchableOpacity onPress={onPressCancelSearch}> {autocompleteView.searchRes.map(item => (
<Text style={pal.text}>Cancel</Text> <ProfileCard
</TouchableOpacity> key={item.did}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
/>
))}
</>
) : query && !autocompleteView.searchRes.length ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
No results found for {autocompleteView.prefix}
</Text>
</View> </View>
) : undefined} ) : isInputFocused ? (
</View> <View>
{query && autocompleteView.searchRes.length ? ( <Text style={[pal.textLight, styles.searchPrompt]}>
<> Search for users on the network
{autocompleteView.searchRes.map(item => ( </Text>
<ProfileCard </View>
key={item.did} ) : (
handle={item.handle} <ScrollView onScroll={Keyboard.dismiss}>
displayName={item.displayName} <WhoToFollow key={`wtf-${lastRenderTime}`} />
avatar={item.avatar} <SuggestedPosts key={`sp-${lastRenderTime}`} />
/> <View style={s.footerSpacer} />
))} </ScrollView>
</> )}
) : query && !autocompleteView.searchRes.length ? ( <View style={s.footerSpacer} />
<View> </ScrollView>
<Text style={[pal.textLight, styles.searchPrompt]}> </TouchableWithoutFeedback>
No results found for {autocompleteView.prefix} )
</Text> }),
</View> )
) : isInputFocused ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
Search for users on the network
</Text>
</View>
) : (
<ScrollView onScroll={Keyboard.dismiss}>
<WhoToFollow key={`wtf-${lastRenderTime}`} />
<SuggestedPosts key={`sp-${lastRenderTime}`} />
<View style={s.footerSpacer} />
</ScrollView>
)}
<View style={s.footerSpacer} />
</ScrollView>
</TouchableWithoutFeedback>
)
})
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ScrollView} from '../com/util/Views' import {ScrollView} from '../com/util/Views'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {
@ -17,46 +18,48 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
const FIVE_MIN = 5 * 60 * 1e3 const FIVE_MIN = 5 * 60 * 1e3
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = observer(({}: Props) => { export const SearchScreen = withAuthRequired(
const pal = usePalette('default') observer(({}: Props) => {
const store = useStores() const pal = usePalette('default')
const scrollElRef = React.useRef<ScrollView>(null) const store = useStores()
const onMainScroll = useOnMainScroll(store) const scrollElRef = React.useRef<ScrollView>(null)
const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads const onMainScroll = useOnMainScroll(store)
const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
const onSoftReset = () => { const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0}) scrollElRef.current?.scrollTo({x: 0, y: 0})
} }
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset) const softResetSub = store.onScreenSoftReset(onSoftReset)
const now = Date.now() const now = Date.now()
if (now - lastRenderTime > FIVE_MIN) { if (now - lastRenderTime > FIVE_MIN) {
setRenderTime(Date.now()) // trigger reload of suggestions setRenderTime(Date.now()) // trigger reload of suggestions
} }
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
return () => { return () => {
softResetSub.remove() softResetSub.remove()
} }
}, [store, lastRenderTime, setRenderTime]), }, [store, lastRenderTime, setRenderTime]),
) )
return ( return (
<ScrollView <ScrollView
ref={scrollElRef} ref={scrollElRef}
testID="searchScrollView" testID="searchScrollView"
style={[pal.view, styles.container]} style={[pal.view, styles.container]}
onScroll={onMainScroll} onScroll={onMainScroll}
scrollEventThrottle={100}> scrollEventThrottle={100}>
<WhoToFollow key={`wtf-${lastRenderTime}`} /> <WhoToFollow key={`wtf-${lastRenderTime}`} />
<SuggestedPosts key={`sp-${lastRenderTime}`} /> <SuggestedPosts key={`sp-${lastRenderTime}`} />
<View style={s.footerSpacer} /> <View style={s.footerSpacer} />
</ScrollView> </ScrollView>
) )
}) }),
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -16,6 +16,7 @@ import {
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import * as AppInfo from 'lib/app-info' import * as AppInfo from 'lib/app-info'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
@ -33,235 +34,237 @@ import {useAnalytics} from 'lib/analytics'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export const SettingsScreen = observer(function Settings({}: Props) { export const SettingsScreen = withAuthRequired(
const theme = useTheme() observer(function Settings({}: Props) {
const pal = usePalette('default') const theme = useTheme()
const store = useStores() const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>() const store = useStores()
const {screen, track} = useAnalytics() const navigation = useNavigation<NavigationProp>()
const [isSwitching, setIsSwitching] = React.useState(false) const {screen, track} = useAnalytics()
const [isSwitching, setIsSwitching] = React.useState(false)
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('Settings') screen('Settings')
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
}, [screen, store]), }, [screen, store]),
) )
const onPressSwitchAccount = async (acct: AccountData) => { const onPressSwitchAccount = async (acct: AccountData) => {
track('Settings:SwitchAccountButtonClicked') track('Settings:SwitchAccountButtonClicked')
setIsSwitching(true) setIsSwitching(true)
if (await store.session.resumeSession(acct)) { if (await store.session.resumeSession(acct)) {
setIsSwitching(false)
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
return
}
setIsSwitching(false) setIsSwitching(false)
Toast.show('Sorry! We need you to enter your password.')
navigation.navigate('HomeTab') navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop()) navigation.dispatch(StackActions.popToTop())
Toast.show(`Signed in as ${acct.displayName || acct.handle}`) store.session.clear()
return }
const onPressAddAccount = () => {
track('Settings:AddAccountButtonClicked')
store.session.clear()
}
const onPressChangeHandle = () => {
track('Settings:ChangeHandleButtonClicked')
store.shell.openModal({
name: 'change-handle',
onChanged() {
setIsSwitching(true)
store.session.reloadFromServer().then(
() => {
setIsSwitching(false)
Toast.show('Your handle has been updated')
},
err => {
store.log.error(
'Failed to reload from server after handle update',
{err},
)
setIsSwitching(false)
},
)
},
})
}
const onPressSignout = () => {
track('Settings:SignOutButtonClicked')
store.session.logout()
}
const onPressDeleteAccount = () => {
store.shell.openModal({name: 'delete-account'})
} }
setIsSwitching(false)
Toast.show('Sorry! We need you to enter your password.')
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
store.session.clear()
}
const onPressAddAccount = () => {
track('Settings:AddAccountButtonClicked')
store.session.clear()
}
const onPressChangeHandle = () => {
track('Settings:ChangeHandleButtonClicked')
store.shell.openModal({
name: 'change-handle',
onChanged() {
setIsSwitching(true)
store.session.reloadFromServer().then(
() => {
setIsSwitching(false)
Toast.show('Your handle has been updated')
},
err => {
store.log.error(
'Failed to reload from server after handle update',
{err},
)
setIsSwitching(false)
},
)
},
})
}
const onPressSignout = () => {
track('Settings:SignOutButtonClicked')
store.session.logout()
}
const onPressDeleteAccount = () => {
store.shell.openModal({name: 'delete-account'})
}
return ( return (
<View style={[s.hContentRegion]} testID="settingsScreen"> <View style={[s.hContentRegion]} testID="settingsScreen">
<ViewHeader title="Settings" /> <ViewHeader title="Settings" />
<ScrollView style={s.hContentRegion}> <ScrollView style={s.hContentRegion}>
<View style={styles.spacer20} /> <View style={styles.spacer20} />
<View style={[s.flexRow, styles.heading]}> <View style={[s.flexRow, styles.heading]}>
<Text type="xl-bold" style={pal.text}> <Text type="xl-bold" style={pal.text}>
Signed in as Signed in as
</Text> </Text>
<View style={s.flex1} /> <View style={s.flex1} />
</View>
{isSwitching ? (
<View style={[pal.view, styles.linkCard]}>
<ActivityIndicator />
</View> </View>
) : ( {isSwitching ? (
<Link
href={`/profile/${store.me.handle}`}
title="Your profile"
noFeedback>
<View style={[pal.view, styles.linkCard]}> <View style={[pal.view, styles.linkCard]}>
<ActivityIndicator />
</View>
) : (
<Link
href={`/profile/${store.me.handle}`}
title="Your profile"
noFeedback>
<View style={[pal.view, styles.linkCard]}>
<View style={styles.avi}>
<UserAvatar size={40} avatar={store.me.avatar} />
</View>
<View style={[s.flex1]}>
<Text type="md-bold" style={pal.text} numberOfLines={1}>
{store.me.displayName || store.me.handle}
</Text>
<Text type="sm" style={pal.textLight} numberOfLines={1}>
{store.me.handle}
</Text>
</View>
<TouchableOpacity
testID="signOutBtn"
onPress={isSwitching ? undefined : onPressSignout}>
<Text type="lg" style={pal.link}>
Sign out
</Text>
</TouchableOpacity>
</View>
</Link>
)}
{store.session.switchableAccounts.map(account => (
<TouchableOpacity
testID={`switchToAccountBtn-${account.handle}`}
key={account.did}
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
onPress={
isSwitching ? undefined : () => onPressSwitchAccount(account)
}>
<View style={styles.avi}> <View style={styles.avi}>
<UserAvatar size={40} avatar={store.me.avatar} /> <UserAvatar size={40} avatar={account.aviUrl} />
</View> </View>
<View style={[s.flex1]}> <View style={[s.flex1]}>
<Text type="md-bold" style={pal.text} numberOfLines={1}> <Text type="md-bold" style={pal.text}>
{store.me.displayName || store.me.handle} {account.displayName || account.handle}
</Text> </Text>
<Text type="sm" style={pal.textLight} numberOfLines={1}> <Text type="sm" style={pal.textLight}>
{store.me.handle} {account.handle}
</Text> </Text>
</View> </View>
<TouchableOpacity <AccountDropdownBtn handle={account.handle} />
testID="signOutBtn" </TouchableOpacity>
onPress={isSwitching ? undefined : onPressSignout}> ))}
<Text type="lg" style={pal.link}>
Sign out
</Text>
</TouchableOpacity>
</View>
</Link>
)}
{store.session.switchableAccounts.map(account => (
<TouchableOpacity <TouchableOpacity
testID={`switchToAccountBtn-${account.handle}`} testID="switchToNewAccountBtn"
key={account.did} style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]} onPress={isSwitching ? undefined : onPressAddAccount}>
onPress={ <View style={[styles.iconContainer, pal.btn]}>
isSwitching ? undefined : () => onPressSwitchAccount(account) <FontAwesomeIcon
}> icon="plus"
<View style={styles.avi}> style={pal.text as FontAwesomeIconStyle}
<UserAvatar size={40} avatar={account.aviUrl} /> />
</View> </View>
<View style={[s.flex1]}> <Text type="lg" style={pal.text}>
<Text type="md-bold" style={pal.text}> Add account
{account.displayName || account.handle} </Text>
</Text>
<Text type="sm" style={pal.textLight}>
{account.handle}
</Text>
</View>
<AccountDropdownBtn handle={account.handle} />
</TouchableOpacity> </TouchableOpacity>
))}
<TouchableOpacity <View style={styles.spacer20} />
testID="switchToNewAccountBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} <Text type="xl-bold" style={[pal.text, styles.heading]}>
onPress={isSwitching ? undefined : onPressAddAccount}> Advanced
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="plus"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
Add account
</Text> </Text>
</TouchableOpacity> <TouchableOpacity
testID="changeHandleBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressChangeHandle}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="at"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
Change my handle
</Text>
</TouchableOpacity>
<View style={styles.spacer20} /> <View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}> <Text type="xl-bold" style={[pal.text, styles.heading]}>
Advanced Danger zone
</Text>
<TouchableOpacity
testID="changeHandleBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressChangeHandle}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="at"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
Change my handle
</Text> </Text>
</TouchableOpacity> <TouchableOpacity
style={[pal.view, styles.linkCard]}
<View style={styles.spacer20} /> onPress={onPressDeleteAccount}>
<View
<Text type="xl-bold" style={[pal.text, styles.heading]}> style={[
Danger zone styles.iconContainer,
</Text> theme.colorScheme === 'dark'
<TouchableOpacity ? styles.trashIconContainerDark
style={[pal.view, styles.linkCard]} : styles.trashIconContainerLight,
onPress={onPressDeleteAccount}> ]}>
<View <FontAwesomeIcon
style={[ icon={['far', 'trash-can']}
styles.iconContainer, style={
theme.colorScheme === 'dark' theme.colorScheme === 'dark'
? styles.trashIconContainerDark ? styles.dangerDark
: styles.trashIconContainerLight, : styles.dangerLight
]}> }
<FontAwesomeIcon size={21}
icon={['far', 'trash-can']} />
</View>
<Text
type="lg"
style={ style={
theme.colorScheme === 'dark' theme.colorScheme === 'dark'
? styles.dangerDark ? styles.dangerDark
: styles.dangerLight : styles.dangerLight
} }>
size={21} Delete my account
/> </Text>
</View> </TouchableOpacity>
<Text
type="lg"
style={
theme.colorScheme === 'dark'
? styles.dangerDark
: styles.dangerLight
}>
Delete my account
</Text>
</TouchableOpacity>
<View style={styles.spacer20} /> <View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}> <Text type="xl-bold" style={[pal.text, styles.heading]}>
Developer tools Developer tools
</Text>
<Link
style={[pal.view, styles.linkCardNoIcon]}
href="/sys/log"
title="System log">
<Text type="lg" style={pal.text}>
System log
</Text> </Text>
</Link> <Link
<Link style={[pal.view, styles.linkCardNoIcon]}
style={[pal.view, styles.linkCardNoIcon]} href="/sys/log"
href="/sys/debug" title="System log">
title="Debug tools"> <Text type="lg" style={pal.text}>
<Text type="lg" style={pal.text}> System log
Storybook </Text>
</Link>
<Link
style={[pal.view, styles.linkCardNoIcon]}
href="/sys/debug"
title="Debug tools">
<Text type="lg" style={pal.text}>
Storybook
</Text>
</Link>
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
Build version {AppInfo.appVersion} ({AppInfo.buildVersion})
</Text> </Text>
</Link> <View style={s.footerSpacer} />
<Text type="sm" style={[styles.buildInfo, pal.textLight]}> </ScrollView>
Build version {AppInfo.appVersion} ({AppInfo.buildVersion}) </View>
</Text> )
<View style={s.footerSpacer} /> }),
</ScrollView> )
</View>
)
})
function AccountDropdownBtn({handle}: {handle: string}) { function AccountDropdownBtn({handle}: {handle: string}) {
const store = useStores() const store = useStores()

View File

@ -131,7 +131,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
return ( return (
<View style={styles.leftNav}> <View style={styles.leftNav}>
<ProfileCard /> {store.session.hasSession && <ProfileCard />}
<BackBtn /> <BackBtn />
<NavItem <NavItem
href="/" href="/"
@ -164,14 +164,16 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
} }
label="Notifications" label="Notifications"
/> />
<NavItem {store.session.hasSession && (
href={`/profile/${store.me.handle}`} <NavItem
icon={<UserIcon strokeWidth={1.75} size={28} style={pal.text} />} href={`/profile/${store.me.handle}`}
iconFilled={ icon={<UserIcon strokeWidth={1.75} size={28} style={pal.text} />}
<UserIconSolid strokeWidth={1.75} size={28} style={pal.text} /> iconFilled={
} <UserIconSolid strokeWidth={1.75} size={28} style={pal.text} />
label="Profile" }
/> label="Profile"
/>
)}
<NavItem <NavItem
href="/settings" href="/settings"
icon={<CogIcon strokeWidth={1.75} size={28} style={pal.text} />} icon={<CogIcon strokeWidth={1.75} size={28} style={pal.text} />}
@ -180,7 +182,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
} }
label="Settings" label="Settings"
/> />
<ComposeBtn /> {store.session.hasSession && <ComposeBtn />}
</View> </View>
) )
}) })

View File

@ -7,12 +7,14 @@ import {Text} from 'view/com/util/text/Text'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {FEEDBACK_FORM_URL} from 'lib/constants' import {FEEDBACK_FORM_URL} from 'lib/constants'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useStores} from 'state/index'
export const DesktopRightNav = observer(function DesktopRightNav() { export const DesktopRightNav = observer(function DesktopRightNav() {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
<View style={[styles.rightNav, pal.view]}> <View style={[styles.rightNav, pal.view]}>
<DesktopSearch /> {store.session.hasSession && <DesktopSearch />}
<View style={styles.message}> <View style={styles.message}>
<Text type="md" style={[pal.textLight, styles.messageLine]}> <Text type="md" style={[pal.textLight, styles.messageLine]}>
Welcome to Bluesky! This is a beta application that's still in Welcome to Bluesky! This is a beta application that's still in

View File

@ -5,7 +5,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Drawer} from 'react-native-drawer-layout' import {Drawer} from 'react-native-drawer-layout'
import {useNavigationState} from '@react-navigation/native' import {useNavigationState} from '@react-navigation/native'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {Login} from 'view/screens/Login'
import {ModalsContainer} from 'view/com/modals/Modal' import {ModalsContainer} from 'view/com/modals/Modal'
import {Lightbox} from 'view/com/lightbox/Lightbox' import {Lightbox} from 'view/com/lightbox/Lightbox'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
@ -104,20 +103,6 @@ export const Shell: React.FC = observer(() => {
) )
} }
if (!store.session.hasSession) {
return (
<View style={styles.outerContainer}>
<StatusBar
barStyle={
theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
}
/>
<Login />
<ModalsContainer />
</View>
)
}
return ( return (
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
<StatusBar <StatusBar

View File

@ -4,7 +4,6 @@ import {View, StyleSheet} from 'react-native'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav' import {DesktopRightNav} from './desktop/RightNav'
import {Login} from '../screens/Login'
import {ErrorBoundary} from '../com/util/ErrorBoundary' import {ErrorBoundary} from '../com/util/ErrorBoundary'
import {Lightbox} from '../com/lightbox/Lightbox' import {Lightbox} from '../com/lightbox/Lightbox'
import {ModalsContainer} from '../com/modals/Modal' import {ModalsContainer} from '../com/modals/Modal'
@ -45,21 +44,10 @@ const ShellInner = observer(() => {
export const Shell: React.FC = observer(() => { export const Shell: React.FC = observer(() => {
const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
const store = useStores()
if (isMobileWeb) { if (isMobileWeb) {
return <NoMobileWeb /> return <NoMobileWeb />
} }
if (!store.session.hasSession) {
return (
<View style={[s.hContentRegion, pageBg]}>
<Login />
<ModalsContainer />
</View>
)
}
return ( return (
<View style={[s.hContentRegion, pageBg]}> <View style={[s.hContentRegion, pageBg]}>
<RoutesContainer> <RoutesContainer>