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,28 +11,29 @@ 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
backgroundColor: colors.background, style={{
position: position, backgroundColor: colors.background,
top: 0, position,
right: 0, top: 0,
width: '100%', right: 0,
height: '100%', width: '100%',
justifyContent: 'center', height: '100%',
}}> justifyContent: 'center',
<ActivityIndicator }}>
animating={true} <ActivityIndicator animating size="large" color={colors.primary} />
size={'large'} </View>
color={colors.primary}/> );
</View>
);
} }
export default withTheme(BasicLoadingScreen); export default withTheme(BasicLoadingScreen);

View file

@ -2,191 +2,189 @@
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%',
}, },
inner: { inner: {
marginTop: 'auto', marginTop: 'auto',
marginBottom: 'auto', marginBottom: 'auto',
}, },
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,233 +1,247 @@
// @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, collapsibleStack: Collapsible,
customPaddingFunction: null | (padding: number) => string, onMessage: (event: {nativeEvent: {data: string}}) => void,
collapsibleStack: Collapsible, onScroll: (event: SyntheticEvent<EventTarget>) => void,
onMessage: Function, customJS?: string,
onScroll: Function, customPaddingFunction?: null | ((padding: number) => string),
showAdvancedControls: boolean, 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 = {
customJS: '',
showAdvancedControls: true,
customPaddingFunction: null,
};
static defaultProps = { webviewRef: {current: null | WebView};
customJS: '',
showAdvancedControls: true,
customPaddingFunction: null,
};
webviewRef: { current: null | WebView }; canGoBack: boolean;
canGoBack: boolean; constructor() {
super();
this.webviewRef = React.createRef();
this.canGoBack = false;
}
constructor() { /**
super(); * Creates header buttons and listens to events after mounting
this.webviewRef = React.createRef(); */
this.canGoBack = false; componentDidMount() {
const {props} = this;
props.navigation.setOptions({
headerRight: props.showAdvancedControls
? this.getAdvancedButtons
: this.getBasicButton,
});
props.navigation.addListener('focus', () => {
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
props.navigation.addListener('blur', () => {
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
}
/**
* Goes back on the webview or on the navigation stack if we cannot go back anymore
*
* @returns {boolean}
*/
onBackButtonPressAndroid = (): boolean => {
if (this.canGoBack) {
this.onGoBackClicked();
return true;
} }
return false;
};
/** /**
* Creates header buttons and listens to events after mounting * Gets header refresh and open in browser buttons
*/ *
componentDidMount() { * @return {*}
this.props.navigation.setOptions({ */
headerRight: this.props.showAdvancedControls getBasicButton = (): React.Node => {
? this.getAdvancedButtons return (
: this.getBasicButton, <MaterialHeaderButtons>
}); <Item
this.props.navigation.addListener( title="refresh"
'focus', iconName="refresh"
() => onPress={this.onRefreshClicked}
BackHandler.addEventListener( />
'hardwareBackPress', <Item
this.onBackButtonPressAndroid title={i18n.t('general.openInBrowser')}
) iconName="open-in-new"
); onPress={this.onOpenClicked}
this.props.navigation.addListener( />
'blur', </MaterialHeaderButtons>
() => );
BackHandler.removeEventListener( };
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
}
/** /**
* Goes back on the webview or on the navigation stack if we cannot go back anymore * Creates advanced header control buttons.
* * These buttons allows the user to refresh, go back, go forward and open in the browser.
* @returns {boolean} *
*/ * @returns {*}
onBackButtonPressAndroid = () => { */
if (this.canGoBack) { getAdvancedButtons = (): React.Node => {
this.onGoBackClicked(); const {props} = this;
return true; return (
} <MaterialHeaderButtons>
return false; <Item
}; title="refresh"
iconName="refresh"
/** onPress={this.onRefreshClicked}
* Gets header refresh and open in browser buttons />
* <OverflowMenu
* @return {*} style={{marginHorizontal: 10}}
*/ OverflowIcon={
getBasicButton = () => { <MaterialCommunityIcons
return ( name="dots-vertical"
<MaterialHeaderButtons> size={26}
<Item color={props.theme.colors.text}
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}/>
<Item
title={i18n.t("general.openInBrowser")}
iconName="open-in-new"
onPress={this.onOpenClicked}/>
</MaterialHeaderButtons>
);
};
/**
* Creates advanced header control buttons.
* These buttons allows the user to refresh, go back, go forward and open in the browser.
*
* @returns {*}
*/
getAdvancedButtons = () => {
return (
<MaterialHeaderButtons>
<Item
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}
/>
<OverflowMenu
style={{marginHorizontal: 10}}
OverflowIcon={
<MaterialCommunityIcons
name="dots-vertical"
size={26}
color={this.props.theme.colors.text}
/>}
>
<HiddenItem
title={i18n.t("general.goBack")}
onPress={this.onGoBackClicked}/>
<HiddenItem
title={i18n.t("general.goForward")}
onPress={this.onGoForwardClicked}/>
<Divider/>
<HiddenItem
title={i18n.t("general.openInBrowser")}
onPress={this.onOpenClicked}/>
</OverflowMenu>
</MaterialHeaderButtons>
);
}
/**
* Callback to use when refresh button is clicked. Reloads the webview.
*/
onRefreshClicked = () => {
if (this.webviewRef.current != null)
this.webviewRef.current.reload();
}
onGoBackClicked = () => {
if (this.webviewRef.current != null)
this.webviewRef.current.goBack();
}
onGoForwardClicked = () => {
if (this.webviewRef.current != null)
this.webviewRef.current.goForward();
}
onOpenClicked = () => Linking.openURL(this.props.url);
/**
* Injects the given javascript string into the web page
*
* @param script The script to inject
*/
injectJavaScript = (script: string) => {
if (this.webviewRef.current != null)
this.webviewRef.current.injectJavaScript(script);
}
/**
* Gets the loading indicator
*
* @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 (
<AnimatedWebView
ref={this.webviewRef}
source={{uri: this.props.url}}
startInLoadingState={true}
injectedJavaScript={this.props.customJS}
javaScriptEnabled={true}
renderLoading={this.getRenderLoading}
renderError={() => <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
/>}
onNavigationStateChange={navState => {
this.canGoBack = navState.canGoBack;
}}
onMessage={this.props.onMessage}
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
// Animations
onScroll={onScrollWithListener(this.onScroll)}
/> />
); }>
} <HiddenItem
title={i18n.t('general.goBack')}
onPress={this.onGoBackClicked}
/>
<HiddenItem
title={i18n.t('general.goForward')}
onPress={this.onGoForwardClicked}
/>
<Divider />
<HiddenItem
title={i18n.t('general.openInBrowser')}
onPress={this.onOpenClicked}
/>
</OverflowMenu>
</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.
*/
onRefreshClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.reload();
};
onGoBackClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goBack();
};
onGoForwardClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goForward();
};
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
*
* @param script The script to inject
*/
injectJavaScript = (script: string) => {
if (this.webviewRef.current != null)
this.webviewRef.current.injectJavaScript(script);
};
render(): React.Node {
const {props} = this;
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
return (
<AnimatedWebView
ref={this.webviewRef}
source={{uri: props.url}}
startInLoadingState
injectedJavaScript={props.customJS}
javaScriptEnabled
renderLoading={this.getRenderLoading}
renderError={(): React.Node => (
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
/>
)}
onNavigationStateChange={(navState: {canGoBack: boolean}) => {
this.canGoBack = navState.canGoBack;
}}
onMessage={props.onMessage}
onLoad={() => {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}}
// Animations
onScroll={onScrollWithListener(this.onScroll)}
/>
);
}
} }
export default withCollapsible(withTheme(WebViewScreen)); export default withCollapsible(withTheme(WebViewScreen));