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

View file

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

View file

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