Rework logged out state to preserve routing and work for web
parent
b5c64a03b6
commit
774fb83719
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
})
|
|
@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue