Ensure the UI always renders, even in bad network conditions (close #6)
This commit is contained in:
parent
59363181e1
commit
f27e32e54c
13 changed files with 259 additions and 72 deletions
|
@ -1,14 +1,21 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {colors} from '../../lib/styles'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {MagnifyingGlassIcon} from '../../lib/icons'
|
||||
import {useStores} from '../../../state'
|
||||
|
||||
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
||||
const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
||||
|
||||
export function ViewHeader({
|
||||
export const ViewHeader = observer(function ViewHeader({
|
||||
title,
|
||||
subtitle,
|
||||
onPost,
|
||||
|
@ -27,43 +34,91 @@ export function ViewHeader({
|
|||
const onPressSearch = () => {
|
||||
store.nav.navigate(`/search`)
|
||||
}
|
||||
const onPressReconnect = () => {
|
||||
store.session.connect().catch(e => {
|
||||
// log for debugging but ignore otherwise
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
{store.nav.tab.canGoBack ? (
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
{store.nav.tab.canGoBack ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={styles.backIcon}>
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={{marginTop: 6}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
{subtitle ? (
|
||||
<Text style={styles.subtitle} numberOfLines={1}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={styles.backIcon}>
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} />
|
||||
onPress={onPressCompose}
|
||||
hitSlop={HITSLOP}
|
||||
style={styles.btn}>
|
||||
<FontAwesomeIcon size={18} icon="plus" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onPressSearch}
|
||||
hitSlop={HITSLOP}
|
||||
style={[styles.btn, {marginLeft: 8}]}>
|
||||
<MagnifyingGlassIcon
|
||||
size={18}
|
||||
strokeWidth={3}
|
||||
style={styles.searchBtnIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{!store.session.online ? (
|
||||
<TouchableOpacity style={styles.offline} onPress={onPressReconnect}>
|
||||
{store.session.attemptingConnect ? (
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Text style={[s.gray1, s.bold, s.flex1, s.pl5, s.pt5, s.pb5]}>
|
||||
Connecting...
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon="signal" style={[s.gray2]} size={18} />
|
||||
<FontAwesomeIcon
|
||||
icon="x"
|
||||
style={[
|
||||
s.red4,
|
||||
{
|
||||
backgroundColor: colors.gray6,
|
||||
position: 'relative',
|
||||
left: -4,
|
||||
top: 6,
|
||||
},
|
||||
]}
|
||||
border
|
||||
size={12}
|
||||
/>
|
||||
<Text style={[s.gray1, s.bold, s.flex1, s.pl2]}>
|
||||
Unable to connect
|
||||
</Text>
|
||||
<View style={styles.offlineBtn}>
|
||||
<Text style={styles.offlineBtnText}>Try again</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
{subtitle ? (
|
||||
<Text style={styles.subtitle} numberOfLines={1}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={onPressCompose}
|
||||
hitSlop={HITSLOP}
|
||||
style={styles.btn}>
|
||||
<FontAwesomeIcon size={18} icon="plus" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onPressSearch}
|
||||
hitSlop={HITSLOP}
|
||||
style={[styles.btn, {marginLeft: 8}]}>
|
||||
<MagnifyingGlassIcon
|
||||
size={18}
|
||||
strokeWidth={3}
|
||||
style={styles.searchBtnIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
|
@ -108,4 +163,26 @@ const styles = StyleSheet.create({
|
|||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
|
||||
offline: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.gray6,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 4,
|
||||
marginTop: 4,
|
||||
},
|
||||
offlineBtn: {
|
||||
backgroundColor: colors.gray5,
|
||||
borderRadius: 5,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
offlineBtnText: {
|
||||
color: colors.white,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -45,6 +45,7 @@ import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
|
|||
import {faShare} from '@fortawesome/free-solid-svg-icons/faShare'
|
||||
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
|
||||
import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
|
||||
import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal'
|
||||
import {faReply} from '@fortawesome/free-solid-svg-icons/faReply'
|
||||
import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
|
||||
import {faRss} from '@fortawesome/free-solid-svg-icons/faRss'
|
||||
|
@ -110,6 +111,7 @@ export function setup() {
|
|||
faShare,
|
||||
faShareFromSquare,
|
||||
faShield,
|
||||
faSignal,
|
||||
faUser,
|
||||
faUsers,
|
||||
faUserCheck,
|
||||
|
|
|
@ -10,6 +10,9 @@ export const colors = {
|
|||
gray3: '#c1b9b9',
|
||||
gray4: '#968d8d',
|
||||
gray5: '#645454',
|
||||
gray6: '#423737',
|
||||
gray7: '#2D2626',
|
||||
gray8: '#131010',
|
||||
|
||||
blue0: '#bfe1ff',
|
||||
blue1: '#8bc7fd',
|
||||
|
@ -131,6 +134,7 @@ export const s = StyleSheet.create({
|
|||
flexRow: {flexDirection: 'row'},
|
||||
flexCol: {flexDirection: 'column'},
|
||||
flex1: {flex: 1},
|
||||
alignCenter: {alignItems: 'center'},
|
||||
|
||||
// position
|
||||
absolute: {position: 'absolute'},
|
||||
|
|
|
@ -25,6 +25,7 @@ import {useStores, DEFAULT_SERVICE} from '../../state'
|
|||
import {ServiceDescription} from '../../state/models/session'
|
||||
import {ServerInputModel} from '../../state/models/shell-ui'
|
||||
import {ComAtprotoAccountCreate} from '../../third-party/api/index'
|
||||
import {isNetworkError} from '../../lib/errors'
|
||||
|
||||
enum ScreenState {
|
||||
SigninOrCreateAccount,
|
||||
|
@ -186,7 +187,7 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
setIsProcessing(false)
|
||||
if (errMsg.includes('Authentication Required')) {
|
||||
setError('Invalid username or password')
|
||||
} else if (errMsg.includes('Network request failed')) {
|
||||
} else if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Unable to contact your service. Please check your Internet connection.',
|
||||
)
|
||||
|
@ -210,16 +211,6 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
</Text>
|
||||
<FontAwesomeIcon icon="pen" size={10} style={styles.groupTitleIcon} />
|
||||
</TouchableOpacity>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
|
||||
<TextInput
|
||||
|
@ -249,18 +240,31 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity onPress={onPressNext}>
|
||||
{isProcessing ? (
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<Text style={[s.white, s.f18, s.pl10]}>Connecting...</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
|
@ -689,18 +693,19 @@ const styles = StyleSheet.create({
|
|||
color: colors.white,
|
||||
},
|
||||
error: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.blue1,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.red5,
|
||||
backgroundColor: colors.red4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 5,
|
||||
backgroundColor: colors.blue2,
|
||||
marginTop: -5,
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 15,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 5,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
errorFloating: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.blue1,
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
|
|
|
@ -9,7 +9,7 @@ export const DesktopWebShell: React.FC = observer(({children}) => {
|
|||
const store = useStores()
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
{store.session.isAuthed ? (
|
||||
{store.session.hasSession ? (
|
||||
<>
|
||||
<DesktopLeftColumn />
|
||||
<View style={styles.innerContainer}>{children}</View>
|
||||
|
|
|
@ -231,7 +231,7 @@ export const MobileShell: React.FC = observer(() => {
|
|||
transform: [{scale: newTabInterp.value}],
|
||||
}))
|
||||
|
||||
if (!store.session.isAuthed) {
|
||||
if (!store.session.hasSession) {
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={['#007CFF', '#00BCFF']}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue