diff --git a/src/components/Mascot/MascotPopup.tsx b/src/components/Mascot/MascotPopup.tsx index d2df012..21c5f61 100644 --- a/src/components/Mascot/MascotPopup.tsx +++ b/src/components/Mascot/MascotPopup.tsx @@ -17,81 +17,31 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; -import { - Avatar, - Button, - Card, - Paragraph, - Portal, - withTheme, -} from 'react-native-paper'; +import React, { useEffect, useRef, useState } from 'react'; +import { Portal, withTheme } from 'react-native-paper'; import * as Animatable from 'react-native-animatable'; import { BackHandler, Dimensions, - ScrollView, StyleSheet, TouchableWithoutFeedback, View, } from 'react-native'; import Mascot from './Mascot'; -import SpeechArrow from './SpeechArrow'; import AsyncStorageManager from '../../managers/AsyncStorageManager'; import GENERAL_STYLES from '../../constants/Styles'; +import MascotSpeechBubble, { + MascotSpeechBubbleProps, +} from './MascotSpeechBubble'; +import { useMountEffect } from '../../utils/customHooks'; -type PropsType = { - theme: ReactNativePaper.Theme; - icon: string; - title: string; - message: string; - buttons: { - action?: { - message: string; - icon?: string; - color?: string; - onPress?: () => void; - }; - cancel?: { - message: string; - icon?: string; - color?: string; - onPress?: () => void; - }; - }; +type PropsType = MascotSpeechBubbleProps & { emotion: number; visible?: boolean; prefKey?: string; }; -type StateType = { - shouldRenderDialog: boolean; // Used to stop rendering after hide animation - dialogVisible: boolean; -}; - const styles = StyleSheet.create({ - speechBubbleContainer: { - marginLeft: '10%', - marginRight: '10%', - }, - speechBubbleCard: { - borderWidth: 4, - borderRadius: 10, - }, - speechBubbleIcon: { - backgroundColor: 'transparent', - }, - speechBubbleText: { - marginBottom: 10, - }, - actionsContainer: { - marginTop: 10, - marginBottom: 10, - }, - button: { - ...GENERAL_STYLES.centerHorizontal, - marginBottom: 10, - }, background: { position: 'absolute', backgroundColor: 'rgba(0,0,0,0.7)', @@ -104,245 +54,139 @@ const styles = StyleSheet.create({ }, }); +const MASCOT_SIZE = Dimensions.get('window').height / 6; +const BUBBLE_HEIGHT = Dimensions.get('window').height / 3; + /** * Component used to display a popup with the mascot. */ -class MascotPopup extends React.Component { - mascotSize: number; - - windowWidth: number; - - windowHeight: number; - - constructor(props: PropsType) { - super(props); - - 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, - }; +function MascotPopup(props: PropsType) { + const isVisible = () => { + if (props.visible !== undefined) { + return props.visible; } else if (props.prefKey != null) { - const visible = AsyncStorageManager.getBool(props.prefKey); - this.state = { - shouldRenderDialog: visible, - dialogVisible: visible, - }; + return AsyncStorageManager.getBool(props.prefKey); } else { - this.state = { - shouldRenderDialog: false, - dialogVisible: false, - }; + return false; } - } - - componentDidMount() { - BackHandler.addEventListener( - 'hardwareBackPress', - this.onBackButtonPressAndroid - ); - } - - shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean { - // TODO this is so dirty it shouldn't even work - 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; - } - - onAnimationEnd = () => { - this.setState({ - shouldRenderDialog: false, - }); }; - onBackButtonPressAndroid = (): boolean => { - const { state, props } = this; - if (state.dialogVisible) { + const [shouldRenderDialog, setShouldRenderDialog] = useState(isVisible()); + const [dialogVisible, setDialogVisible] = useState(isVisible()); + const lastVisibleProps = useRef(props.visible); + const lastVisibleState = useRef(dialogVisible); + + useMountEffect(() => { + BackHandler.addEventListener('hardwareBackPress', onBackButtonPressAndroid); + }); + + useEffect(() => { + if (props.visible && !dialogVisible) { + setShouldRenderDialog(true); + setDialogVisible(true); + } else if ( + lastVisibleProps.current !== props.visible || + (!dialogVisible && dialogVisible !== lastVisibleState.current) + ) { + setDialogVisible(false); + setTimeout(onAnimationEnd, 400); + } + lastVisibleProps.current = props.visible; + lastVisibleState.current = dialogVisible; + }, [props.visible, dialogVisible]); + + const onAnimationEnd = () => { + setShouldRenderDialog(false); + }; + + const onBackButtonPressAndroid = (): boolean => { + if (dialogVisible) { const { cancel } = props.buttons; const { action } = props.buttons; if (cancel) { - this.onDismiss(cancel.onPress); + onDismiss(cancel.onPress); } else if (action) { - this.onDismiss(action.onPress); + onDismiss(action.onPress); } else { - this.onDismiss(); + onDismiss(); } - return true; } return false; }; - getSpeechBubble() { - const { state, props } = this; + const getSpeechBubble = () => { return ( - - - - ( - - ) - : undefined - } - /> - - - - {props.message} - - - - - - {this.getButtons()} - - - + ); - } + }; - getMascot() { - const { props, state } = this; + const getMascot = () => { return ( ); - } + }; - getButtons() { - const { props } = this; - const { action } = props.buttons; - const { cancel } = props.buttons; - return ( - - {action != null ? ( - - ) : null} - {cancel != null ? ( - - ) : null} - - ); - } - - getBackground() { - const { props, state } = this; + const getBackground = () => { return ( { - this.onDismiss(props.buttons.cancel?.onPress); + onDismiss(props.buttons.cancel?.onPress); }} > ); - } + }; - onDismiss = (callback?: () => void) => { - const { prefKey } = this.props; - if (prefKey != null) { - AsyncStorageManager.set(prefKey, false); - this.setState({ dialogVisible: false }); + const onDismiss = (callback?: () => void) => { + if (props.prefKey != null) { + AsyncStorageManager.set(props.prefKey, false); + setDialogVisible(false); } - if (callback != null) { + if (callback) { callback(); } }; - render() { - const { shouldRenderDialog } = this.state; - if (shouldRenderDialog) { - return ( - - {this.getBackground()} - - - {this.getMascot()} - {this.getSpeechBubble()} - + if (shouldRenderDialog) { + return ( + + {getBackground()} + + + {getMascot()} + {getSpeechBubble()} - - ); - } - return null; + + + ); } + return null; } -export default withTheme(MascotPopup); +export default MascotPopup; diff --git a/src/components/Mascot/MascotSpeechBubble.tsx b/src/components/Mascot/MascotSpeechBubble.tsx new file mode 100644 index 0000000..ddfc8fd --- /dev/null +++ b/src/components/Mascot/MascotSpeechBubble.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import * as Animatable from 'react-native-animatable'; +import { Avatar, Button, Card, Paragraph, useTheme } from 'react-native-paper'; +import GENERAL_STYLES from '../../constants/Styles'; +import SpeechArrow from './SpeechArrow'; + +export type MascotSpeechBubbleProps = { + icon: string; + title: string; + message: string; + visible?: boolean; + buttons: { + action?: { + message: string; + icon?: string; + color?: string; + onPress?: () => void; + }; + cancel?: { + message: string; + icon?: string; + color?: string; + onPress?: () => void; + }; + }; +}; + +type Props = MascotSpeechBubbleProps & { + onDismiss: (callback?: () => void) => void; + speechArrowPos: number; + bubbleMaxHeight: number; +}; + +const styles = StyleSheet.create({ + speechBubbleContainer: { + marginLeft: '10%', + marginRight: '10%', + }, + speechBubbleCard: { + borderWidth: 4, + borderRadius: 10, + }, + speechBubbleIcon: { + backgroundColor: 'transparent', + }, + speechBubbleText: { + marginBottom: 10, + }, + actionsContainer: { + marginTop: 10, + marginBottom: 10, + }, + button: { + ...GENERAL_STYLES.centerHorizontal, + marginBottom: 10, + }, +}); + +export default function MascotSpeechBubble(props: Props) { + const theme = useTheme(); + const getButtons = () => { + const { action, cancel } = props.buttons; + return ( + + {action ? ( + + ) : null} + {cancel != null ? ( + + ) : null} + + ); + }; + + return ( + + + + ( + + ) + : undefined + } + /> + + + + {props.message} + + + + + + {getButtons()} + + + + ); +}