Browse Source

Convert mascot popup to functional

THis fixes TS issues
Arnaud Vergnet 6 months ago
parent
commit
5795fca035
2 changed files with 230 additions and 239 deletions
  1. 83
    239
      src/components/Mascot/MascotPopup.tsx
  2. 147
    0
      src/components/Mascot/MascotSpeechBubble.tsx

+ 83
- 239
src/components/Mascot/MascotPopup.tsx View File

@@ -17,81 +17,31 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
21
-import {
22
-  Avatar,
23
-  Button,
24
-  Card,
25
-  Paragraph,
26
-  Portal,
27
-  withTheme,
28
-} from 'react-native-paper';
20
+import React, { useEffect, useRef, useState } from 'react';
21
+import { Portal, withTheme } from 'react-native-paper';
29 22
 import * as Animatable from 'react-native-animatable';
30 23
 import {
31 24
   BackHandler,
32 25
   Dimensions,
33
-  ScrollView,
34 26
   StyleSheet,
35 27
   TouchableWithoutFeedback,
36 28
   View,
37 29
 } from 'react-native';
38 30
 import Mascot from './Mascot';
39
-import SpeechArrow from './SpeechArrow';
40 31
 import AsyncStorageManager from '../../managers/AsyncStorageManager';
41 32
 import GENERAL_STYLES from '../../constants/Styles';
33
+import MascotSpeechBubble, {
34
+  MascotSpeechBubbleProps,
35
+} from './MascotSpeechBubble';
36
+import { useMountEffect } from '../../utils/customHooks';
42 37
 
43
-type PropsType = {
44
-  theme: ReactNativePaper.Theme;
45
-  icon: string;
46
-  title: string;
47
-  message: string;
48
-  buttons: {
49
-    action?: {
50
-      message: string;
51
-      icon?: string;
52
-      color?: string;
53
-      onPress?: () => void;
54
-    };
55
-    cancel?: {
56
-      message: string;
57
-      icon?: string;
58
-      color?: string;
59
-      onPress?: () => void;
60
-    };
61
-  };
38
+type PropsType = MascotSpeechBubbleProps & {
62 39
   emotion: number;
63 40
   visible?: boolean;
64 41
   prefKey?: string;
65 42
 };
66 43
 
67
-type StateType = {
68
-  shouldRenderDialog: boolean; // Used to stop rendering after hide animation
69
-  dialogVisible: boolean;
70
-};
71
-
72 44
 const styles = StyleSheet.create({
73
-  speechBubbleContainer: {
74
-    marginLeft: '10%',
75
-    marginRight: '10%',
76
-  },
77
-  speechBubbleCard: {
78
-    borderWidth: 4,
79
-    borderRadius: 10,
80
-  },
81
-  speechBubbleIcon: {
82
-    backgroundColor: 'transparent',
83
-  },
84
-  speechBubbleText: {
85
-    marginBottom: 10,
86
-  },
87
-  actionsContainer: {
88
-    marginTop: 10,
89
-    marginBottom: 10,
90
-  },
91
-  button: {
92
-    ...GENERAL_STYLES.centerHorizontal,
93
-    marginBottom: 10,
94
-  },
95 45
   background: {
96 46
     position: 'absolute',
97 47
     backgroundColor: 'rgba(0,0,0,0.7)',
@@ -104,245 +54,139 @@ const styles = StyleSheet.create({
104 54
   },
105 55
 });
106 56
 
57
+const MASCOT_SIZE = Dimensions.get('window').height / 6;
58
+const BUBBLE_HEIGHT = Dimensions.get('window').height / 3;
59
+
107 60
 /**
108 61
  * Component used to display a popup with the mascot.
109 62
  */
110
-class MascotPopup extends React.Component<PropsType, StateType> {
111
-  mascotSize: number;
112
-
113
-  windowWidth: number;
114
-
115
-  windowHeight: number;
116
-
117
-  constructor(props: PropsType) {
118
-    super(props);
119
-
120
-    this.windowWidth = Dimensions.get('window').width;
121
-    this.windowHeight = Dimensions.get('window').height;
122
-
123
-    this.mascotSize = Dimensions.get('window').height / 6;
124
-
125
-    if (props.visible != null) {
126
-      this.state = {
127
-        shouldRenderDialog: props.visible,
128
-        dialogVisible: props.visible,
129
-      };
63
+function MascotPopup(props: PropsType) {
64
+  const isVisible = () => {
65
+    if (props.visible !== undefined) {
66
+      return props.visible;
130 67
     } else if (props.prefKey != null) {
131
-      const visible = AsyncStorageManager.getBool(props.prefKey);
132
-      this.state = {
133
-        shouldRenderDialog: visible,
134
-        dialogVisible: visible,
135
-      };
68
+      return AsyncStorageManager.getBool(props.prefKey);
136 69
     } else {
137
-      this.state = {
138
-        shouldRenderDialog: false,
139
-        dialogVisible: false,
140
-      };
70
+      return false;
141 71
     }
142
-  }
72
+  };
143 73
 
144
-  componentDidMount() {
145
-    BackHandler.addEventListener(
146
-      'hardwareBackPress',
147
-      this.onBackButtonPressAndroid
148
-    );
149
-  }
74
+  const [shouldRenderDialog, setShouldRenderDialog] = useState(isVisible());
75
+  const [dialogVisible, setDialogVisible] = useState(isVisible());
76
+  const lastVisibleProps = useRef(props.visible);
77
+  const lastVisibleState = useRef(dialogVisible);
78
+
79
+  useMountEffect(() => {
80
+    BackHandler.addEventListener('hardwareBackPress', onBackButtonPressAndroid);
81
+  });
150 82
 
151
-  shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean {
152
-    // TODO this is so dirty it shouldn't even work
153
-    const { props, state } = this;
154
-    if (nextProps.visible) {
155
-      this.state.shouldRenderDialog = true;
156
-      this.state.dialogVisible = true;
83
+  useEffect(() => {
84
+    if (props.visible && !dialogVisible) {
85
+      setShouldRenderDialog(true);
86
+      setDialogVisible(true);
157 87
     } else if (
158
-      nextProps.visible !== props.visible ||
159
-      (!nextState.dialogVisible &&
160
-        nextState.dialogVisible !== state.dialogVisible)
88
+      lastVisibleProps.current !== props.visible ||
89
+      (!dialogVisible && dialogVisible !== lastVisibleState.current)
161 90
     ) {
162
-      this.state.dialogVisible = false;
163
-      setTimeout(this.onAnimationEnd, 300);
91
+      setDialogVisible(false);
92
+      setTimeout(onAnimationEnd, 400);
164 93
     }
165
-    return true;
166
-  }
94
+    lastVisibleProps.current = props.visible;
95
+    lastVisibleState.current = dialogVisible;
96
+  }, [props.visible, dialogVisible]);
167 97
 
168
-  onAnimationEnd = () => {
169
-    this.setState({
170
-      shouldRenderDialog: false,
171
-    });
98
+  const onAnimationEnd = () => {
99
+    setShouldRenderDialog(false);
172 100
   };
173 101
 
174
-  onBackButtonPressAndroid = (): boolean => {
175
-    const { state, props } = this;
176
-    if (state.dialogVisible) {
102
+  const onBackButtonPressAndroid = (): boolean => {
103
+    if (dialogVisible) {
177 104
       const { cancel } = props.buttons;
178 105
       const { action } = props.buttons;
179 106
       if (cancel) {
180
-        this.onDismiss(cancel.onPress);
107
+        onDismiss(cancel.onPress);
181 108
       } else if (action) {
182
-        this.onDismiss(action.onPress);
109
+        onDismiss(action.onPress);
183 110
       } else {
184
-        this.onDismiss();
111
+        onDismiss();
185 112
       }
186
-
187 113
       return true;
188 114
     }
189 115
     return false;
190 116
   };
191 117
 
192
-  getSpeechBubble() {
193
-    const { state, props } = this;
118
+  const getSpeechBubble = () => {
194 119
     return (
195
-      <Animatable.View
196
-        style={styles.speechBubbleContainer}
197
-        useNativeDriver
198
-        animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
199
-        duration={state.dialogVisible ? 1000 : 300}
200
-      >
201
-        <SpeechArrow
202
-          style={{ marginLeft: this.mascotSize / 3 }}
203
-          size={20}
204
-          color={props.theme.colors.mascotMessageArrow}
205
-        />
206
-        <Card
207
-          style={{
208
-            borderColor: props.theme.colors.mascotMessageArrow,
209
-            ...styles.speechBubbleCard,
210
-          }}
211
-        >
212
-          <Card.Title
213
-            title={props.title}
214
-            left={
215
-              props.icon != null
216
-                ? () => (
217
-                    <Avatar.Icon
218
-                      size={48}
219
-                      style={styles.speechBubbleIcon}
220
-                      color={props.theme.colors.primary}
221
-                      icon={props.icon}
222
-                    />
223
-                  )
224
-                : undefined
225
-            }
226
-          />
227
-          <Card.Content
228
-            style={{
229
-              maxHeight: this.windowHeight / 3,
230
-            }}
231
-          >
232
-            <ScrollView>
233
-              <Paragraph style={styles.speechBubbleText}>
234
-                {props.message}
235
-              </Paragraph>
236
-            </ScrollView>
237
-          </Card.Content>
238
-
239
-          <Card.Actions style={styles.actionsContainer}>
240
-            {this.getButtons()}
241
-          </Card.Actions>
242
-        </Card>
243
-      </Animatable.View>
120
+      <MascotSpeechBubble
121
+        title={props.title}
122
+        message={props.message}
123
+        icon={props.icon}
124
+        buttons={props.buttons}
125
+        visible={dialogVisible}
126
+        onDismiss={onDismiss}
127
+        speechArrowPos={MASCOT_SIZE / 3}
128
+        bubbleMaxHeight={BUBBLE_HEIGHT}
129
+      />
244 130
     );
245
-  }
131
+  };
246 132
 
247
-  getMascot() {
248
-    const { props, state } = this;
133
+  const getMascot = () => {
249 134
     return (
250 135
       <Animatable.View
251 136
         useNativeDriver
252
-        animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
253
-        duration={state.dialogVisible ? 1500 : 200}
137
+        animation={dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
138
+        duration={dialogVisible ? 1500 : 200}
254 139
       >
255 140
         <Mascot
256
-          style={{ width: this.mascotSize }}
141
+          style={{ width: MASCOT_SIZE }}
257 142
           animated
258 143
           emotion={props.emotion}
259 144
         />
260 145
       </Animatable.View>
261 146
     );
262
-  }
263
-
264
-  getButtons() {
265
-    const { props } = this;
266
-    const { action } = props.buttons;
267
-    const { cancel } = props.buttons;
268
-    return (
269
-      <View style={GENERAL_STYLES.center}>
270
-        {action != null ? (
271
-          <Button
272
-            style={styles.button}
273
-            mode="contained"
274
-            icon={action.icon}
275
-            color={action.color}
276
-            onPress={() => {
277
-              this.onDismiss(action.onPress);
278
-            }}
279
-          >
280
-            {action.message}
281
-          </Button>
282
-        ) : null}
283
-        {cancel != null ? (
284
-          <Button
285
-            style={styles.button}
286
-            mode="contained"
287
-            icon={cancel.icon}
288
-            color={cancel.color}
289
-            onPress={() => {
290
-              this.onDismiss(cancel.onPress);
291
-            }}
292
-          >
293
-            {cancel.message}
294
-          </Button>
295
-        ) : null}
296
-      </View>
297
-    );
298
-  }
147
+  };
299 148
 
300
-  getBackground() {
301
-    const { props, state } = this;
149
+  const getBackground = () => {
302 150
     return (
303 151
       <TouchableWithoutFeedback
304 152
         onPress={() => {
305
-          this.onDismiss(props.buttons.cancel?.onPress);
153
+          onDismiss(props.buttons.cancel?.onPress);
306 154
         }}
307 155
       >
308 156
         <Animatable.View
309 157
           style={styles.background}
310 158
           useNativeDriver
311
-          animation={state.dialogVisible ? 'fadeIn' : 'fadeOut'}
312
-          duration={state.dialogVisible ? 300 : 300}
159
+          animation={dialogVisible ? 'fadeIn' : 'fadeOut'}
160
+          duration={dialogVisible ? 300 : 300}
313 161
         />
314 162
       </TouchableWithoutFeedback>
315 163
     );
316
-  }
164
+  };
317 165
 
318
-  onDismiss = (callback?: () => void) => {
319
-    const { prefKey } = this.props;
320
-    if (prefKey != null) {
321
-      AsyncStorageManager.set(prefKey, false);
322
-      this.setState({ dialogVisible: false });
166
+  const onDismiss = (callback?: () => void) => {
167
+    if (props.prefKey != null) {
168
+      AsyncStorageManager.set(props.prefKey, false);
169
+      setDialogVisible(false);
323 170
     }
324
-    if (callback != null) {
171
+    if (callback) {
325 172
       callback();
326 173
     }
327 174
   };
328 175
 
329
-  render() {
330
-    const { shouldRenderDialog } = this.state;
331
-    if (shouldRenderDialog) {
332
-      return (
333
-        <Portal>
334
-          {this.getBackground()}
335
-          <View style={GENERAL_STYLES.centerVertical}>
336
-            <View style={styles.container}>
337
-              {this.getMascot()}
338
-              {this.getSpeechBubble()}
339
-            </View>
176
+  if (shouldRenderDialog) {
177
+    return (
178
+      <Portal>
179
+        {getBackground()}
180
+        <View style={GENERAL_STYLES.centerVertical}>
181
+          <View style={styles.container}>
182
+            {getMascot()}
183
+            {getSpeechBubble()}
340 184
           </View>
341
-        </Portal>
342
-      );
343
-    }
344
-    return null;
185
+        </View>
186
+      </Portal>
187
+    );
345 188
   }
189
+  return null;
346 190
 }
347 191
 
348
-export default withTheme(MascotPopup);
192
+export default MascotPopup;

+ 147
- 0
src/components/Mascot/MascotSpeechBubble.tsx View File

@@ -0,0 +1,147 @@
1
+import React from 'react';
2
+import { ScrollView, StyleSheet, View } from 'react-native';
3
+import * as Animatable from 'react-native-animatable';
4
+import { Avatar, Button, Card, Paragraph, useTheme } from 'react-native-paper';
5
+import GENERAL_STYLES from '../../constants/Styles';
6
+import SpeechArrow from './SpeechArrow';
7
+
8
+export type MascotSpeechBubbleProps = {
9
+  icon: string;
10
+  title: string;
11
+  message: string;
12
+  visible?: boolean;
13
+  buttons: {
14
+    action?: {
15
+      message: string;
16
+      icon?: string;
17
+      color?: string;
18
+      onPress?: () => void;
19
+    };
20
+    cancel?: {
21
+      message: string;
22
+      icon?: string;
23
+      color?: string;
24
+      onPress?: () => void;
25
+    };
26
+  };
27
+};
28
+
29
+type Props = MascotSpeechBubbleProps & {
30
+  onDismiss: (callback?: () => void) => void;
31
+  speechArrowPos: number;
32
+  bubbleMaxHeight: number;
33
+};
34
+
35
+const styles = StyleSheet.create({
36
+  speechBubbleContainer: {
37
+    marginLeft: '10%',
38
+    marginRight: '10%',
39
+  },
40
+  speechBubbleCard: {
41
+    borderWidth: 4,
42
+    borderRadius: 10,
43
+  },
44
+  speechBubbleIcon: {
45
+    backgroundColor: 'transparent',
46
+  },
47
+  speechBubbleText: {
48
+    marginBottom: 10,
49
+  },
50
+  actionsContainer: {
51
+    marginTop: 10,
52
+    marginBottom: 10,
53
+  },
54
+  button: {
55
+    ...GENERAL_STYLES.centerHorizontal,
56
+    marginBottom: 10,
57
+  },
58
+});
59
+
60
+export default function MascotSpeechBubble(props: Props) {
61
+  const theme = useTheme();
62
+  const getButtons = () => {
63
+    const { action, cancel } = props.buttons;
64
+    return (
65
+      <View style={GENERAL_STYLES.center}>
66
+        {action ? (
67
+          <Button
68
+            style={styles.button}
69
+            mode="contained"
70
+            icon={action.icon}
71
+            color={action.color}
72
+            onPress={() => {
73
+              props.onDismiss(action.onPress);
74
+            }}
75
+          >
76
+            {action.message}
77
+          </Button>
78
+        ) : null}
79
+        {cancel != null ? (
80
+          <Button
81
+            style={styles.button}
82
+            mode="contained"
83
+            icon={cancel.icon}
84
+            color={cancel.color}
85
+            onPress={() => {
86
+              props.onDismiss(cancel.onPress);
87
+            }}
88
+          >
89
+            {cancel.message}
90
+          </Button>
91
+        ) : null}
92
+      </View>
93
+    );
94
+  };
95
+
96
+  return (
97
+    <Animatable.View
98
+      style={styles.speechBubbleContainer}
99
+      useNativeDriver={true}
100
+      animation={props.visible ? 'bounceInLeft' : 'bounceOutLeft'}
101
+      duration={props.visible ? 1000 : 300}
102
+    >
103
+      <SpeechArrow
104
+        style={{ marginLeft: props.speechArrowPos }}
105
+        size={20}
106
+        color={theme.colors.mascotMessageArrow}
107
+      />
108
+      <Card
109
+        style={{
110
+          borderColor: theme.colors.mascotMessageArrow,
111
+          ...styles.speechBubbleCard,
112
+        }}
113
+      >
114
+        <Card.Title
115
+          title={props.title}
116
+          left={
117
+            props.icon
118
+              ? () => (
119
+                  <Avatar.Icon
120
+                    size={48}
121
+                    style={styles.speechBubbleIcon}
122
+                    color={theme.colors.primary}
123
+                    icon={props.icon}
124
+                  />
125
+                )
126
+              : undefined
127
+          }
128
+        />
129
+        <Card.Content
130
+          style={{
131
+            maxHeight: props.bubbleMaxHeight,
132
+          }}
133
+        >
134
+          <ScrollView>
135
+            <Paragraph style={styles.speechBubbleText}>
136
+              {props.message}
137
+            </Paragraph>
138
+          </ScrollView>
139
+        </Card.Content>
140
+
141
+        <Card.Actions style={styles.actionsContainer}>
142
+          {getButtons()}
143
+        </Card.Actions>
144
+      </Card>
145
+    </Animatable.View>
146
+  );
147
+}

Loading…
Cancel
Save