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()}
+
+
+
+ );
+}