From 7b94afadccbc2ddc5c375c544a725c23c39a8dab Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Tue, 4 Aug 2020 19:26:25 +0200 Subject: [PATCH] Improve Mascot components to match linter --- src/components/Mascot/Mascot.js | 466 +++++++++++------------ src/components/Mascot/MascotPopup.js | 537 ++++++++++++++------------- src/components/Mascot/SpeechArrow.js | 66 ++-- 3 files changed, 559 insertions(+), 510 deletions(-) diff --git a/src/components/Mascot/Mascot.js b/src/components/Mascot/Mascot.js index a88071c..87c5599 100644 --- a/src/components/Mascot/Mascot.js +++ b/src/components/Mascot/Mascot.js @@ -1,259 +1,269 @@ // @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 = { - 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"); - -export const EYE_STYLE = { - NORMAL: 0, - GIRLY: 2, - CUTE: 3, - WINK: 4, - HEART: 5, - ANGRY: 6, -} - -const GLASSES_STYLE = { - NORMAL: 0, - COOl: 1 -} - -export const MASCOT_STYLE = { - NORMAL: 0, - HAPPY: 1, - GIRLY: 2, - WINK: 3, - CUTE: 4, - INTELLO: 5, - LOVE: 6, - COOL: 7, - ANGRY: 8, - RANDOM: 999, +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, +}; -class Mascot extends React.Component { +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'); - static defaultProps = { - animated: false, - entryAnimation: { - useNativeDriver: true, - animation: "rubberBand", - duration: 2000, - }, - loopAnimation: { - useNativeDriver: true, - animation: "swing", - duration: 2000, - iterationDelay: 250, - iterationCount: "infinite", - }, - clickAnimation: { - useNativeDriver: true, - animation: "rubberBand", - duration: 2000, - }, - } +export const EYE_STYLE = { + NORMAL: 0, + GIRLY: 2, + CUTE: 3, + WINK: 4, + HEART: 5, + ANGRY: 6, +}; - viewRef: AnimatableViewRef; - eyeList: { [key: number]: number | string }; - glassesList: { [key: number]: number | string }; +const GLASSES_STYLE = { + NORMAL: 0, + COOl: 1, +}; - onPress: (viewRef: AnimatableViewRef) => null; - onLongPress: (viewRef: AnimatableViewRef) => null; +export const MASCOT_STYLE = { + NORMAL: 0, + HAPPY: 1, + GIRLY: 2, + WINK: 3, + CUTE: 4, + INTELLO: 5, + LOVE: 6, + COOL: 7, + ANGRY: 8, + RANDOM: 999, +}; - initialEmotion: number; +class Mascot extends React.Component { + static defaultProps = { + emotion: MASCOT_STYLE.NORMAL, + animated: false, + style: null, + entryAnimation: { + useNativeDriver: true, + animation: 'rubberBand', + duration: 2000, + }, + loopAnimation: { + useNativeDriver: true, + animation: 'swing', + duration: 2000, + iterationDelay: 250, + iterationCount: 'infinite', + }, + onPress: null, + onLongPress: null, + }; - constructor(props: Props) { - super(props); - this.viewRef = React.createRef(); - this.eyeList = {}; - this.glassesList = {}; - this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL; - this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY; - this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE; - this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK; - this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART; - this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY; + viewRef: AnimatableViewRefType; - this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES; - this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES; + eyeList: {[key: number]: number | string}; - this.initialEmotion = this.props.emotion; + glassesList: {[key: number]: number | string}; - if (this.initialEmotion === MASCOT_STYLE.RANDOM) - this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1; + onPress: (viewRef: AnimatableViewRefType) => void; - this.state = { - currentEmotion: this.initialEmotion + onLongPress: (viewRef: AnimatableViewRefType) => void; + + initialEmotion: number; + + constructor(props: PropsType) { + super(props); + this.viewRef = React.createRef(); + this.eyeList = {}; + this.glassesList = {}; + this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL; + this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY; + this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE; + this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK; + this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART; + this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY; + + this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES; + this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES; + + 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, + }; + + 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}); + }); } + }; + } else this.onPress = props.onPress; - if (this.props.onPress == null) { - this.onPress = (viewRef: AnimatableViewRef) => { - let 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; - - if (this.props.onLongPress == null) { - this.onLongPress = (viewRef: AnimatableViewRef) => { - let 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; - - } - - getGlasses(style: number) { - const glasses = this.glassesList[style]; - return - } - - getEye(style: number, isRight: boolean, rotation: string="0deg") { - const eye = this.eyeList[style]; - return - } - - getEyes(emotion: number) { - let final = []; - final.push(); - if (emotion === MASCOT_STYLE.CUTE) { - final.push(this.getEye(EYE_STYLE.CUTE, true)); - final.push(this.getEye(EYE_STYLE.CUTE, false)); - } else if (emotion === MASCOT_STYLE.GIRLY) { - final.push(this.getEye(EYE_STYLE.GIRLY, true)); - final.push(this.getEye(EYE_STYLE.GIRLY, false)); - } else if (emotion === MASCOT_STYLE.HAPPY) { - final.push(this.getEye(EYE_STYLE.WINK, true)); - final.push(this.getEye(EYE_STYLE.WINK, false)); - } else if (emotion === MASCOT_STYLE.WINK) { - final.push(this.getEye(EYE_STYLE.WINK, true)); - final.push(this.getEye(EYE_STYLE.NORMAL, false)); - } else if (emotion === MASCOT_STYLE.LOVE) { - final.push(this.getEye(EYE_STYLE.HEART, true)); - 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")); - } else if (emotion === MASCOT_STYLE.COOL) { - final.push(this.getGlasses(GLASSES_STYLE.COOl)); - } else { - final.push(this.getEye(EYE_STYLE.NORMAL, true)); - final.push(this.getEye(EYE_STYLE.NORMAL, false)); + 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}); + }); } + }; + } else this.onLongPress = props.onLongPress; + } - if (emotion === MASCOT_STYLE.INTELLO) { // Needs to have normal eyes behind the glasses - final.push(this.getGlasses(GLASSES_STYLE.NORMAL)); + getGlasses(style: number): React.Node { + const glasses = this.glassesList[style]; + return ( + ); - return final; + style={{ + position: 'absolute', + top: '15%', + left: 0, + width: '100%', + height: '100%', + }} + /> + ); + } + + getEye( + style: number, + isRight: boolean, + rotation: string = '0deg', + ): React.Node { + const eye = this.eyeList[style]; + return ( + + ); + } + + getEyes(emotion: number): React.Node { + const final = []; + final.push( + , + ); + if (emotion === MASCOT_STYLE.CUTE) { + final.push(this.getEye(EYE_STYLE.CUTE, true)); + final.push(this.getEye(EYE_STYLE.CUTE, false)); + } else if (emotion === MASCOT_STYLE.GIRLY) { + final.push(this.getEye(EYE_STYLE.GIRLY, true)); + final.push(this.getEye(EYE_STYLE.GIRLY, false)); + } else if (emotion === MASCOT_STYLE.HAPPY) { + final.push(this.getEye(EYE_STYLE.WINK, true)); + final.push(this.getEye(EYE_STYLE.WINK, false)); + } else if (emotion === MASCOT_STYLE.WINK) { + final.push(this.getEye(EYE_STYLE.WINK, true)); + final.push(this.getEye(EYE_STYLE.NORMAL, false)); + } else if (emotion === MASCOT_STYLE.LOVE) { + final.push(this.getEye(EYE_STYLE.HEART, true)); + 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')); + } else if (emotion === MASCOT_STYLE.COOL) { + final.push(this.getGlasses(GLASSES_STYLE.COOl)); + } else { + final.push(this.getEye(EYE_STYLE.NORMAL, true)); + final.push(this.getEye(EYE_STYLE.NORMAL, false)); } - render() { - const entryAnimation = this.props.animated ? this.props.entryAnimation : null; - const loopAnimation = this.props.animated ? this.props.loopAnimation : null; - return ( + if (emotion === MASCOT_STYLE.INTELLO) { + // Needs to have normal eyes behind the glasses + final.push(this.getGlasses(GLASSES_STYLE.NORMAL)); + } + final.push(); + return final; + } + + render(): React.Node { + const {props, state} = this; + const entryAnimation = props.animated ? props.entryAnimation : null; + const loopAnimation = props.animated ? props.loopAnimation : null; + return ( + + { + this.onPress(this.viewRef); + }} + onLongPress={() => { + this.onLongPress(this.viewRef); + }}> + + - this.onPress(this.viewRef)} - onLongPress={() => this.onLongPress(this.viewRef)} - > - - - - {this.getEyes(this.state.currentEmotion)} - - - + /> + {this.getEyes(state.currentEmotion)} - ); - } + + + + ); + } } export default Mascot; diff --git a/src/components/Mascot/MascotPopup.js b/src/components/Mascot/MascotPopup.js index cc80091..e4b6688 100644 --- a/src/components/Mascot/MascotPopup.js +++ b/src/components/Mascot/MascotPopup.js @@ -1,283 +1,312 @@ // @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 = { - theme: CustomTheme, - icon: string, - title: string, - message: string, - buttons: { - action: { - message: string, - icon: string | null, - color: string | null, - onPress?: () => void, - }, - cancel: { - message: string, - icon: string | null, - color: string | null, - onPress?: () => void, - } +type PropsType = { + theme: CustomTheme, + icon: string, + title: string, + message: string, + buttons: { + action: { + message: string, + icon: string | null, + color: string | null, + onPress?: () => void, }, - emotion: number, - visible?: boolean, - prefKey?: string, -} + cancel: { + message: string, + icon: string | null, + color: string | null, + onPress?: () => void, + }, + }, + emotion: number, + visible?: boolean, + prefKey?: string, +}; -type State = { - shouldRenderDialog: boolean, // Used to stop rendering after hide animation - dialogVisible: boolean, -} +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 { +class MascotPopup extends React.Component { + static defaultProps = { + visible: null, + prefKey: null, + }; - mascotSize: number; - windowWidth: number; - windowHeight: number; + mascotSize: number; - constructor(props: Props) { - super(props); + windowWidth: number; - this.windowWidth = Dimensions.get('window').width; - this.windowHeight = Dimensions.get('window').height; + windowHeight: number; - this.mascotSize = Dimensions.get('window').height / 6; + constructor(props: PropsType) { + super(props); - if (this.props.visible != null) { - this.state = { - shouldRenderDialog: this.props.visible, - dialogVisible: this.props.visible, - }; - } else if (this.props.prefKey != null) { - const visible = AsyncStorageManager.getBool(this.props.prefKey); - this.state = { - shouldRenderDialog: visible, - dialogVisible: visible, - }; - } else { - this.state = { - shouldRenderDialog: false, - dialogVisible: false, - }; - } + this.windowWidth = Dimensions.get('window').width; + this.windowHeight = Dimensions.get('window').height; + this.mascotSize = Dimensions.get('window').height / 6; + + if (props.visible != null) { + this.state = { + shouldRenderDialog: props.visible, + dialogVisible: props.visible, + }; + } else if (props.prefKey != null) { + const visible = AsyncStorageManager.getBool(props.prefKey); + this.state = { + shouldRenderDialog: visible, + dialogVisible: visible, + }; + } else { + this.state = { + shouldRenderDialog: false, + dialogVisible: false, + }; } + } - onAnimationEnd = () => { - this.setState({ - shouldRenderDialog: false, - }) + componentDidMount(): * { + BackHandler.addEventListener( + 'hardwareBackPress', + this.onBackButtonPressAndroid, + ); + } + + 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 !== props.visible || + (!nextState.dialogVisible && + nextState.dialogVisible !== state.dialogVisible) + ) { + this.state.dialogVisible = false; + setTimeout(this.onAnimationEnd, 300); } + return true; + } - shouldComponentUpdate(nextProps: Props, nextState: State): boolean { - 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)) { - this.state.dialogVisible = false; - setTimeout(this.onAnimationEnd, 300); - } - return true; + onAnimationEnd = () => { + this.setState({ + shouldRenderDialog: false, + }); + }; + + 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; + }; - 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; - } - }; - - getSpeechBubble() { - return ( - - - - - - : null} + getSpeechBubble(): React.Node { + const {state, props} = this; + return ( + + + + ( + - - - - - {this.props.message} - - - - - - {this.getButtons()} - - - - ); - } - - getMascot() { - return ( - - - - ); - } - - getButtons() { - const action = this.props.buttons.action; - const cancel = this.props.buttons.cancel; - return ( - + - {action != null - ? - : null} - {cancel != null - ? - : null} + + {props.message} + + + + + {this.getButtons()} + + + + ); + } + + getMascot(): React.Node { + const {props, state} = this; + return ( + + + + ); + } + + getButtons(): React.Node { + const {props} = this; + const {action} = props.buttons; + const {cancel} = props.buttons; + return ( + + {action != null ? ( + + ) : null} + {cancel != null ? ( + + ) : null} + + ); + } + + getBackground(): React.Node { + const {props, state} = this; + return ( + { + this.onDismiss(props.buttons.cancel.onPress); + }}> + + + ); + } + + onDismiss = (callback?: () => void) => { + const {prefKey} = this.props; + if (prefKey != null) { + AsyncStorageManager.set(prefKey, false); + this.setState({dialogVisible: false}); + } + if (callback != null) callback(); + }; + + render(): React.Node { + const {shouldRenderDialog} = this.state; + if (shouldRenderDialog) { + return ( + + {this.getBackground()} + + + {this.getMascot()} + {this.getSpeechBubble()} - ); - } - - getBackground() { - return ( - this.onDismiss(this.props.buttons.cancel.onPress)}> - - - - ); - } - - onDismiss = (callback?: ()=> void) => { - if (this.props.prefKey != null) { - AsyncStorageManager.set(this.props.prefKey, false); - this.setState({dialogVisible: false}); - } - if (callback != null) - callback(); - } - - render() { - if (this.state.shouldRenderDialog) { - return ( - - {this.getBackground()} - - - {this.getMascot()} - {this.getSpeechBubble()} - - - - - ); - } else - return null; - + + + ); } + return null; + } } export default withTheme(MascotPopup); diff --git a/src/components/Mascot/SpeechArrow.js b/src/components/Mascot/SpeechArrow.js index f72d340..c95e582 100644 --- a/src/components/Mascot/SpeechArrow.js +++ b/src/components/Mascot/SpeechArrow.js @@ -1,33 +1,43 @@ // @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, - size: number, - color: string, -} - -export default class SpeechArrow extends React.Component { - - render() { - return ( - - - - ); - } +type PropsType = { + style?: ViewStyle | null, + size: number, + color: string, +}; + +export default class SpeechArrow extends React.Component { + static defaultProps = { + style: null, + }; + + shouldComponentUpdate(): boolean { + return false; + } + + render(): React.Node { + const {props} = this; + return ( + + + + ); + } }