Improve Mascot components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-04 19:26:25 +02:00
parent 1cc0802c12
commit 7b94afadcc
3 changed files with 559 additions and 510 deletions

View file

@ -1,35 +1,35 @@
// @flow
import * as React from 'react';
import * as Animatable from "react-native-animatable";
import {Image, TouchableWithoutFeedback, View} from "react-native";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
import * as Animatable from 'react-native-animatable';
import {Image, TouchableWithoutFeedback, View} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
type Props = {
style?: ViewStyle,
emotion: number,
animated: boolean,
entryAnimation: Animatable.AnimatableProperties | null,
loopAnimation: Animatable.AnimatableProperties | null,
onPress?: (viewRef: AnimatableViewRef) => null,
onLongPress?: (viewRef: AnimatableViewRef) => null,
}
export type AnimatableViewRefType = {current: null | Animatable.View};
type State = {
type PropsType = {
emotion?: number,
animated?: boolean,
style?: ViewStyle | null,
entryAnimation?: Animatable.AnimatableProperties | null,
loopAnimation?: Animatable.AnimatableProperties | null,
onPress?: null | ((viewRef: AnimatableViewRefType) => void),
onLongPress?: null | ((viewRef: AnimatableViewRefType) => void),
};
type StateType = {
currentEmotion: number,
}
};
export type AnimatableViewRef = {current: null | Animatable.View};
const MASCOT_IMAGE = require("../../../assets/mascot/mascot.png");
const MASCOT_EYES_NORMAL = require("../../../assets/mascot/mascot_eyes_normal.png");
const MASCOT_EYES_GIRLY = require("../../../assets/mascot/mascot_eyes_girly.png");
const MASCOT_EYES_CUTE = require("../../../assets/mascot/mascot_eyes_cute.png");
const MASCOT_EYES_WINK = require("../../../assets/mascot/mascot_eyes_wink.png");
const MASCOT_EYES_HEART = require("../../../assets/mascot/mascot_eyes_heart.png");
const MASCOT_EYES_ANGRY = require("../../../assets/mascot/mascot_eyes_angry.png");
const MASCOT_GLASSES = require("../../../assets/mascot/mascot_glasses.png");
const MASCOT_SUNGLASSES = require("../../../assets/mascot/mascot_sunglasses.png");
const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png');
const MASCOT_EYES_NORMAL = require('../../../assets/mascot/mascot_eyes_normal.png');
const MASCOT_EYES_GIRLY = require('../../../assets/mascot/mascot_eyes_girly.png');
const MASCOT_EYES_CUTE = require('../../../assets/mascot/mascot_eyes_cute.png');
const MASCOT_EYES_WINK = require('../../../assets/mascot/mascot_eyes_wink.png');
const MASCOT_EYES_HEART = require('../../../assets/mascot/mascot_eyes_heart.png');
const MASCOT_EYES_ANGRY = require('../../../assets/mascot/mascot_eyes_angry.png');
const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png');
const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png');
export const EYE_STYLE = {
NORMAL: 0,
@ -38,12 +38,12 @@ export const EYE_STYLE = {
WINK: 4,
HEART: 5,
ANGRY: 6,
}
};
const GLASSES_STYLE = {
NORMAL: 0,
COOl: 1
}
COOl: 1,
};
export const MASCOT_STYLE = {
NORMAL: 0,
@ -58,40 +58,40 @@ export const MASCOT_STYLE = {
RANDOM: 999,
};
class Mascot extends React.Component<Props, State> {
class Mascot extends React.Component<PropsType, StateType> {
static defaultProps = {
emotion: MASCOT_STYLE.NORMAL,
animated: false,
style: null,
entryAnimation: {
useNativeDriver: true,
animation: "rubberBand",
animation: 'rubberBand',
duration: 2000,
},
loopAnimation: {
useNativeDriver: true,
animation: "swing",
animation: 'swing',
duration: 2000,
iterationDelay: 250,
iterationCount: "infinite",
iterationCount: 'infinite',
},
clickAnimation: {
useNativeDriver: true,
animation: "rubberBand",
duration: 2000,
},
}
onPress: null,
onLongPress: null,
};
viewRef: AnimatableViewRefType;
viewRef: AnimatableViewRef;
eyeList: {[key: number]: number | string};
glassesList: {[key: number]: number | string};
onPress: (viewRef: AnimatableViewRef) => null;
onLongPress: (viewRef: AnimatableViewRef) => null;
onPress: (viewRef: AnimatableViewRefType) => void;
onLongPress: (viewRef: AnimatableViewRefType) => void;
initialEmotion: number;
constructor(props: Props) {
constructor(props: PropsType) {
super(props);
this.viewRef = React.createRef();
this.eyeList = {};
@ -106,87 +106,94 @@ class Mascot extends React.Component<Props, State> {
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES;
this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
this.initialEmotion = this.props.emotion;
this.initialEmotion =
props.emotion != null ? props.emotion : Mascot.defaultProps.emotion;
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
this.state = {
currentEmotion: this.initialEmotion
}
currentEmotion: this.initialEmotion,
};
if (this.props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRef) => {
let ref = viewRef.current;
if (props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
ref.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.initialEmotion});
});
}
return null;
}
} else
this.onPress = this.props.onPress;
};
} else this.onPress = props.onPress;
if (this.props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRef) => {
let ref = viewRef.current;
if (props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion});
});
}
return null;
}
} else
this.onLongPress = this.props.onLongPress;
};
} else this.onLongPress = props.onLongPress;
}
getGlasses(style: number) {
getGlasses(style: number): React.Node {
const glasses = this.glassesList[style];
return <Image
key={"glasses"}
source={glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]}
return (
<Image
key="glasses"
source={
glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]
}
style={{
position: "absolute",
top: "15%",
position: 'absolute',
top: '15%',
left: 0,
width: "100%",
height: "100%",
width: '100%',
height: '100%',
}}
/>
);
}
getEye(style: number, isRight: boolean, rotation: string="0deg") {
getEye(
style: number,
isRight: boolean,
rotation: string = '0deg',
): React.Node {
const eye = this.eyeList[style];
return <Image
key={isRight ? "right" : "left"}
return (
<Image
key={isRight ? 'right' : 'left'}
source={eye != null ? eye : this.eyeList[EYE_STYLE.NORMAL]}
style={{
position: "absolute",
top: "15%",
left: isRight ? "-11%" : "11%",
width: "100%",
height: "100%",
transform: [{rotateY: rotation}]
position: 'absolute',
top: '15%',
left: isRight ? '-11%' : '11%',
width: '100%',
height: '100%',
transform: [{rotateY: rotation}],
}}
/>
);
}
getEyes(emotion: number) {
let final = [];
final.push(<View
key={"container"}
getEyes(emotion: number): React.Node {
const final = [];
final.push(
<View
key="container"
style={{
position: "absolute",
width: "100%",
height: "100%",
}}/>);
position: 'absolute',
width: '100%',
height: '100%',
}}
/>,
);
if (emotion === MASCOT_STYLE.CUTE) {
final.push(this.getEye(EYE_STYLE.CUTE, true));
final.push(this.getEye(EYE_STYLE.CUTE, false));
@ -204,7 +211,7 @@ class Mascot extends React.Component<Props, State> {
final.push(this.getEye(EYE_STYLE.HEART, false));
} else if (emotion === MASCOT_STYLE.ANGRY) {
final.push(this.getEye(EYE_STYLE.ANGRY, true));
final.push(this.getEye(EYE_STYLE.ANGRY, false, "180deg"));
final.push(this.getEye(EYE_STYLE.ANGRY, false, '180deg'));
} else if (emotion === MASCOT_STYLE.COOL) {
final.push(this.getGlasses(GLASSES_STYLE.COOl));
} else {
@ -212,42 +219,45 @@ class Mascot extends React.Component<Props, State> {
final.push(this.getEye(EYE_STYLE.NORMAL, false));
}
if (emotion === MASCOT_STYLE.INTELLO) { // Needs to have normal eyes behind the glasses
if (emotion === MASCOT_STYLE.INTELLO) {
// Needs to have normal eyes behind the glasses
final.push(this.getGlasses(GLASSES_STYLE.NORMAL));
}
final.push(<View key={"container2"}/>);
final.push(<View key="container2" />);
return final;
}
render() {
const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
render(): React.Node {
const {props, state} = this;
const entryAnimation = props.animated ? props.entryAnimation : null;
const loopAnimation = props.animated ? props.loopAnimation : null;
return (
<Animatable.View
style={{
aspectRatio: 1,
...this.props.style
...props.style,
}}
{...entryAnimation}
>
// eslint-disable-next-line react/jsx-props-no-spreading
{...entryAnimation}>
<TouchableWithoutFeedback
onPress={() => this.onPress(this.viewRef)}
onLongPress={() => this.onLongPress(this.viewRef)}
>
onPress={() => {
this.onPress(this.viewRef);
}}
onLongPress={() => {
this.onLongPress(this.viewRef);
}}>
<Animatable.View ref={this.viewRef}>
<Animatable.View
ref={this.viewRef}
>
<Animatable.View
{...loopAnimation}
>
// eslint-disable-next-line react/jsx-props-no-spreading
{...loopAnimation}>
<Image
source={MASCOT_IMAGE}
style={{
width: "100%",
height:"100%",
width: '100%',
height: '100%',
}}
/>
{this.getEyes(this.state.currentEmotion)}
{this.getEyes(state.currentEmotion)}
</Animatable.View>
</Animatable.View>
</TouchableWithoutFeedback>

View file

@ -1,15 +1,28 @@
// @flow
import * as React from 'react';
import {Avatar, Button, Card, Paragraph, Portal, withTheme} from 'react-native-paper';
import Mascot from "./Mascot";
import * as Animatable from "react-native-animatable";
import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager";
import SpeechArrow from "./SpeechArrow";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {
Avatar,
Button,
Card,
Paragraph,
Portal,
withTheme,
} from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import {
BackHandler,
Dimensions,
ScrollView,
TouchableWithoutFeedback,
View,
} from 'react-native';
import Mascot from './Mascot';
import type {CustomTheme} from '../../managers/ThemeManager';
import SpeechArrow from './SpeechArrow';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
type Props = {
type PropsType = {
theme: CustomTheme,
icon: string,
title: string,
@ -26,28 +39,34 @@ type Props = {
icon: string | null,
color: string | null,
onPress?: () => void,
}
},
},
emotion: number,
visible?: boolean,
prefKey?: string,
}
};
type State = {
type StateType = {
shouldRenderDialog: boolean, // Used to stop rendering after hide animation
dialogVisible: boolean,
}
};
/**
* Component used to display a popup with the mascot.
*/
class MascotPopup extends React.Component<Props, State> {
class MascotPopup extends React.Component<PropsType, StateType> {
static defaultProps = {
visible: null,
prefKey: null,
};
mascotSize: number;
windowWidth: number;
windowHeight: number;
constructor(props: Props) {
constructor(props: PropsType) {
super(props);
this.windowWidth = Dimensions.get('window').width;
@ -55,13 +74,13 @@ class MascotPopup extends React.Component<Props, State> {
this.mascotSize = Dimensions.get('window').height / 6;
if (this.props.visible != null) {
if (props.visible != null) {
this.state = {
shouldRenderDialog: this.props.visible,
dialogVisible: this.props.visible,
shouldRenderDialog: props.visible,
dialogVisible: props.visible,
};
} else if (this.props.prefKey != null) {
const visible = AsyncStorageManager.getBool(this.props.prefKey);
} else if (props.prefKey != null) {
const visible = AsyncStorageManager.getBool(props.prefKey);
this.state = {
shouldRenderDialog: visible,
dialogVisible: visible,
@ -72,90 +91,92 @@ class MascotPopup extends React.Component<Props, State> {
dialogVisible: false,
};
}
}
onAnimationEnd = () => {
this.setState({
shouldRenderDialog: false,
})
componentDidMount(): * {
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
}
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean {
const {props, state} = this;
if (nextProps.visible) {
this.state.shouldRenderDialog = true;
this.state.dialogVisible = true;
} else if (nextProps.visible !== this.props.visible
|| (!nextState.dialogVisible && nextState.dialogVisible !== this.state.dialogVisible)) {
} else if (
nextProps.visible !== props.visible ||
(!nextState.dialogVisible &&
nextState.dialogVisible !== state.dialogVisible)
) {
this.state.dialogVisible = false;
setTimeout(this.onAnimationEnd, 300);
}
return true;
}
componentDidMount(): * {
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
}
onBackButtonPressAndroid = () => {
if (this.state.dialogVisible) {
const cancel = this.props.buttons.cancel;
const action = this.props.buttons.action;
if (cancel != null)
this.onDismiss(cancel.onPress);
else
this.onDismiss(action.onPress);
return true;
} else {
return false;
}
onAnimationEnd = () => {
this.setState({
shouldRenderDialog: false,
});
};
getSpeechBubble() {
onBackButtonPressAndroid = (): boolean => {
const {state, props} = this;
if (state.dialogVisible) {
const {cancel} = props.buttons;
const {action} = props.buttons;
if (cancel != null) this.onDismiss(cancel.onPress);
else this.onDismiss(action.onPress);
return true;
}
return false;
};
getSpeechBubble(): React.Node {
const {state, props} = this;
return (
<Animatable.View
style={{
marginLeft: "10%",
marginRight: "10%",
marginLeft: '10%',
marginRight: '10%',
}}
useNativeDriver={true}
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
duration={this.state.dialogVisible ? 1000 : 300}
>
useNativeDriver
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
duration={state.dialogVisible ? 1000 : 300}>
<SpeechArrow
style={{marginLeft: this.mascotSize / 3}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
color={props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
<Card
style={{
borderColor: props.theme.colors.mascotMessageArrow,
borderWidth: 4,
borderRadius: 10,
}}>
<Card.Title
title={this.props.title}
left={this.props.icon != null ?
(props) => <Avatar.Icon
{...props}
title={props.title}
left={
props.icon != null
? (): React.Node => (
<Avatar.Icon
size={48}
style={{backgroundColor: "transparent"}}
color={this.props.theme.colors.primary}
icon={this.props.icon}
style={{backgroundColor: 'transparent'}}
color={props.theme.colors.primary}
icon={props.icon}
/>
: null}
)
: null
}
/>
<Card.Content style={{
maxHeight: this.windowHeight / 3
<Card.Content
style={{
maxHeight: this.windowHeight / 3,
}}>
<ScrollView>
<Paragraph style={{marginBottom: 10}}>
{this.props.message}
</Paragraph>
<Paragraph style={{marginBottom: 10}}>{props.message}</Paragraph>
</ScrollView>
</Card.Content>
@ -167,116 +188,124 @@ class MascotPopup extends React.Component<Props, State> {
);
}
getMascot() {
getMascot(): React.Node {
const {props, state} = this;
return (
<Animatable.View
useNativeDriver={true}
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
duration={this.state.dialogVisible ? 1500 : 200}
>
useNativeDriver
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
duration={state.dialogVisible ? 1500 : 200}>
<Mascot
style={{width: this.mascotSize}}
animated={true}
emotion={this.props.emotion}
animated
emotion={props.emotion}
/>
</Animatable.View>
);
}
getButtons() {
const action = this.props.buttons.action;
const cancel = this.props.buttons.cancel;
getButtons(): React.Node {
const {props} = this;
const {action} = props.buttons;
const {cancel} = props.buttons;
return (
<View style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: "auto",
marginBottom: "auto",
<View
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
}}>
{action != null
? <Button
{action != null ? (
<Button
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 10,
}}
mode={"contained"}
mode="contained"
icon={action.icon}
color={action.color}
onPress={() => this.onDismiss(action.onPress)}
>
onPress={() => {
this.onDismiss(action.onPress);
}}>
{action.message}
</Button>
: null}
{cancel != null
? <Button
) : null}
{cancel != null ? (
<Button
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}
mode={"contained"}
mode="contained"
icon={cancel.icon}
color={cancel.color}
onPress={() => this.onDismiss(cancel.onPress)}
>
onPress={() => {
this.onDismiss(cancel.onPress);
}}>
{cancel.message}
</Button>
: null}
) : null}
</View>
);
}
getBackground() {
getBackground(): React.Node {
const {props, state} = this;
return (
<TouchableWithoutFeedback onPress={() => this.onDismiss(this.props.buttons.cancel.onPress)}>
<TouchableWithoutFeedback
onPress={() => {
this.onDismiss(props.buttons.cancel.onPress);
}}>
<Animatable.View
style={{
position: "absolute",
backgroundColor: "rgba(0,0,0,0.7)",
width: "100%",
height: "100%",
position: 'absolute',
backgroundColor: 'rgba(0,0,0,0.7)',
width: '100%',
height: '100%',
}}
useNativeDriver={true}
animation={this.state.dialogVisible ? "fadeIn" : "fadeOut"}
duration={this.state.dialogVisible ? 300 : 300}
useNativeDriver
animation={state.dialogVisible ? 'fadeIn' : 'fadeOut'}
duration={state.dialogVisible ? 300 : 300}
/>
</TouchableWithoutFeedback>
);
}
onDismiss = (callback?: () => void) => {
if (this.props.prefKey != null) {
AsyncStorageManager.set(this.props.prefKey, false);
const {prefKey} = this.props;
if (prefKey != null) {
AsyncStorageManager.set(prefKey, false);
this.setState({dialogVisible: false});
}
if (callback != null)
callback();
}
if (callback != null) callback();
};
render() {
if (this.state.shouldRenderDialog) {
render(): React.Node {
const {shouldRenderDialog} = this.state;
if (shouldRenderDialog) {
return (
<Portal>
{this.getBackground()}
<View style={{
marginTop: "auto",
marginBottom: "auto",
<View
style={{
marginTop: 'auto',
marginBottom: 'auto',
}}>
<View style={{
<View
style={{
marginTop: -80,
width: "100%"
width: '100%',
}}>
{this.getMascot()}
{this.getSpeechBubble()}
</View>
</View>
</Portal>
);
} else
}
return null;
}
}

View file

@ -1,32 +1,42 @@
// @flow
import * as React from 'react';
import {View} from "react-native";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
import {View} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
type Props = {
style?: ViewStyle,
type PropsType = {
style?: ViewStyle | null,
size: number,
color: string,
};
export default class SpeechArrow extends React.Component<PropsType> {
static defaultProps = {
style: null,
};
shouldComponentUpdate(): boolean {
return false;
}
export default class SpeechArrow extends React.Component<Props> {
render() {
render(): React.Node {
const {props} = this;
return (
<View style={this.props.style}>
<View style={{
<View style={props.style}>
<View
style={{
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: this.props.size,
borderBottomWidth: this.props.size,
borderRightWidth: props.size,
borderBottomWidth: props.size,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: this.props.color,
}}/>
borderBottomColor: props.color,
}}
/>
</View>
);
}