Browse Source

refactor planex screen to functionnal component

Arnaud Vergnet 2 years ago
parent
commit
aefeb8373a
1 changed files with 138 additions and 174 deletions
  1. 138
    174
      src/screens/Planex/PlanexScreen.tsx

+ 138
- 174
src/screens/Planex/PlanexScreen.tsx View File

@@ -17,8 +17,8 @@
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 { Title, withTheme } from 'react-native-paper';
20
+import React, { useCallback, useRef, useState } from 'react';
21
+import { Title, useTheme } from 'react-native-paper';
22 22
 import i18n from 'i18n-js';
23 23
 import {
24 24
   NativeScrollEvent,
@@ -26,8 +26,11 @@ import {
26 26
   StyleSheet,
27 27
   View,
28 28
 } from 'react-native';
29
-import { CommonActions } from '@react-navigation/native';
30
-import { StackNavigationProp } from '@react-navigation/stack';
29
+import {
30
+  CommonActions,
31
+  useFocusEffect,
32
+  useNavigation,
33
+} from '@react-navigation/native';
31 34
 import Autolink from 'react-native-autolink';
32 35
 import ThemeManager from '../../managers/ThemeManager';
33 36
 import WebViewScreen from '../../components/Screens/WebViewScreen';
@@ -43,20 +46,7 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
43 46
 import { getPrettierPlanexGroupName } from '../../utils/Utils';
44 47
 import GENERAL_STYLES from '../../constants/Styles';
45 48
 import Urls from '../../constants/Urls';
46
-
47
-type PropsType = {
48
-  navigation: StackNavigationProp<any>;
49
-  route: { params: { group: PlanexGroupType } };
50
-  theme: ReactNativePaper.Theme;
51
-};
52
-
53
-type StateType = {
54
-  dialogVisible: boolean;
55
-  dialogTitle: string | React.ReactNode;
56
-  dialogMessage: string;
57
-  currentGroup: PlanexGroupType;
58
-  injectJS: string;
59
-};
49
+import { useMountEffect } from '../../utils/customHooks';
60 50
 
61 51
 // // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing
62 52
 // // Remove alpha from given Jquery node
@@ -131,14 +121,19 @@ calendar.option({
131 121
   }
132 122
 });`;
133 123
 
124
+// Mobile friendly CSS
134 125
 const CUSTOM_CSS =
135 126
   'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
127
+
128
+// Dark mode CSS, to be used with the mobile friendly css
136 129
 const CUSTOM_CSS_DARK =
137 130
   'body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
138 131
 
139
-const INJECT_STYLE = `
140
-$('head').append('<style>${CUSTOM_CSS}</style>');
141
-`;
132
+// Inject the custom css into the webpage
133
+const INJECT_STYLE = `$('head').append('<style>${CUSTOM_CSS}</style>');`;
134
+
135
+// Inject the dark mode into the webpage, to call after the custom css inject above
136
+const INJECT_STYLE_DARK = `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
142 137
 
143 138
 const styles = StyleSheet.create({
144 139
   container: {
@@ -148,60 +143,64 @@ const styles = StyleSheet.create({
148 143
   },
149 144
 });
150 145
 
151
-/**
152
- * Class defining the app's Planex screen.
153
- * This screen uses a webview to render the page
154
- */
155
-class PlanexScreen extends React.Component<PropsType, StateType> {
156
-  barRef: { current: null | AnimatedBottomBar };
146
+type Props = {
147
+  route: {
148
+    params: {
149
+      group?: PlanexGroupType;
150
+    };
151
+  };
152
+};
157 153
 
158
-  /**
159
-   * Defines custom injected JavaScript to improve the page display on mobile
160
-   */
161
-  constructor(props: PropsType) {
162
-    super(props);
163
-    this.barRef = React.createRef();
154
+function PlanexScreen(props: Props) {
155
+  const navigation = useNavigation();
156
+  const theme = useTheme();
157
+  const barRef = useRef<typeof AnimatedBottomBar>();
158
+
159
+  const [dialogContent, setDialogContent] = useState<
160
+    | undefined
161
+    | {
162
+        title: string | React.ReactElement;
163
+        message: string | React.ReactElement;
164
+      }
165
+  >();
166
+  const [injectJS, setInjectJS] = useState('');
167
+  const [currentGroup, setCurrentGroup] = useState<
168
+    PlanexGroupType | undefined
169
+  >();
170
+
171
+  useMountEffect(() => {
164 172
     let currentGroupString = AsyncStorageManager.getString(
165 173
       AsyncStorageManager.PREFERENCES.planexCurrentGroup.key
166 174
     );
167
-    let currentGroup: PlanexGroupType;
168
-    if (currentGroupString === '') {
169
-      currentGroup = { name: 'SELECT GROUP', id: -1 };
170
-    } else {
171
-      currentGroup = JSON.parse(currentGroupString);
172
-      props.navigation.setOptions({
173
-        title: getPrettierPlanexGroupName(currentGroup.name),
175
+    let group: PlanexGroupType;
176
+    if (currentGroupString !== '') {
177
+      group = JSON.parse(currentGroupString);
178
+      navigation.setOptions({
179
+        title: getPrettierPlanexGroupName(group.name),
174 180
       });
181
+      setCurrentGroup(group);
175 182
     }
176
-    this.state = {
177
-      dialogVisible: false,
178
-      dialogTitle: '',
179
-      dialogMessage: '',
180
-      currentGroup,
181
-      injectJS: '',
182
-    };
183
-  }
184
-
185
-  /**
186
-   * Register for events and show the banner after 2 seconds
187
-   */
188
-  componentDidMount() {
189
-    const { navigation } = this.props;
190
-    navigation.addListener('focus', this.onScreenFocus);
191
-  }
183
+  });
192 184
 
185
+  useFocusEffect(
186
+    useCallback(() => {
187
+      if (props.route.params?.group) {
188
+        // reset params to prevent infinite loop
189
+        selectNewGroup(props.route.params.group);
190
+        navigation.dispatch(CommonActions.setParams({ group: undefined }));
191
+      }
192
+      // eslint-disable-next-line react-hooks/exhaustive-deps
193
+    }, [])
194
+  );
193 195
   /**
194 196
    * Gets the Webview, with an error view on top if no group is selected.
195 197
    *
196 198
    * @returns {*}
197 199
    */
198
-  getWebView() {
199
-    const { state } = this;
200
-    const showWebview = state.currentGroup.id !== -1;
201
-
200
+  const getWebView = () => {
202 201
     return (
203 202
       <View style={GENERAL_STYLES.flex}>
204
-        {!showWebview ? (
203
+        {!currentGroup ? (
205 204
           <ErrorView
206 205
             icon={'account-clock'}
207 206
             message={i18n.t('screens.planex.noGroupSelected')}
@@ -209,28 +208,21 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
209 208
         ) : null}
210 209
         <WebViewScreen
211 210
           url={Urls.planex.planning}
212
-          initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
213
-          injectJS={this.state.injectJS}
214
-          onMessage={this.onMessage}
215
-          onScroll={this.onScroll}
211
+          initialJS={generateInjectedJS(currentGroup)}
212
+          injectJS={injectJS}
213
+          onMessage={onMessage}
214
+          onScroll={onScroll}
216 215
           showAdvancedControls={false}
217 216
         />
218 217
       </View>
219 218
     );
220
-  }
219
+  };
221 220
 
222 221
   /**
223 222
    * Callback used when the user clicks on the navigate to settings button.
224 223
    * This will hide the banner and open the SettingsScreen
225 224
    */
226
-  onGoToSettings = () => {
227
-    const { navigation } = this.props;
228
-    navigation.navigate('settings');
229
-  };
230
-
231
-  onScreenFocus = () => {
232
-    this.handleNavigationParams();
233
-  };
225
+  const onGoToSettings = () => navigation.navigate('settings');
234 226
 
235 227
   /**
236 228
    * Sends a FullCalendar action to the web page inside the webview.
@@ -239,7 +231,7 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
239 231
    * Or "setGroup" with the group id as data to set the selected group
240 232
    * @param data Data to pass to the action
241 233
    */
242
-  sendMessage = (action: string, data?: string) => {
234
+  const sendMessage = (action: string, data?: string) => {
243 235
     let command;
244 236
     if (action === 'setGroup') {
245 237
       command = `displayAde(${data})`;
@@ -249,10 +241,10 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
249 241
     // String must resolve to true to prevent crash on iOS
250 242
     command += ';true;';
251 243
     // Change the injected
252
-    if (command === this.state.injectJS) {
244
+    if (command === injectJS) {
253 245
       command += ';true;';
254 246
     }
255
-    this.setState({ injectJS: command });
247
+    setInjectJS(command);
256 248
   };
257 249
 
258 250
   /**
@@ -260,7 +252,7 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
260 252
    *
261 253
    * @param event
262 254
    */
263
-  onMessage = (event: { nativeEvent: { data: string } }) => {
255
+  const onMessage = (event: { nativeEvent: { data: string } }) => {
264 256
     const data: {
265 257
       start: string;
266 258
       end: string;
@@ -276,7 +268,7 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
276 268
     if (startString != null && endString != null) {
277 269
       msg += `${startString} - ${endString}`;
278 270
     }
279
-    this.showDialog(data.title, msg);
271
+    showDialog(data.title, msg);
280 272
   };
281 273
 
282 274
   /**
@@ -285,10 +277,9 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
285 277
    * @param title The dialog's title
286 278
    * @param message The message to show
287 279
    */
288
-  showDialog = (title: string, message: string) => {
289
-    this.setState({
290
-      dialogVisible: true,
291
-      dialogTitle: (
280
+  const showDialog = (title: string, message: string) => {
281
+    setDialogContent({
282
+      title: (
292 283
         <Autolink
293 284
           text={title}
294 285
           hashtag={'facebook'}
@@ -299,44 +290,20 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
299 290
           phone={true}
300 291
         />
301 292
       ),
302
-      dialogMessage: message,
293
+      message: message,
303 294
     });
304 295
   };
305 296
 
306
-  /**
307
-   * Hides the dialog
308
-   */
309
-  hideDialog = () => {
310
-    this.setState({
311
-      dialogVisible: false,
312
-    });
313
-  };
297
+  const hideDialog = () => setDialogContent(undefined);
314 298
 
315 299
   /**
316 300
    * Binds the onScroll event to the control bar for automatic hiding based on scroll direction and speed
317 301
    *
318 302
    * @param event
319 303
    */
320
-  onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
321
-    if (this.barRef.current != null) {
322
-      this.barRef.current.onScroll(event);
323
-    }
324
-  };
325
-
326
-  /**
327
-   * If navigations parameters contain a group, set it as selected
328
-   */
329
-  handleNavigationParams = () => {
330
-    const { props } = this;
331
-    if (props.route.params != null) {
332
-      if (
333
-        props.route.params.group !== undefined &&
334
-        props.route.params.group !== null
335
-      ) {
336
-        // reset params to prevent infinite loop
337
-        this.selectNewGroup(props.route.params.group);
338
-        props.navigation.dispatch(CommonActions.setParams({ group: null }));
339
-      }
304
+  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
305
+    if (barRef.current) {
306
+      barRef.current.onScroll(event);
340 307
     }
341 308
   };
342 309
 
@@ -345,89 +312,86 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
345 312
    *
346 313
    * @param group The group object selected
347 314
    */
348
-  selectNewGroup(group: PlanexGroupType) {
349
-    const { navigation } = this.props;
350
-    this.sendMessage('setGroup', group.id.toString());
351
-    this.setState({ currentGroup: group });
315
+  const selectNewGroup = (group: PlanexGroupType) => {
316
+    sendMessage('setGroup', group.id.toString());
317
+    setCurrentGroup(group);
352 318
     AsyncStorageManager.set(
353 319
       AsyncStorageManager.PREFERENCES.planexCurrentGroup.key,
354 320
       group
355 321
     );
356 322
     navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) });
357
-  }
323
+  };
358 324
 
359 325
   /**
360 326
    * Generates custom JavaScript to be injected into the webpage
361 327
    *
362 328
    * @param groupID The current group selected
363 329
    */
364
-  generateInjectedJS(groupID: number) {
330
+  const generateInjectedJS = (group: PlanexGroupType | undefined) => {
365 331
     let customInjectedJS = `$(document).ready(function() {
366 332
       ${OBSERVE_MUTATIONS_INJECTED}
367
-      ${FULL_CALENDAR_SETTINGS}
368
-      displayAde(${groupID});
369
-      ${INJECT_STYLE}`;
333
+      ${INJECT_STYLE}
334
+      ${FULL_CALENDAR_SETTINGS}`;
335
+    if (group) {
336
+      customInjectedJS += `displayAde(${group.id});`;
337
+    }
370 338
     if (DateManager.isWeekend(new Date())) {
371 339
       customInjectedJS += `calendar.next();`;
372 340
     }
373 341
     if (ThemeManager.getNightMode()) {
374
-      customInjectedJS += `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
342
+      customInjectedJS += INJECT_STYLE_DARK;
375 343
     }
376
-
377 344
     customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
378 345
     return customInjectedJS;
379
-  }
346
+  };
380 347
 
381
-  render() {
382
-    const { props, state } = this;
383
-    return (
384
-      <View style={GENERAL_STYLES.flex}>
385
-        {/* Allow to draw webview bellow banner */}
386
-        <View style={styles.container}>
387
-          {props.theme.dark ? ( // Force component theme update by recreating it on theme change
388
-            this.getWebView()
389
-          ) : (
390
-            <View style={GENERAL_STYLES.flex}>{this.getWebView()}</View>
391
-          )}
392
-        </View>
393
-        {AsyncStorageManager.getString(
394
-          AsyncStorageManager.PREFERENCES.defaultStartScreen.key
395
-        ).toLowerCase() !== 'planex' ? (
396
-          <MascotPopup
397
-            prefKey={AsyncStorageManager.PREFERENCES.planexShowMascot.key}
398
-            title={i18n.t('screens.planex.mascotDialog.title')}
399
-            message={i18n.t('screens.planex.mascotDialog.message')}
400
-            icon="emoticon-kiss"
401
-            buttons={{
402
-              action: {
403
-                message: i18n.t('screens.planex.mascotDialog.ok'),
404
-                icon: 'cog',
405
-                onPress: this.onGoToSettings,
406
-              },
407
-              cancel: {
408
-                message: i18n.t('screens.planex.mascotDialog.cancel'),
409
-                icon: 'close',
410
-                color: props.theme.colors.warning,
411
-              },
412
-            }}
413
-            emotion={MASCOT_STYLE.INTELLO}
414
-          />
415
-        ) : null}
416
-        <AlertDialog
417
-          visible={state.dialogVisible}
418
-          onDismiss={this.hideDialog}
419
-          title={state.dialogTitle}
420
-          message={state.dialogMessage}
421
-        />
422
-        <AnimatedBottomBar
423
-          navigation={props.navigation}
424
-          ref={this.barRef}
425
-          onPress={this.sendMessage}
426
-          seekAttention={state.currentGroup.id === -1}
427
-        />
348
+  return (
349
+    <View style={GENERAL_STYLES.flex}>
350
+      {/* Allow to draw webview bellow banner */}
351
+      <View style={styles.container}>
352
+        {theme.dark ? ( // Force component theme update by recreating it on theme change
353
+          getWebView()
354
+        ) : (
355
+          <View style={GENERAL_STYLES.flex}>{getWebView()}</View>
356
+        )}
428 357
       </View>
429
-    );
430
-  }
358
+      {AsyncStorageManager.getString(
359
+        AsyncStorageManager.PREFERENCES.defaultStartScreen.key
360
+      ).toLowerCase() !== 'planex' ? (
361
+        <MascotPopup
362
+          prefKey={AsyncStorageManager.PREFERENCES.planexShowMascot.key}
363
+          title={i18n.t('screens.planex.mascotDialog.title')}
364
+          message={i18n.t('screens.planex.mascotDialog.message')}
365
+          icon="emoticon-kiss"
366
+          buttons={{
367
+            action: {
368
+              message: i18n.t('screens.planex.mascotDialog.ok'),
369
+              icon: 'cog',
370
+              onPress: onGoToSettings,
371
+            },
372
+            cancel: {
373
+              message: i18n.t('screens.planex.mascotDialog.cancel'),
374
+              icon: 'close',
375
+              color: theme.colors.warning,
376
+            },
377
+          }}
378
+          emotion={MASCOT_STYLE.INTELLO}
379
+        />
380
+      ) : null}
381
+      <AlertDialog
382
+        visible={dialogContent !== undefined}
383
+        onDismiss={hideDialog}
384
+        title={dialogContent ? dialogContent.title : ''}
385
+        message={dialogContent ? dialogContent.message : ''}
386
+      />
387
+      <AnimatedBottomBar
388
+        navigation={navigation}
389
+        ref={barRef}
390
+        onPress={sendMessage}
391
+        seekAttention={currentGroup !== undefined}
392
+      />
393
+    </View>
394
+  );
431 395
 }
432 396
 
433
-export default withTheme(PlanexScreen);
397
+export default PlanexScreen;

Loading…
Cancel
Save