This commit is contained in:
Paul Frazee
2023-03-10 14:14:39 -06:00
parent 5accb619ac
commit b2300a8381
10 changed files with 277 additions and 258 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ export function useCameraPermission() {
const [cameraPermissionStatus] = Camera.useCameraPermissions() const [cameraPermissionStatus] = Camera.useCameraPermissions()
const requestCameraAccessIfNeeded = async () => { const requestCameraAccessIfNeeded = async () => {
if (cameraPermissionStatus.granted) { if (cameraPermissionStatus?.granted) {
return true return true
} else { } else {
openPermissionAlert('camera') openPermissionAlert('camera')
+1 -1
View File
@@ -15,7 +15,7 @@ import {State, NavigationProp} from './types'
// } // }
export function getCurrentRoute(state: State) { export function getCurrentRoute(state: State) {
let node = state.routes[state.index] let node = state.routes[state.index || 0]
while (node.state?.routes && typeof node.state?.index === 'number') { while (node.state?.routes && typeof node.state?.index === 'number') {
node = node.state?.routes[node.state?.index] node = node.state?.routes[node.state?.index]
} }
+14 -2
View File
@@ -19,14 +19,26 @@ export type HomeTabNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
} }
export type NotificationsTabNavigatorParams = CommonNavigatorParams & { export type HomeDrawerNavigatorParams = {
Notifications: undefined HomeInner: undefined
} }
export type SearchTabNavigatorParams = CommonNavigatorParams & { export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: undefined Search: undefined
} }
export type SearchDrawerNavigatorParams = {
SearchInner: undefined
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined
}
export type NotificationsDrawerNavigatorParams = {
NotificationsInner: undefined
}
// NOTE // NOTE
// this isn't strictly correct but it should be close enough // this isn't strictly correct but it should be close enough
// a TS wizard might be able to get this 100% // a TS wizard might be able to get this 100%
+35 -31
View File
@@ -6,8 +6,11 @@ import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
import { import {
HomeTabNavigatorParams, HomeTabNavigatorParams,
NotificationsTabNavigatorParams, HomeDrawerNavigatorParams,
SearchTabNavigatorParams, SearchTabNavigatorParams,
SearchDrawerNavigatorParams,
NotificationsTabNavigatorParams,
NotificationsDrawerNavigatorParams,
State, State,
} from 'lib/routes/types' } from 'lib/routes/types'
@@ -27,11 +30,12 @@ import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {DebugScreen} from './view/screens/Debug' import {DebugScreen} from './view/screens/Debug'
import {LogScreen} from './view/screens/Log' import {LogScreen} from './view/screens/Log'
const HomeDrawer = createDrawerNavigator() const HomeDrawer = createDrawerNavigator<HomeDrawerNavigatorParams>()
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>() const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
const SearchDrawer = createDrawerNavigator() const SearchDrawer = createDrawerNavigator<SearchDrawerNavigatorParams>()
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>() const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
const NotificationsDrawer = createDrawerNavigator() const NotificationsDrawer =
createDrawerNavigator<NotificationsDrawerNavigatorParams>()
const NotificationsTab = const NotificationsTab =
createNativeStackNavigator<NotificationsTabNavigatorParams>() createNativeStackNavigator<NotificationsTabNavigatorParams>()
const Tab = createBottomTabNavigator() const Tab = createBottomTabNavigator()
@@ -139,7 +143,7 @@ function buildStateObject(stack: string, route: string, params: RouteParams) {
} }
} }
function commonScreens(Stack: ReturnType<typeof createNativeStackNavigator>) { function commonScreens(Stack: typeof HomeTab) {
return ( return (
<> <>
<Stack.Screen name="Settings" component={SettingsScreen} /> <Stack.Screen name="Settings" component={SettingsScreen} />
@@ -183,6 +187,31 @@ function HomeTabNavigator() {
) )
} }
function SearchDrawerNavigator() {
const drawerContent = React.useCallback(props => <Drawer {...props} />, [])
return (
<SearchDrawer.Navigator
drawerContent={drawerContent}
screenOptions={{swipeEdgeWidth: 300, headerShown: false}}>
<SearchDrawer.Screen name="SearchInner" component={SearchScreen} />
</SearchDrawer.Navigator>
)
}
function SearchTabNavigator() {
return (
<SearchTab.Navigator
screenOptions={{
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
}}>
<SearchTab.Screen name="Search" component={SearchDrawerNavigator} />
{commonScreens(SearchTab as typeof HomeTab)}
</SearchTab.Navigator>
)
}
function NotificationsDrawerNavigator() { function NotificationsDrawerNavigator() {
const drawerContent = React.useCallback(props => <Drawer {...props} />, []) const drawerContent = React.useCallback(props => <Drawer {...props} />, [])
return ( return (
@@ -209,36 +238,11 @@ function NotificationsTabNavigator() {
name="Notifications" name="Notifications"
component={NotificationsDrawerNavigator} component={NotificationsDrawerNavigator}
/> />
{commonScreens(NotificationsTab)} {commonScreens(NotificationsTab as typeof HomeTab)}
</NotificationsTab.Navigator> </NotificationsTab.Navigator>
) )
} }
function SearchDrawerNavigator() {
const drawerContent = React.useCallback(props => <Drawer {...props} />, [])
return (
<SearchDrawer.Navigator
drawerContent={drawerContent}
screenOptions={{swipeEdgeWidth: 300, headerShown: false}}>
<SearchDrawer.Screen name="SearchInner" component={SearchScreen} />
</SearchDrawer.Navigator>
)
}
function SearchTabNavigator() {
return (
<SearchTab.Navigator
screenOptions={{
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
}}>
<SearchTab.Screen name="Search" component={SearchDrawerNavigator} />
{commonScreens(SearchTab)}
</SearchTab.Navigator>
)
}
export function TabsNavigator() { export function TabsNavigator() {
const tabBar = React.useCallback(props => <BottomBar {...props} />, []) const tabBar = React.useCallback(props => <BottomBar {...props} />, [])
return ( return (
+3
View File
@@ -117,8 +117,11 @@ function onPressInner(
shouldHandle = e ? !e.defaultPrevented : true shouldHandle = e ? !e.defaultPrevented : true
} else if ( } else if (
!e.defaultPrevented && // onPress prevented default !e.defaultPrevented && // onPress prevented default
// @ts-ignore Web only -prf
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
// @ts-ignore Web only -prf
(e.button == null || e.button === 0) && // ignore everything but left clicks (e.button == null || e.button === 0) && // ignore everything but left clicks
// @ts-ignore Web only -prf
[undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc. [undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
) { ) {
e.preventDefault() e.preventDefault()
+5 -2
View File
@@ -3,7 +3,10 @@ import {FlatList, View} from 'react-native'
import {useFocusEffect, useIsFocused} from '@react-navigation/native' 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,
HomeDrawerNavigatorParams,
} from 'lib/routes/types'
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'
@@ -17,7 +20,7 @@ import {ComposeIcon2} from 'lib/icons'
const HEADER_HEIGHT = 42 const HEADER_HEIGHT = 42
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeDrawerNavigatorParams, 'HomeInner'>
export const HomeScreen = observer(function Home(_opts: Props) { export const HomeScreen = observer(function Home(_opts: Props) {
const store = useStores() const store = useStores()
const onMainScroll = useOnMainScroll(store) const onMainScroll = useOnMainScroll(store)
+6 -5
View File
@@ -4,7 +4,7 @@ import {useFocusEffect} from '@react-navigation/native'
import useAppState from 'react-native-appstate-hook' import useAppState from 'react-native-appstate-hook'
import { import {
NativeStackScreenProps, NativeStackScreenProps,
NotificationsTabNavigatorParams, NotificationsDrawerNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
@@ -15,10 +15,11 @@ import {useAnalytics} from 'lib/analytics'
const NOTIFICATIONS_POLL_INTERVAL = 15e3 const NOTIFICATIONS_POLL_INTERVAL = 15e3
export const NotificationsScreen = ({}: NativeStackScreenProps< type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams, NotificationsDrawerNavigatorParams,
'Notifications' 'NotificationsInner'
>) => { >
export const NotificationsScreen = ({}: Props) => {
const store = useStores() const store = useStores()
const onMainScroll = useOnMainScroll(store) const onMainScroll = useOnMainScroll(store)
const scrollElRef = React.useRef<FlatList>(null) const scrollElRef = React.useRef<FlatList>(null)
+1 -1
View File
@@ -1,4 +1,4 @@
import React, {useEffect} 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 {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+3 -3
View File
@@ -15,7 +15,7 @@ import {
import {ScrollView} from '../com/util/Views' import {ScrollView} from '../com/util/Views'
import { import {
NativeStackScreenProps, NativeStackScreenProps,
SearchTabNavigatorParams, SearchDrawerNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {UserAvatar} from '../com/util/UserAvatar' import {UserAvatar} from '../com/util/UserAvatar'
@@ -34,8 +34,8 @@ import {useAnalytics} from 'lib/analytics'
const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} 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<SearchDrawerNavigatorParams, 'SearchInner'>
export const SearchScreen = observer(({}: Props) => { export const SearchScreen = observer<Props>(({}: Props) => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {track} = useAnalytics() const {track} = useAnalytics()
+208 -212
View File
@@ -32,250 +32,246 @@ import {AccountData} from 'state/models/session'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
export const SettingsScreen = observer( type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
function Settings({}: NativeStackScreenProps< export const SettingsScreen = observer(function Settings({}: Props) {
CommonNavigatorParams, const theme = useTheme()
'Settings' const pal = usePalette('default')
>) { const store = useStores()
const theme = useTheme() const navigation = useNavigation<NavigationProp>()
const pal = usePalette('default') const {screen, track} = useAnalytics()
const store = useStores() const [isSwitching, setIsSwitching] = React.useState(false)
const navigation = useNavigation<NavigationProp>()
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())
store.session.clear() Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
} 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}
displayName={store.me.displayName}
handle={store.me.handle || ''}
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 <UserAvatar
size={40} size={40}
displayName={account.displayName} displayName={store.me.displayName}
handle={account.handle || ''} handle={store.me.handle || ''}
avatar={account.aviUrl} avatar={store.me.avatar}
/> />
</View> </View>
<View style={[s.flex1]}> <View style={[s.flex1]}>
<Text type="md-bold" style={pal.text}> <Text type="md-bold" style={pal.text} numberOfLines={1}>
{account.displayName || account.handle} {store.me.displayName || store.me.handle}
</Text> </Text>
<Text type="sm" style={pal.textLight}> <Text type="sm" style={pal.textLight} numberOfLines={1}>
{account.handle} {store.me.handle}
</Text> </Text>
</View> </View>
<AccountDropdownBtn handle={account.handle} /> <TouchableOpacity
</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 <TouchableOpacity
testID="switchToNewAccountBtn" testID={`switchToAccountBtn-${account.handle}`}
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} key={account.did}
onPress={isSwitching ? undefined : onPressAddAccount}> style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
<View style={[styles.iconContainer, pal.btn]}> onPress={
<FontAwesomeIcon isSwitching ? undefined : () => onPressSwitchAccount(account)
icon="plus" }>
style={pal.text as FontAwesomeIconStyle} <View style={styles.avi}>
<UserAvatar
size={40}
displayName={account.displayName}
handle={account.handle || ''}
avatar={account.aviUrl}
/> />
</View> </View>
<Text type="lg" style={pal.text}> <View style={[s.flex1]}>
Add account <Text type="md-bold" style={pal.text}>
</Text> {account.displayName || account.handle}
</Text>
<Text type="sm" style={pal.textLight}>
{account.handle}
</Text>
</View>
<AccountDropdownBtn handle={account.handle} />
</TouchableOpacity> </TouchableOpacity>
))}
<View style={styles.spacer20} /> <TouchableOpacity
testID="switchToNewAccountBtn"
<Text type="xl-bold" style={[pal.text, styles.heading]}> style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
Advanced onPress={isSwitching ? undefined : onPressAddAccount}>
<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]}>
Danger zone Advanced
</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]}
onPress={onPressDeleteAccount}> <View style={styles.spacer20} />
<View
style={[ <Text type="xl-bold" style={[pal.text, styles.heading]}>
styles.iconContainer, Danger zone
theme.colorScheme === 'dark' </Text>
? styles.trashIconContainerDark <TouchableOpacity
: styles.trashIconContainerLight, style={[pal.view, styles.linkCard]}
]}> onPress={onPressDeleteAccount}>
<FontAwesomeIcon <View
icon={['far', 'trash-can']} style={[
style={ styles.iconContainer,
theme.colorScheme === 'dark' theme.colorScheme === 'dark'
? styles.dangerDark ? styles.trashIconContainerDark
: styles.dangerLight : styles.trashIconContainerLight,
} ]}>
size={21} <FontAwesomeIcon
/> icon={['far', 'trash-can']}
</View>
<Text
type="lg"
style={ style={
theme.colorScheme === 'dark' theme.colorScheme === 'dark'
? styles.dangerDark ? styles.dangerDark
: styles.dangerLight : styles.dangerLight
}> }
Delete my account size={21}
</Text> />
</TouchableOpacity> </View>
<Text
<View style={styles.spacer20} /> type="lg"
style={
<Text type="xl-bold" style={[pal.text, styles.heading]}> theme.colorScheme === 'dark'
Developer tools ? styles.dangerDark
: styles.dangerLight
}>
Delete my account
</Text> </Text>
<Link </TouchableOpacity>
style={[pal.view, styles.linkCardNoIcon]}
href="/sys/log" <View style={styles.spacer20} />
title="System log">
<Text type="lg" style={pal.text}> <Text type="xl-bold" style={[pal.text, styles.heading]}>
System log Developer tools
</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>
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
Build version {AppInfo.appVersion} ({AppInfo.buildVersion})
</Text> </Text>
<View style={s.footerSpacer} /> </Link>
</ScrollView> <Link
</View> 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>
<View style={s.footerSpacer} />
</ScrollView>
</View>
)
})
function AccountDropdownBtn({handle}: {handle: string}) { function AccountDropdownBtn({handle}: {handle: string}) {
const store = useStores() const store = useStores()