Update mascot components to use TypeScript

This commit is contained in:
Arnaud Vergnet 2020-09-22 18:25:19 +02:00
parent e4530ded18
commit 172b7e8187
4 changed files with 137 additions and 183 deletions

View file

@ -17,27 +17,27 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {Image, TouchableWithoutFeedback, View} from 'react-native'; import {Image, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import {AnimatableProperties} from 'react-native-animatable';
export type AnimatableViewRefType = {current: null | Animatable.View}; export type AnimatableViewRefType = {
current: null | (typeof Animatable.View & View);
};
type PropsType = { type PropsType = {
emotion?: number, emotion?: MASCOT_STYLE;
animated?: boolean, animated?: boolean;
style?: ViewStyle | null, style?: ViewStyle;
entryAnimation?: Animatable.AnimatableProperties | null, entryAnimation?: AnimatableProperties<ViewStyle>;
loopAnimation?: Animatable.AnimatableProperties | null, loopAnimation?: AnimatableProperties<ViewStyle>;
onPress?: null | ((viewRef: AnimatableViewRefType) => void), onPress?: null | ((viewRef: AnimatableViewRefType) => void);
onLongPress?: null | ((viewRef: AnimatableViewRefType) => void), onLongPress?: null | ((viewRef: AnimatableViewRefType) => void);
}; };
type StateType = { type StateType = {
currentEmotion: number, currentEmotion: MASCOT_STYLE;
}; };
const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png'); const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png');
@ -50,32 +50,32 @@ const MASCOT_EYES_ANGRY = require('../../../assets/mascot/mascot_eyes_angry.png'
const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png'); const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png');
const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png'); const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png');
export const EYE_STYLE = { enum EYE_STYLE {
NORMAL: 0, NORMAL,
GIRLY: 2, GIRLY,
CUTE: 3, CUTE,
WINK: 4, WINK,
HEART: 5, HEART,
ANGRY: 6, ANGRY,
}; }
const GLASSES_STYLE = { enum GLASSES_STYLE {
NORMAL: 0, NORMAL,
COOl: 1, COOl,
}; }
export const MASCOT_STYLE = { export enum MASCOT_STYLE {
NORMAL: 0, NORMAL,
HAPPY: 1, HAPPY,
GIRLY: 2, GIRLY,
WINK: 3, WINK,
CUTE: 4, CUTE,
INTELLO: 5, INTELLO,
LOVE: 6, LOVE,
COOL: 7, COOL,
ANGRY: 8, ANGRY,
RANDOM: 999, RANDOM = 999,
}; }
class Mascot extends React.Component<PropsType, StateType> { class Mascot extends React.Component<PropsType, StateType> {
static defaultProps = { static defaultProps = {
@ -100,9 +100,9 @@ class Mascot extends React.Component<PropsType, StateType> {
viewRef: AnimatableViewRefType; viewRef: AnimatableViewRefType;
eyeList: {[key: number]: number | string}; eyeList: {[key in EYE_STYLE]: number};
glassesList: {[key: number]: number | string}; glassesList: {[key in GLASSES_STYLE]: number};
onPress: (viewRef: AnimatableViewRefType) => void; onPress: (viewRef: AnimatableViewRefType) => void;
@ -113,23 +113,25 @@ class Mascot extends React.Component<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.viewRef = React.createRef(); this.viewRef = React.createRef();
this.eyeList = {}; this.eyeList = {
this.glassesList = {}; [EYE_STYLE.NORMAL]: MASCOT_EYES_NORMAL,
this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL; [EYE_STYLE.GIRLY]: MASCOT_EYES_GIRLY,
this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY; [EYE_STYLE.CUTE]: MASCOT_EYES_CUTE,
this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE; [EYE_STYLE.WINK]: MASCOT_EYES_WINK,
this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK; [EYE_STYLE.HEART]: MASCOT_EYES_HEART,
this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART; [EYE_STYLE.ANGRY]: MASCOT_EYES_ANGRY,
this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY; };
this.glassesList = {
[GLASSES_STYLE.NORMAL]: MASCOT_GLASSES,
[GLASSES_STYLE.COOl]: MASCOT_SUNGLASSES,
};
this.initialEmotion = props.emotion
? props.emotion
: Mascot.defaultProps.emotion;
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES; if (this.initialEmotion === MASCOT_STYLE.RANDOM) {
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.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
}
this.state = { this.state = {
currentEmotion: this.initialEmotion, currentEmotion: this.initialEmotion,
@ -138,29 +140,33 @@ class Mascot extends React.Component<PropsType, StateType> {
if (props.onPress == null) { if (props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRefType) => { this.onPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current; const ref = viewRef.current;
if (ref != null) { if (ref && ref.rubberBand) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE}); this.setState({currentEmotion: MASCOT_STYLE.LOVE});
ref.rubberBand(1500).then(() => { ref.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
} }
}; };
} else this.onPress = props.onPress; } else {
this.onPress = props.onPress;
}
if (props.onLongPress == null) { if (props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRefType) => { this.onLongPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current; const ref = viewRef.current;
if (ref != null) { if (ref && ref.tada) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY}); this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
ref.tada(1000).then(() => { ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
} }
}; };
} else this.onLongPress = props.onLongPress; } else {
this.onLongPress = props.onLongPress;
}
} }
getGlasses(style: number): React.Node { getGlasses(style: GLASSES_STYLE) {
const glasses = this.glassesList[style]; const glasses = this.glassesList[style];
return ( return (
<Image <Image
@ -179,11 +185,7 @@ class Mascot extends React.Component<PropsType, StateType> {
); );
} }
getEye( getEye(style: EYE_STYLE, isRight: boolean, rotation: string = '0deg') {
style: number,
isRight: boolean,
rotation: string = '0deg',
): React.Node {
const eye = this.eyeList[style]; const eye = this.eyeList[style];
return ( return (
<Image <Image
@ -201,7 +203,7 @@ class Mascot extends React.Component<PropsType, StateType> {
); );
} }
getEyes(emotion: number): React.Node { getEyes(emotion: MASCOT_STYLE) {
const final = []; const final = [];
final.push( final.push(
<View <View
@ -246,7 +248,7 @@ class Mascot extends React.Component<PropsType, StateType> {
return final; return final;
} }
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
const entryAnimation = props.animated ? props.entryAnimation : null; const entryAnimation = props.animated ? props.entryAnimation : null;
const loopAnimation = props.animated ? props.loopAnimation : null; const loopAnimation = props.animated ? props.loopAnimation : null;
@ -256,7 +258,6 @@ class Mascot extends React.Component<PropsType, StateType> {
aspectRatio: 1, aspectRatio: 1,
...props.style, ...props.style,
}} }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...entryAnimation}> {...entryAnimation}>
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={() => { onPress={() => {
@ -266,9 +267,7 @@ class Mascot extends React.Component<PropsType, StateType> {
this.onLongPress(this.viewRef); this.onLongPress(this.viewRef);
}}> }}>
<Animatable.View ref={this.viewRef}> <Animatable.View ref={this.viewRef}>
<Animatable.View <Animatable.View {...loopAnimation}>
// eslint-disable-next-line react/jsx-props-no-spreading
{...loopAnimation}>
<Image <Image
source={MASCOT_IMAGE} source={MASCOT_IMAGE}
style={{ style={{

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {
Avatar, Avatar,
@ -37,48 +35,42 @@ import {
View, View,
} from 'react-native'; } from 'react-native';
import Mascot from './Mascot'; import Mascot from './Mascot';
import type {CustomThemeType} from '../../managers/ThemeManager';
import SpeechArrow from './SpeechArrow'; import SpeechArrow from './SpeechArrow';
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: ReactNativePaper.Theme;
icon: string, icon: string;
title: string, title: string;
message: string, message: string;
buttons: { buttons: {
action: { action?: {
message: string, message: string;
icon: string | null, icon?: string;
color: string | null, color?: string;
onPress?: () => void, onPress?: () => void;
}, };
cancel: { cancel?: {
message: string, message: string;
icon: string | null, icon?: string;
color: string | null, color?: string;
onPress?: () => void, onPress?: () => void;
}, };
}, };
emotion: number, emotion: number;
visible?: boolean, visible?: boolean;
prefKey?: string, prefKey?: string;
}; };
type StateType = { type StateType = {
shouldRenderDialog: boolean, // Used to stop rendering after hide animation shouldRenderDialog: boolean; // Used to stop rendering after hide animation
dialogVisible: boolean, dialogVisible: boolean;
}; };
/** /**
* Component used to display a popup with the mascot. * Component used to display a popup with the mascot.
*/ */
class MascotPopup extends React.Component<PropsType, StateType> { class MascotPopup extends React.Component<PropsType, StateType> {
static defaultProps = {
visible: null,
prefKey: null,
};
mascotSize: number; mascotSize: number;
windowWidth: number; windowWidth: number;
@ -112,7 +104,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
} }
} }
componentDidMount(): * { componentDidMount() {
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid, this.onBackButtonPressAndroid,
@ -146,14 +138,20 @@ class MascotPopup extends React.Component<PropsType, StateType> {
if (state.dialogVisible) { if (state.dialogVisible) {
const {cancel} = props.buttons; const {cancel} = props.buttons;
const {action} = props.buttons; const {action} = props.buttons;
if (cancel != null) this.onDismiss(cancel.onPress); if (cancel) {
else this.onDismiss(action.onPress); this.onDismiss(cancel.onPress);
} else if (action) {
this.onDismiss(action.onPress);
} else {
this.onDismiss();
}
return true; return true;
} }
return false; return false;
}; };
getSpeechBubble(): React.Node { getSpeechBubble() {
const {state, props} = this; const {state, props} = this;
return ( return (
<Animatable.View <Animatable.View
@ -179,7 +177,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
title={props.title} title={props.title}
left={ left={
props.icon != null props.icon != null
? (): React.Node => ( ? () => (
<Avatar.Icon <Avatar.Icon
size={48} size={48}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
@ -187,7 +185,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
icon={props.icon} icon={props.icon}
/> />
) )
: null : undefined
} }
/> />
<Card.Content <Card.Content
@ -207,7 +205,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
); );
} }
getMascot(): React.Node { getMascot() {
const {props, state} = this; const {props, state} = this;
return ( return (
<Animatable.View <Animatable.View
@ -223,7 +221,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
); );
} }
getButtons(): React.Node { getButtons() {
const {props} = this; const {props} = this;
const {action} = props.buttons; const {action} = props.buttons;
const {cancel} = props.buttons; const {cancel} = props.buttons;
@ -270,12 +268,12 @@ class MascotPopup extends React.Component<PropsType, StateType> {
); );
} }
getBackground(): React.Node { getBackground() {
const {props, state} = this; const {props, state} = this;
return ( return (
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={() => { onPress={() => {
this.onDismiss(props.buttons.cancel.onPress); this.onDismiss(props.buttons.cancel?.onPress);
}}> }}>
<Animatable.View <Animatable.View
style={{ style={{
@ -298,10 +296,12 @@ class MascotPopup extends React.Component<PropsType, StateType> {
AsyncStorageManager.set(prefKey, false); AsyncStorageManager.set(prefKey, false);
this.setState({dialogVisible: false}); this.setState({dialogVisible: false});
} }
if (callback != null) callback(); if (callback != null) {
callback();
}
}; };
render(): React.Node { render() {
const {shouldRenderDialog} = this.state; const {shouldRenderDialog} = this.state;
if (shouldRenderDialog) { if (shouldRenderDialog) {
return ( return (

View file

@ -1,62 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {View} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
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;
}
render(): React.Node {
const {props} = this;
return (
<View style={props.style}>
<View
style={{
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: props.size,
borderBottomWidth: props.size,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: props.color,
}}
/>
</View>
);
}
}

View file

@ -17,15 +17,32 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow import * as React from 'react';
import {View, ViewStyle} from 'react-native';
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; type PropsType = {
style?: ViewStyle;
export type ListIconPropsType = { size: number;
color: string, color: string;
style: ViewStyleProp,
}; };
export type CardTitleIconPropsType = { export default function SpeechArrow(props: PropsType) {
size: number, return (
}; <View style={props.style}>
<View
style={{
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: props.size,
borderBottomWidth: props.size,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: props.color,
}}
/>
</View>
);
}