Improve basic screen components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-04 21:49:19 +02:00
parent 4db4516296
commit 0117b25cd8
3 changed files with 425 additions and 411 deletions

View file

@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {ActivityIndicator, withTheme} from 'react-native-paper'; import {ActivityIndicator, withTheme} from 'react-native-paper';
import type {CustomTheme} from '../../managers/ThemeManager';
/** /**
* Component used to display a header button * Component used to display a header button
@ -10,26 +11,27 @@ import {ActivityIndicator, withTheme} from 'react-native-paper';
* @param props Props to pass to the component * @param props Props to pass to the component
* @return {*} * @return {*}
*/ */
function BasicLoadingScreen(props) { function BasicLoadingScreen(props: {
const {colors} = props.theme; theme: CustomTheme,
let position = undefined; isAbsolute: boolean,
if (props.isAbsolute !== undefined && props.isAbsolute) }): React.Node {
position = 'absolute'; const {theme, isAbsolute} = props;
const {colors} = theme;
let position;
if (isAbsolute != null && isAbsolute) position = 'absolute';
return ( return (
<View style={{ <View
style={{
backgroundColor: colors.background, backgroundColor: colors.background,
position: position, position,
top: 0, top: 0,
right: 0, right: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
justifyContent: 'center', justifyContent: 'center',
}}> }}>
<ActivityIndicator <ActivityIndicator animating size="large" color={colors.primary} />
animating={true}
size={'large'}
color={colors.primary}/>
</View> </View>
); );
} }

View file

@ -2,168 +2,25 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Subheading, withTheme} from 'react-native-paper'; import {Button, Subheading, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from 'react-native';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {ERROR_TYPE} from "../../utils/WebData";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack';
import {ERROR_TYPE} from '../../utils/WebData';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
navigation: Object, navigation: StackNavigationProp,
route: Object, theme: CustomTheme,
errorCode: number, route: {name: string},
onRefresh: Function, onRefresh?: () => void,
icon: string, errorCode?: number,
message: string, icon?: string,
showRetryButton: boolean, message?: string,
} showRetryButton?: boolean,
type State = {
refreshing: boolean,
}
class ErrorView extends React.PureComponent<Props, State> {
colors: Object;
message: string;
icon: string;
showLoginButton: boolean;
static defaultProps = {
errorCode: 0,
icon: '',
message: '',
showRetryButton: true,
}
state = {
refreshing: false,
}; };
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.icon = "";
}
generateMessage() {
this.showLoginButton = false;
if (this.props.errorCode !== 0) {
switch (this.props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
this.message = i18n.t("errors.badCredentials");
this.icon = "account-alert-outline";
break;
case ERROR_TYPE.BAD_TOKEN:
this.message = i18n.t("errors.badToken");
this.icon = "account-alert-outline";
this.showLoginButton = true;
break;
case ERROR_TYPE.NO_CONSENT:
this.message = i18n.t("errors.noConsent");
this.icon = "account-remove-outline";
break;
case ERROR_TYPE.TOKEN_SAVE:
this.message = i18n.t("errors.tokenSave");
this.icon = "alert-circle-outline";
break;
case ERROR_TYPE.BAD_INPUT:
this.message = i18n.t("errors.badInput");
this.icon = "alert-circle-outline";
break;
case ERROR_TYPE.FORBIDDEN:
this.message = i18n.t("errors.forbidden");
this.icon = "lock";
break;
case ERROR_TYPE.CONNECTION_ERROR:
this.message = i18n.t("errors.connectionError");
this.icon = "access-point-network-off";
break;
case ERROR_TYPE.SERVER_ERROR:
this.message = i18n.t("errors.serverError");
this.icon = "server-network-off";
break;
default:
this.message = i18n.t("errors.unknown");
this.icon = "alert-circle-outline";
break;
}
this.message += "\n\nCode " + this.props.errorCode;
} else {
this.message = this.props.message;
this.icon = this.props.icon;
}
}
getRetryButton() {
return <Button
mode={'contained'}
icon={'refresh'}
onPress={this.props.onRefresh}
style={styles.button}
>
{i18n.t("general.retry")}
</Button>;
}
goToLogin = () => {
this.props.navigation.navigate("login",
{
screen: 'login',
params: {nextScreen: this.props.route.name}
})
};
getLoginButton() {
return <Button
mode={'contained'}
icon={'login'}
onPress={this.goToLogin}
style={styles.button}
>
{i18n.t("screens.login.title")}
</Button>;
}
render() {
this.generateMessage();
return (
<Animatable.View
style={{
...styles.outer,
backgroundColor: this.colors.background
}}
animation={"zoomIn"}
duration={200}
useNativeDriver
>
<View style={styles.inner}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={this.icon}
size={150}
color={this.colors.textDisabled}/>
</View>
<Subheading style={{
...styles.subheading,
color: this.colors.textDisabled
}}>
{this.message}
</Subheading>
{this.props.showRetryButton
? (this.showLoginButton
? this.getLoginButton()
: this.getRetryButton())
: null}
</View>
</Animatable.View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
height: '100%', height: '100%',
@ -175,18 +32,159 @@ const styles = StyleSheet.create({
iconContainer: { iconContainer: {
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginBottom: 20 marginBottom: 20,
}, },
subheading: { subheading: {
textAlign: 'center', textAlign: 'center',
paddingHorizontal: 20 paddingHorizontal: 20,
}, },
button: { button: {
marginTop: 10, marginTop: 10,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
} },
}); });
class ErrorView extends React.PureComponent<PropsType> {
static defaultProps = {
onRefresh: (): void => null,
errorCode: 0,
icon: '',
message: '',
showRetryButton: true,
};
message: string;
icon: string;
showLoginButton: boolean;
constructor(props: PropsType) {
super(props);
this.icon = '';
}
getRetryButton(): React.Node {
const {props} = this;
return (
<Button
mode="contained"
icon="refresh"
onPress={props.onRefresh}
style={styles.button}>
{i18n.t('general.retry')}
</Button>
);
}
getLoginButton(): React.Node {
return (
<Button
mode="contained"
icon="login"
onPress={this.goToLogin}
style={styles.button}>
{i18n.t('screens.login.title')}
</Button>
);
}
goToLogin = () => {
const {props} = this;
props.navigation.navigate('login', {
screen: 'login',
params: {nextScreen: props.route.name},
});
};
generateMessage() {
const {props} = this;
this.showLoginButton = false;
if (props.errorCode !== 0) {
switch (props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
this.message = i18n.t('errors.badCredentials');
this.icon = 'account-alert-outline';
break;
case ERROR_TYPE.BAD_TOKEN:
this.message = i18n.t('errors.badToken');
this.icon = 'account-alert-outline';
this.showLoginButton = true;
break;
case ERROR_TYPE.NO_CONSENT:
this.message = i18n.t('errors.noConsent');
this.icon = 'account-remove-outline';
break;
case ERROR_TYPE.TOKEN_SAVE:
this.message = i18n.t('errors.tokenSave');
this.icon = 'alert-circle-outline';
break;
case ERROR_TYPE.BAD_INPUT:
this.message = i18n.t('errors.badInput');
this.icon = 'alert-circle-outline';
break;
case ERROR_TYPE.FORBIDDEN:
this.message = i18n.t('errors.forbidden');
this.icon = 'lock';
break;
case ERROR_TYPE.CONNECTION_ERROR:
this.message = i18n.t('errors.connectionError');
this.icon = 'access-point-network-off';
break;
case ERROR_TYPE.SERVER_ERROR:
this.message = i18n.t('errors.serverError');
this.icon = 'server-network-off';
break;
default:
this.message = i18n.t('errors.unknown');
this.icon = 'alert-circle-outline';
break;
}
this.message += `\n\nCode ${props.errorCode}`;
} else {
this.message = props.message;
this.icon = props.icon;
}
}
render(): React.Node {
const {props} = this;
this.generateMessage();
let button;
if (this.showLoginButton) button = this.getLoginButton();
else if (props.showRetryButton) button = this.getRetryButton();
else button = null;
return (
<Animatable.View
style={{
...styles.outer,
backgroundColor: props.theme.colors.background,
}}
animation="zoomIn"
duration={200}
useNativeDriver>
<View style={styles.inner}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={this.icon}
size={150}
color={props.theme.colors.textDisabled}
/>
</View>
<Subheading
style={{
...styles.subheading,
color: props.theme.colors.textDisabled,
}}>
{this.message}
</Subheading>
{button}
</View>
</Animatable.View>
);
}
}
export default withTheme(ErrorView); export default withTheme(ErrorView);

View file

@ -1,40 +1,43 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import WebView from "react-native-webview"; import WebView from 'react-native-webview';
import BasicLoadingScreen from "./BasicLoadingScreen"; import {
import ErrorView from "./ErrorView"; Divider,
import {ERROR_TYPE} from "../../utils/WebData"; HiddenItem,
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton'; OverflowMenu,
import {Divider, HiddenItem, OverflowMenu} from "react-navigation-header-buttons"; } from 'react-navigation-header-buttons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Animated, BackHandler, Linking} from "react-native"; import {Animated, BackHandler, Linking} from 'react-native';
import {withCollapsible} from "../../utils/withCollapsible"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import {withTheme} from 'react-native-paper';
import {withTheme} from "react-native-paper"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../managers/ThemeManager"; import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from "@react-navigation/stack"; import type {CustomTheme} from '../../managers/ThemeManager';
import {Collapsible} from "react-navigation-collapsible"; import {withCollapsible} from '../../utils/withCollapsible';
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData';
import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
url: string, url: string,
customJS: string,
customPaddingFunction: null | (padding: number) => string,
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
onMessage: Function, onMessage: (event: {nativeEvent: {data: string}}) => void,
onScroll: Function, onScroll: (event: SyntheticEvent<EventTarget>) => void,
showAdvancedControls: boolean, customJS?: string,
} customPaddingFunction?: null | ((padding: number) => string),
showAdvancedControls?: boolean,
};
const AnimatedWebView = Animated.createAnimatedComponent(WebView); const AnimatedWebView = Animated.createAnimatedComponent(WebView);
/** /**
* Class defining a webview screen. * Class defining a webview screen.
*/ */
class WebViewScreen extends React.PureComponent<Props> { class WebViewScreen extends React.PureComponent<PropsType> {
static defaultProps = { static defaultProps = {
customJS: '', customJS: '',
showAdvancedControls: true, showAdvancedControls: true,
@ -55,27 +58,24 @@ class WebViewScreen extends React.PureComponent<Props> {
* Creates header buttons and listens to events after mounting * Creates header buttons and listens to events after mounting
*/ */
componentDidMount() { componentDidMount() {
this.props.navigation.setOptions({ const {props} = this;
headerRight: this.props.showAdvancedControls props.navigation.setOptions({
headerRight: props.showAdvancedControls
? this.getAdvancedButtons ? this.getAdvancedButtons
: this.getBasicButton, : this.getBasicButton,
}); });
this.props.navigation.addListener( props.navigation.addListener('focus', () => {
'focus',
() =>
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid this.onBackButtonPressAndroid,
)
); );
this.props.navigation.addListener( });
'blur', props.navigation.addListener('blur', () => {
() =>
BackHandler.removeEventListener( BackHandler.removeEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid this.onBackButtonPressAndroid,
)
); );
});
} }
/** /**
@ -83,7 +83,7 @@ class WebViewScreen extends React.PureComponent<Props> {
* *
* @returns {boolean} * @returns {boolean}
*/ */
onBackButtonPressAndroid = () => { onBackButtonPressAndroid = (): boolean => {
if (this.canGoBack) { if (this.canGoBack) {
this.onGoBackClicked(); this.onGoBackClicked();
return true; return true;
@ -96,17 +96,19 @@ class WebViewScreen extends React.PureComponent<Props> {
* *
* @return {*} * @return {*}
*/ */
getBasicButton = () => { getBasicButton = (): React.Node => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
title="refresh" title="refresh"
iconName="refresh" iconName="refresh"
onPress={this.onRefreshClicked}/> onPress={this.onRefreshClicked}
/>
<Item <Item
title={i18n.t("general.openInBrowser")} title={i18n.t('general.openInBrowser')}
iconName="open-in-new" iconName="open-in-new"
onPress={this.onOpenClicked}/> onPress={this.onOpenClicked}
/>
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
}; };
@ -117,7 +119,8 @@ class WebViewScreen extends React.PureComponent<Props> {
* *
* @returns {*} * @returns {*}
*/ */
getAdvancedButtons = () => { getAdvancedButtons = (): React.Node => {
const {props} = this;
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
@ -131,40 +134,74 @@ class WebViewScreen extends React.PureComponent<Props> {
<MaterialCommunityIcons <MaterialCommunityIcons
name="dots-vertical" name="dots-vertical"
size={26} size={26}
color={this.props.theme.colors.text} color={props.theme.colors.text}
/>} />
> }>
<HiddenItem <HiddenItem
title={i18n.t("general.goBack")} title={i18n.t('general.goBack')}
onPress={this.onGoBackClicked}/> onPress={this.onGoBackClicked}
/>
<HiddenItem <HiddenItem
title={i18n.t("general.goForward")} title={i18n.t('general.goForward')}
onPress={this.onGoForwardClicked}/> onPress={this.onGoForwardClicked}
/>
<Divider /> <Divider />
<HiddenItem <HiddenItem
title={i18n.t("general.openInBrowser")} title={i18n.t('general.openInBrowser')}
onPress={this.onOpenClicked}/> onPress={this.onOpenClicked}
/>
</OverflowMenu> </OverflowMenu>
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
};
/**
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />;
/**
* Gets the javascript needed to generate a padding on top of the page
* This adds padding to the body and runs the custom padding function given in props
*
* @param padding The padding to add in pixels
* @returns {string}
*/
getJavascriptPadding(padding: number): string {
const {props} = this;
const customPadding =
props.customPaddingFunction != null
? props.customPaddingFunction(padding)
: '';
return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
} }
/** /**
* Callback to use when refresh button is clicked. Reloads the webview. * Callback to use when refresh button is clicked. Reloads the webview.
*/ */
onRefreshClicked = () => { onRefreshClicked = () => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null) this.webviewRef.current.reload();
this.webviewRef.current.reload(); };
}
onGoBackClicked = () => { onGoBackClicked = () => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null) this.webviewRef.current.goBack();
this.webviewRef.current.goBack(); };
}
onGoForwardClicked = () => { onGoForwardClicked = () => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null) this.webviewRef.current.goForward();
this.webviewRef.current.goForward(); };
}
onOpenClicked = () => Linking.openURL(this.props.url); onOpenClicked = () => {
const {url} = this.props;
Linking.openURL(url);
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
const {onScroll} = this.props;
if (onScroll) onScroll(event);
};
/** /**
* Injects the given javascript string into the web page * Injects the given javascript string into the web page
@ -174,55 +211,32 @@ class WebViewScreen extends React.PureComponent<Props> {
injectJavaScript = (script: string) => { injectJavaScript = (script: string) => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null)
this.webviewRef.current.injectJavaScript(script); this.webviewRef.current.injectJavaScript(script);
} };
/** render(): React.Node {
* Gets the loading indicator const {props} = this;
* const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
* @return {*}
*/
getRenderLoading = () => <BasicLoadingScreen isAbsolute={true}/>;
/**
* Gets the javascript needed to generate a padding on top of the page
* This adds padding to the body and runs the custom padding function given in props
*
* @param padding The padding to add in pixels
* @returns {string}
*/
getJavascriptPadding(padding: number) {
const customPadding = this.props.customPaddingFunction != null ? this.props.customPaddingFunction(padding) : "";
return (
"document.getElementsByTagName('body')[0].style.paddingTop = '" + padding + "px';" +
customPadding +
"true;"
);
}
onScroll = (event: Object) => {
if (this.props.onScroll)
this.props.onScroll(event);
}
render() {
const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack;
return ( return (
<AnimatedWebView <AnimatedWebView
ref={this.webviewRef} ref={this.webviewRef}
source={{uri: this.props.url}} source={{uri: props.url}}
startInLoadingState={true} startInLoadingState
injectedJavaScript={this.props.customJS} injectedJavaScript={props.customJS}
javaScriptEnabled={true} javaScriptEnabled
renderLoading={this.getRenderLoading} renderLoading={this.getRenderLoading}
renderError={() => <ErrorView renderError={(): React.Node => (
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR} errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked} onRefresh={this.onRefreshClicked}
/>} />
onNavigationStateChange={navState => { )}
onNavigationStateChange={(navState: {canGoBack: boolean}) => {
this.canGoBack = navState.canGoBack; this.canGoBack = navState.canGoBack;
}} }}
onMessage={this.props.onMessage} onMessage={props.onMessage}
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))} onLoad={() => {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}}
// Animations // Animations
onScroll={onScrollWithListener(this.onScroll)} onScroll={onScrollWithListener(this.onScroll)}
/> />