Browse Source

update react native collapsible

Arnaud Vergnet 6 months ago
parent
commit
286c1e6411

+ 24
- 20
App.tsx View File

@@ -37,6 +37,7 @@ import { setupStatusBar } from './src/utils/Utils';
37 37
 import initLocales from './src/utils/Locales';
38 38
 import { NavigationContainerRef } from '@react-navigation/core';
39 39
 import GENERAL_STYLES from './src/constants/Styles';
40
+import CollapsibleProvider from './src/components/providers/CollapsibleProvider';
40 41
 
41 42
 // Native optimizations https://reactnavigation.org/docs/react-native-screens
42 43
 // Crashes app when navigating away from webview on android 9+
@@ -210,26 +211,29 @@ export default class App extends React.Component<{}, StateType> {
210 211
     }
211 212
     return (
212 213
       <PaperProvider theme={state.currentTheme}>
213
-        <OverflowMenuProvider>
214
-          <View
215
-            style={{
216
-              backgroundColor: ThemeManager.getCurrentTheme().colors.background,
217
-              ...GENERAL_STYLES.flex,
218
-            }}
219
-          >
220
-            <SafeAreaView style={GENERAL_STYLES.flex}>
221
-              <NavigationContainer
222
-                theme={state.currentTheme}
223
-                ref={this.navigatorRef}
224
-              >
225
-                <MainNavigator
226
-                  defaultHomeRoute={this.defaultHomeRoute}
227
-                  defaultHomeData={this.defaultHomeData}
228
-                />
229
-              </NavigationContainer>
230
-            </SafeAreaView>
231
-          </View>
232
-        </OverflowMenuProvider>
214
+        <CollapsibleProvider>
215
+          <OverflowMenuProvider>
216
+            <View
217
+              style={{
218
+                backgroundColor: ThemeManager.getCurrentTheme().colors
219
+                  .background,
220
+                ...GENERAL_STYLES.flex,
221
+              }}
222
+            >
223
+              <SafeAreaView style={GENERAL_STYLES.flex}>
224
+                <NavigationContainer
225
+                  theme={state.currentTheme}
226
+                  ref={this.navigatorRef}
227
+                >
228
+                  <MainNavigator
229
+                    defaultHomeRoute={this.defaultHomeRoute}
230
+                    defaultHomeData={this.defaultHomeData}
231
+                  />
232
+                </NavigationContainer>
233
+              </SafeAreaView>
234
+            </View>
235
+          </OverflowMenuProvider>
236
+        </CollapsibleProvider>
233 237
       </PaperProvider>
234 238
     );
235 239
   }

+ 38
- 8
package-lock.json View File

@@ -3931,7 +3931,6 @@
3931 3931
       "version": "3.1.4",
3932 3932
       "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
3933 3933
       "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
3934
-      "dev": true,
3935 3934
       "requires": {
3936 3935
         "node-fetch": "2.6.1"
3937 3936
       }
@@ -10596,14 +10595,39 @@
10596 10595
       "integrity": "sha512-beZjdgbT9Y/Pg591Xy5XkKG20HffJiVad4n9bfcUF/f783A+tvOVXnqvbS58Lkaym93mi4jcDPMuW9Vc1t6rqg=="
10597 10596
     },
10598 10597
     "react-native-gesture-handler": {
10599
-      "version": "1.8.0",
10600
-      "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.8.0.tgz",
10601
-      "integrity": "sha512-E2FZa0qZ5Bi0Z8Jg4n9DaFomHvedSjwbO2DPmUUHYRy1lH2yxXUpSrqJd6yymu+Efzmjg2+JZzsjFYA2Iq8VEQ==",
10598
+      "version": "1.10.3",
10599
+      "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
10600
+      "integrity": "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw==",
10602 10601
       "requires": {
10603 10602
         "@egjs/hammerjs": "^2.0.17",
10603
+        "fbjs": "^3.0.0",
10604 10604
         "hoist-non-react-statics": "^3.3.0",
10605 10605
         "invariant": "^2.2.4",
10606 10606
         "prop-types": "^15.7.2"
10607
+      },
10608
+      "dependencies": {
10609
+        "fbjs": {
10610
+          "version": "3.0.0",
10611
+          "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.0.tgz",
10612
+          "integrity": "sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg==",
10613
+          "requires": {
10614
+            "cross-fetch": "^3.0.4",
10615
+            "fbjs-css-vars": "^1.0.0",
10616
+            "loose-envify": "^1.0.0",
10617
+            "object-assign": "^4.1.0",
10618
+            "promise": "^7.1.1",
10619
+            "setimmediate": "^1.0.5",
10620
+            "ua-parser-js": "^0.7.18"
10621
+          }
10622
+        },
10623
+        "promise": {
10624
+          "version": "7.3.1",
10625
+          "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
10626
+          "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
10627
+          "requires": {
10628
+            "asap": "~2.0.3"
10629
+          }
10630
+        }
10607 10631
       }
10608 10632
     },
10609 10633
     "react-native-image-pan-zoom": {
@@ -10871,11 +10895,12 @@
10871 10895
       }
10872 10896
     },
10873 10897
     "react-navigation-collapsible": {
10874
-      "version": "5.6.4",
10875
-      "resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.6.4.tgz",
10876
-      "integrity": "sha512-dXMbDw2TQ6s5XLk9h+2hUShXoS8KPChfdh/xmmLqfKmntS5YteE01+x78gU5KogB3etDraH1kvhW7xDnbG9AfA==",
10898
+      "version": "5.9.1",
10899
+      "resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.9.1.tgz",
10900
+      "integrity": "sha512-yUwHe8Z7++A8ThrjPI+Mcm7LqBhIqJc+1F4XszpI7EoHz3bJElzczbfyfuEvjSbYU9AgW3MdBWzaRIDluxcEuA==",
10877 10901
       "requires": {
10878
-        "react-native-iphone-x-helper": "^1.2.1"
10902
+        "react-native-iphone-x-helper": "^1.3.0",
10903
+        "shallowequal": "^1.1.0"
10879 10904
       }
10880 10905
     },
10881 10906
     "react-navigation-header-buttons": {
@@ -11453,6 +11478,11 @@
11453 11478
         "kind-of": "^6.0.2"
11454 11479
       }
11455 11480
     },
11481
+    "shallowequal": {
11482
+      "version": "1.1.0",
11483
+      "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
11484
+      "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
11485
+    },
11456 11486
     "shebang-command": {
11457 11487
       "version": "1.2.0",
11458 11488
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",

+ 1
- 1
package.json View File

@@ -53,7 +53,7 @@
53 53
     "react-native-splash-screen": "3.2.0",
54 54
     "react-native-vector-icons": "8.1.0",
55 55
     "react-native-webview": "11.4.3",
56
-    "react-navigation-collapsible": "5.6.4",
56
+    "react-navigation-collapsible": "5.9.1",
57 57
     "react-navigation-header-buttons": "7.0.1"
58 58
   },
59 59
   "devDependencies": {

+ 2
- 2
src/components/Animations/AnimatedBottomBar.tsx View File

@@ -28,7 +28,7 @@ import { FAB, IconButton, Surface, withTheme } from 'react-native-paper';
28 28
 import * as Animatable from 'react-native-animatable';
29 29
 import { StackNavigationProp } from '@react-navigation/stack';
30 30
 import AutoHideHandler from '../../utils/AutoHideHandler';
31
-import CustomTabBar from '../Tabbar/CustomTabBar';
31
+import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
32 32
 
33 33
 const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
34 34
 
@@ -159,7 +159,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
159 159
         useNativeDriver
160 160
         style={{
161 161
           ...styles.container,
162
-          bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT,
162
+          bottom: 10 + TAB_BAR_HEIGHT,
163 163
         }}
164 164
       >
165 165
         <Surface style={styles.surface}>

+ 2
- 2
src/components/Animations/AnimatedFAB.tsx View File

@@ -27,7 +27,7 @@ import {
27 27
 import { FAB } from 'react-native-paper';
28 28
 import * as Animatable from 'react-native-animatable';
29 29
 import AutoHideHandler from '../../utils/AutoHideHandler';
30
-import CustomTabBar from '../Tabbar/CustomTabBar';
30
+import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
31 31
 
32 32
 type PropsType = {
33 33
   icon: string;
@@ -82,7 +82,7 @@ export default class AnimatedFAB extends React.Component<PropsType> {
82 82
         useNativeDriver={true}
83 83
         style={{
84 84
           ...styles.fab,
85
-          bottom: CustomTabBar.TAB_BAR_HEIGHT,
85
+          bottom: TAB_BAR_HEIGHT,
86 86
         }}
87 87
       >
88 88
         <FAB icon={props.icon} onPress={props.onPress} />

+ 41
- 12
src/components/Collapsible/CollapsibleComponent.tsx View File

@@ -17,22 +17,27 @@
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 { useCollapsibleStack } from 'react-navigation-collapsible';
22
-import CustomTabBar from '../Tabbar/CustomTabBar';
20
+import React, { useCallback } from 'react';
21
+import { useCollapsibleHeader } from 'react-navigation-collapsible';
22
+import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
23 23
 import {
24 24
   NativeScrollEvent,
25 25
   NativeSyntheticEvent,
26 26
   StyleSheet,
27 27
 } from 'react-native';
28
+import { useTheme } from 'react-native-paper';
29
+import { useCollapsible } from '../../utils/CollapsibleContext';
30
+import { useFocusEffect } from '@react-navigation/core';
28 31
 
29 32
 export type CollapsibleComponentPropsType = {
30 33
   children?: React.ReactNode;
31 34
   hasTab?: boolean;
32 35
   onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
36
+  paddedProps?: (paddingTop: number) => Record<string, any>;
37
+  headerColors: string;
33 38
 };
34 39
 
35
-type PropsType = CollapsibleComponentPropsType & {
40
+type Props = CollapsibleComponentPropsType & {
36 41
   component: React.ComponentType<any>;
37 42
 };
38 43
 
@@ -42,22 +47,46 @@ const styles = StyleSheet.create({
42 47
   },
43 48
 });
44 49
 
45
-function CollapsibleComponent(props: PropsType) {
46
-  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
47
-    if (props.onScroll) {
48
-      props.onScroll(event);
49
-    }
50
-  };
50
+function CollapsibleComponent(props: Props) {
51
+  const { paddedProps, headerColors } = props;
51 52
   const Comp = props.component;
53
+  const theme = useTheme();
54
+  const { setCollapsible } = useCollapsible();
55
+
56
+  const collapsible = useCollapsibleHeader({
57
+    config: {
58
+      collapsedColor: headerColors ? headerColors : theme.colors.surface,
59
+      useNativeDriver: true,
60
+    },
61
+  });
62
+
63
+  useFocusEffect(
64
+    useCallback(() => {
65
+      setCollapsible(collapsible);
66
+    }, [collapsible, setCollapsible])
67
+  );
68
+
52 69
   const {
53 70
     containerPaddingTop,
54 71
     scrollIndicatorInsetTop,
55 72
     onScrollWithListener,
56
-  } = useCollapsibleStack();
57
-  const paddingBottom = props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0;
73
+  } = collapsible;
74
+
75
+  const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
76
+
77
+  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
78
+    if (props.onScroll) {
79
+      props.onScroll(event);
80
+    }
81
+  };
82
+
83
+  const pprops =
84
+    paddedProps !== undefined ? paddedProps(containerPaddingTop) : undefined;
85
+
58 86
   return (
59 87
     <Comp
60 88
       {...props}
89
+      {...pprops}
61 90
       onScroll={onScrollWithListener(onScroll)}
62 91
       contentContainerStyle={{
63 92
         paddingTop: containerPaddingTop,

+ 2
- 2
src/components/Overrides/CustomModal.tsx View File

@@ -21,7 +21,7 @@ import * as React from 'react';
21 21
 import { useTheme } from 'react-native-paper';
22 22
 import { Modalize } from 'react-native-modalize';
23 23
 import { View } from 'react-native-animatable';
24
-import CustomTabBar from '../Tabbar/CustomTabBar';
24
+import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
25 25
 
26 26
 /**
27 27
  * Abstraction layer for Modalize component, using custom configuration
@@ -45,7 +45,7 @@ function CustomModal(props: {
45 45
     >
46 46
       <View
47 47
         style={{
48
-          paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
48
+          paddingBottom: TAB_BAR_HEIGHT,
49 49
         }}
50 50
       >
51 51
         {children}

+ 15
- 14
src/components/Screens/WebSectionList.tsx View File

@@ -32,10 +32,10 @@ import { Collapsible } from 'react-navigation-collapsible';
32 32
 import { StackNavigationProp } from '@react-navigation/stack';
33 33
 import ErrorView from './ErrorView';
34 34
 import BasicLoadingScreen from './BasicLoadingScreen';
35
-import withCollapsible from '../../utils/withCollapsible';
36
-import CustomTabBar from '../Tabbar/CustomTabBar';
35
+import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
37 36
 import { ERROR_TYPE, readData } from '../../utils/WebData';
38 37
 import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
38
+import GENERAL_STYLES from '../../constants/Styles';
39 39
 
40 40
 export type SectionListDataType<ItemT> = Array<{
41 41
   title: string;
@@ -260,19 +260,20 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
260 260
       dataset = props.createDataset(state.fetchedData, state.refreshing);
261 261
     }
262 262
 
263
-    const { containerPaddingTop } = props.collapsibleStack;
264 263
     return (
265
-      <View>
264
+      <View style={GENERAL_STYLES.flex}>
266 265
         <CollapsibleSectionList
267 266
           sections={dataset}
268 267
           extraData={props.updateData}
269
-          refreshControl={
270
-            <RefreshControl
271
-              progressViewOffset={containerPaddingTop}
272
-              refreshing={state.refreshing}
273
-              onRefresh={this.onRefresh}
274
-            />
275
-          }
268
+          paddedProps={(paddingTop) => ({
269
+            refreshControl: (
270
+              <RefreshControl
271
+                progressViewOffset={paddingTop}
272
+                refreshing={state.refreshing}
273
+                onRefresh={this.onRefresh}
274
+              />
275
+            ),
276
+          })}
276 277
           renderSectionHeader={this.getRenderSectionHeader}
277 278
           renderItem={this.getRenderItem}
278 279
           stickySectionHeadersEnabled={props.stickyHeader}
@@ -299,7 +300,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
299 300
               : undefined
300 301
           }
301 302
           onScroll={this.onScroll}
302
-          hasTab
303
+          hasTab={true}
303 304
         />
304 305
         <Snackbar
305 306
           visible={state.snackbarVisible}
@@ -310,7 +311,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
310 311
           }}
311 312
           duration={4000}
312 313
           style={{
313
-            bottom: CustomTabBar.TAB_BAR_HEIGHT,
314
+            bottom: TAB_BAR_HEIGHT,
314 315
           }}
315 316
         >
316 317
           {i18n.t('general.listUpdateFail')}
@@ -320,4 +321,4 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
320 321
   }
321 322
 }
322 323
 
323
-export default withCollapsible(WebSectionList);
324
+export default WebSectionList;

+ 124
- 154
src/components/Screens/WebViewScreen.tsx View File

@@ -17,7 +17,13 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
20
+import React, {
21
+  useCallback,
22
+  useEffect,
23
+  useLayoutEffect,
24
+  useRef,
25
+  useState,
26
+} from 'react';
21 27
 import WebView from 'react-native-webview';
22 28
 import {
23 29
   Divider,
@@ -34,23 +40,21 @@ import {
34 40
   StyleSheet,
35 41
 } from 'react-native';
36 42
 import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
37
-import { withTheme } from 'react-native-paper';
38
-import { StackNavigationProp } from '@react-navigation/stack';
39
-import { Collapsible } from 'react-navigation-collapsible';
40
-import withCollapsible from '../../utils/withCollapsible';
43
+import { useTheme } from 'react-native-paper';
44
+import { useCollapsibleHeader } from 'react-navigation-collapsible';
41 45
 import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton';
42 46
 import { ERROR_TYPE } from '../../utils/WebData';
43 47
 import ErrorView from './ErrorView';
44 48
 import BasicLoadingScreen from './BasicLoadingScreen';
49
+import { useFocusEffect, useNavigation } from '@react-navigation/core';
50
+import { useCollapsible } from '../../utils/CollapsibleContext';
45 51
 
46
-type PropsType = {
47
-  navigation: StackNavigationProp<any>;
48
-  theme: ReactNativePaper.Theme;
52
+type Props = {
49 53
   url: string;
50
-  collapsibleStack: Collapsible;
51
-  onMessage: (event: { nativeEvent: { data: string } }) => void;
52
-  onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
53
-  customJS?: string;
54
+  onMessage?: (event: { nativeEvent: { data: string } }) => void;
55
+  onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
56
+  initialJS?: string;
57
+  injectJS?: string;
54 58
   customPaddingFunction?: null | ((padding: number) => string);
55 59
   showAdvancedControls?: boolean;
56 60
 };
@@ -66,134 +70,113 @@ const styles = StyleSheet.create({
66 70
 /**
67 71
  * Class defining a webview screen.
68 72
  */
69
-class WebViewScreen extends React.PureComponent<PropsType> {
70
-  static defaultProps = {
71
-    customJS: '',
72
-    showAdvancedControls: true,
73
-    customPaddingFunction: null,
74
-  };
75
-
76
-  currentUrl: string;
77
-
78
-  webviewRef: { current: null | WebView };
73
+function WebViewScreen(props: Props) {
74
+  const [currentUrl, setCurrentUrl] = useState(props.url);
75
+  const [canGoBack, setCanGoBack] = useState(false);
76
+  const navigation = useNavigation();
77
+  const theme = useTheme();
78
+  const webviewRef = useRef<WebView>();
79 79
 
80
-  canGoBack: boolean;
80
+  const { setCollapsible } = useCollapsible();
81
+  const collapsible = useCollapsibleHeader({
82
+    config: { collapsedColor: theme.colors.surface, useNativeDriver: false },
83
+  });
84
+  const { containerPaddingTop, onScrollWithListener } = collapsible;
81 85
 
82
-  constructor(props: PropsType) {
83
-    super(props);
84
-    this.webviewRef = React.createRef();
85
-    this.canGoBack = false;
86
-    this.currentUrl = props.url;
87
-  }
86
+  const [currentInjectedJS, setCurrentInjectedJS] = useState(props.injectJS);
88 87
 
89
-  /**
90
-   * Creates header buttons and listens to events after mounting
91
-   */
92
-  componentDidMount() {
93
-    const { props } = this;
94
-    props.navigation.setOptions({
95
-      headerRight: props.showAdvancedControls
96
-        ? this.getAdvancedButtons
97
-        : this.getBasicButton,
98
-    });
99
-    props.navigation.addListener('focus', () => {
88
+  useFocusEffect(
89
+    useCallback(() => {
90
+      setCollapsible(collapsible);
100 91
       BackHandler.addEventListener(
101 92
         'hardwareBackPress',
102
-        this.onBackButtonPressAndroid
103
-      );
104
-    });
105
-    props.navigation.addListener('blur', () => {
106
-      BackHandler.removeEventListener(
107
-        'hardwareBackPress',
108
-        this.onBackButtonPressAndroid
93
+        onBackButtonPressAndroid
109 94
       );
95
+      return () => {
96
+        BackHandler.removeEventListener(
97
+          'hardwareBackPress',
98
+          onBackButtonPressAndroid
99
+        );
100
+      };
101
+      // eslint-disable-next-line react-hooks/exhaustive-deps
102
+    }, [collapsible, setCollapsible])
103
+  );
104
+
105
+  useLayoutEffect(() => {
106
+    navigation.setOptions({
107
+      headerRight: props.showAdvancedControls
108
+        ? getAdvancedButtons
109
+        : getBasicButton,
110 110
     });
111
-  }
111
+    // eslint-disable-next-line react-hooks/exhaustive-deps
112
+  }, [navigation, props.showAdvancedControls]);
112 113
 
113
-  /**
114
-   * Goes back on the webview or on the navigation stack if we cannot go back anymore
115
-   *
116
-   * @returns {boolean}
117
-   */
118
-  onBackButtonPressAndroid = (): boolean => {
119
-    if (this.canGoBack) {
120
-      this.onGoBackClicked();
114
+  useEffect(() => {
115
+    if (props.injectJS && props.injectJS !== currentInjectedJS) {
116
+      injectJavaScript(props.injectJS);
117
+      setCurrentInjectedJS(props.injectJS);
118
+    }
119
+    // eslint-disable-next-line react-hooks/exhaustive-deps
120
+  }, [props.injectJS]);
121
+
122
+  const onBackButtonPressAndroid = () => {
123
+    if (canGoBack) {
124
+      onGoBackClicked();
121 125
       return true;
122 126
     }
123 127
     return false;
124 128
   };
125 129
 
126
-  /**
127
-   * Gets header refresh and open in browser buttons
128
-   *
129
-   * @return {*}
130
-   */
131
-  getBasicButton = () => {
130
+  const getBasicButton = () => {
132 131
     return (
133 132
       <MaterialHeaderButtons>
134 133
         <Item
135
-          title="refresh"
136
-          iconName="refresh"
137
-          onPress={this.onRefreshClicked}
134
+          title={'refresh'}
135
+          iconName={'refresh'}
136
+          onPress={onRefreshClicked}
138 137
         />
139 138
         <Item
140 139
           title={i18n.t('general.openInBrowser')}
141
-          iconName="open-in-new"
142
-          onPress={this.onOpenClicked}
140
+          iconName={'open-in-new'}
141
+          onPress={onOpenClicked}
143 142
         />
144 143
       </MaterialHeaderButtons>
145 144
     );
146 145
   };
147 146
 
148
-  /**
149
-   * Creates advanced header control buttons.
150
-   * These buttons allows the user to refresh, go back, go forward and open in the browser.
151
-   *
152
-   * @returns {*}
153
-   */
154
-  getAdvancedButtons = () => {
155
-    const { props } = this;
147
+  const getAdvancedButtons = () => {
156 148
     return (
157 149
       <MaterialHeaderButtons>
158
-        <Item
159
-          title="refresh"
160
-          iconName="refresh"
161
-          onPress={this.onRefreshClicked}
162
-        />
150
+        <Item title="refresh" iconName="refresh" onPress={onRefreshClicked} />
163 151
         <OverflowMenu
164 152
           style={styles.overflow}
165 153
           OverflowIcon={
166 154
             <MaterialCommunityIcons
167 155
               name="dots-vertical"
168 156
               size={26}
169
-              color={props.theme.colors.text}
157
+              color={theme.colors.text}
170 158
             />
171 159
           }
172 160
         >
173 161
           <HiddenItem
174 162
             title={i18n.t('general.goBack')}
175
-            onPress={this.onGoBackClicked}
163
+            onPress={onGoBackClicked}
176 164
           />
177 165
           <HiddenItem
178 166
             title={i18n.t('general.goForward')}
179
-            onPress={this.onGoForwardClicked}
167
+            onPress={onGoForwardClicked}
180 168
           />
181 169
           <Divider />
182 170
           <HiddenItem
183 171
             title={i18n.t('general.openInBrowser')}
184
-            onPress={this.onOpenClicked}
172
+            onPress={onOpenClicked}
185 173
           />
186 174
         </OverflowMenu>
187 175
       </MaterialHeaderButtons>
188 176
     );
189 177
   };
190 178
 
191
-  /**
192
-   * Gets the loading indicator
193
-   *
194
-   * @return {*}
195
-   */
196
-  getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
179
+  const getRenderLoading = () => <BasicLoadingScreen isAbsolute={true} />;
197 180
 
198 181
   /**
199 182
    * Gets the javascript needed to generate a padding on top of the page
@@ -202,91 +185,78 @@ class WebViewScreen extends React.PureComponent<PropsType> {
202 185
    * @param padding The padding to add in pixels
203 186
    * @returns {string}
204 187
    */
205
-  getJavascriptPadding(padding: number): string {
206
-    const { props } = this;
188
+  const getJavascriptPadding = (padding: number) => {
207 189
     const customPadding =
208 190
       props.customPaddingFunction != null
209 191
         ? props.customPaddingFunction(padding)
210 192
         : '';
211 193
     return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
212
-  }
194
+  };
213 195
 
214
-  /**
215
-   * Callback to use when refresh button is clicked. Reloads the webview.
216
-   */
217
-  onRefreshClicked = () => {
218
-    if (this.webviewRef.current != null) {
219
-      this.webviewRef.current.reload();
196
+  const onRefreshClicked = () => {
197
+    //@ts-ignore
198
+    if (webviewRef.current) {
199
+      //@ts-ignore
200
+      webviewRef.current.reload();
220 201
     }
221 202
   };
222 203
 
223
-  onGoBackClicked = () => {
224
-    if (this.webviewRef.current != null) {
225
-      this.webviewRef.current.goBack();
204
+  const onGoBackClicked = () => {
205
+    //@ts-ignore
206
+    if (webviewRef.current) {
207
+      //@ts-ignore
208
+      webviewRef.current.goBack();
226 209
     }
227 210
   };
228 211
 
229
-  onGoForwardClicked = () => {
230
-    if (this.webviewRef.current != null) {
231
-      this.webviewRef.current.goForward();
212
+  const onGoForwardClicked = () => {
213
+    //@ts-ignore
214
+    if (webviewRef.current) {
215
+      //@ts-ignore
216
+      webviewRef.current.goForward();
232 217
     }
233 218
   };
234 219
 
235
-  onOpenClicked = () => {
236
-    Linking.openURL(this.currentUrl);
237
-  };
220
+  const onOpenClicked = () => Linking.openURL(currentUrl);
238 221
 
239
-  onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
240
-    const { onScroll } = this.props;
241
-    if (onScroll) {
242
-      onScroll(event);
222
+  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
223
+    if (props.onScroll) {
224
+      props.onScroll(event);
243 225
     }
244 226
   };
245 227
 
246
-  /**
247
-   * Injects the given javascript string into the web page
248
-   *
249
-   * @param script The script to inject
250
-   */
251
-  injectJavaScript = (script: string) => {
252
-    if (this.webviewRef.current != null) {
253
-      this.webviewRef.current.injectJavaScript(script);
228
+  const injectJavaScript = (script: string) => {
229
+    //@ts-ignore
230
+    if (webviewRef.current) {
231
+      //@ts-ignore
232
+      webviewRef.current.injectJavaScript(script);
254 233
     }
255 234
   };
256 235
 
257
-  render() {
258
-    const { props } = this;
259
-    const {
260
-      containerPaddingTop,
261
-      onScrollWithListener,
262
-    } = props.collapsibleStack;
263
-    return (
264
-      <AnimatedWebView
265
-        ref={this.webviewRef}
266
-        source={{ uri: props.url }}
267
-        startInLoadingState
268
-        injectedJavaScript={props.customJS}
269
-        javaScriptEnabled
270
-        renderLoading={this.getRenderLoading}
271
-        renderError={() => (
272
-          <ErrorView
273
-            errorCode={ERROR_TYPE.CONNECTION_ERROR}
274
-            onRefresh={this.onRefreshClicked}
275
-          />
276
-        )}
277
-        onNavigationStateChange={(navState) => {
278
-          this.currentUrl = navState.url;
279
-          this.canGoBack = navState.canGoBack;
280
-        }}
281
-        onMessage={props.onMessage}
282
-        onLoad={() => {
283
-          this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
284
-        }}
285
-        // Animations
286
-        onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
287
-      />
288
-    );
289
-  }
236
+  return (
237
+    <AnimatedWebView
238
+      ref={webviewRef}
239
+      source={{ uri: props.url }}
240
+      startInLoadingState={true}
241
+      injectedJavaScript={props.initialJS}
242
+      javaScriptEnabled={true}
243
+      renderLoading={getRenderLoading}
244
+      renderError={() => (
245
+        <ErrorView
246
+          errorCode={ERROR_TYPE.CONNECTION_ERROR}
247
+          onRefresh={onRefreshClicked}
248
+        />
249
+      )}
250
+      onNavigationStateChange={(navState) => {
251
+        setCurrentUrl(navState.url);
252
+        setCanGoBack(navState.canGoBack);
253
+      }}
254
+      onMessage={props.onMessage}
255
+      onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
256
+      // Animations
257
+      onScroll={onScrollWithListener(onScroll)}
258
+    />
259
+  );
290 260
 }
291 261
 
292
-export default withCollapsible(withTheme(WebViewScreen));
262
+export default WebViewScreen;

+ 69
- 192
src/components/Tabbar/CustomTabBar.tsx View File

@@ -17,211 +17,88 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
20
+import React from 'react';
21
+import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
21 22
 import { Animated, StyleSheet } from 'react-native';
22
-import { withTheme } from 'react-native-paper';
23
-import { Collapsible } from 'react-navigation-collapsible';
24 23
 import TabIcon from './TabIcon';
25
-import TabHomeIcon from './TabHomeIcon';
26
-import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
27
-import { NavigationState } from '@react-navigation/native';
28
-import {
29
-  PartialState,
30
-  Route,
31
-} from '@react-navigation/routers/lib/typescript/src/types';
32
-
33
-type RouteType = Route<string> & {
34
-  state?: NavigationState | PartialState<NavigationState>;
35
-};
24
+import { useTheme } from 'react-native-paper';
25
+import { useCollapsible } from '../../utils/CollapsibleContext';
26
+
27
+export const TAB_BAR_HEIGHT = 50;
28
+
29
+function CustomTabBar(
30
+  props: BottomTabBarProps<any> & {
31
+    icons: {
32
+      [key: string]: {
33
+        normal: string;
34
+        focused: string;
35
+      };
36
+    };
37
+    labels: {
38
+      [key: string]: string;
39
+    };
40
+  }
41
+) {
42
+  const state = props.state;
43
+  const theme = useTheme();
44
+
45
+  const { collapsible } = useCollapsible();
46
+  let translateY: number | Animated.AnimatedInterpolation = 0;
47
+  if (collapsible) {
48
+    translateY = Animated.multiply(-1.5, collapsible.translateY);
49
+  }
36 50
 
37
-interface PropsType extends BottomTabBarProps {
38
-  theme: ReactNativePaper.Theme;
51
+  return (
52
+    <Animated.View
53
+      style={{
54
+        ...styles.bar,
55
+        backgroundColor: theme.colors.surface,
56
+        transform: [{ translateY: translateY }],
57
+      }}
58
+    >
59
+      {state.routes.map(
60
+        (
61
+          route: {
62
+            key: string;
63
+            name: string;
64
+            params?: object | undefined;
65
+          },
66
+          index: number
67
+        ) => {
68
+          const iconData = props.icons[route.name];
69
+          return (
70
+            <TabIcon
71
+              isMiddle={index === 2}
72
+              onPress={() => props.navigation.navigate(route.name)}
73
+              icon={iconData.normal}
74
+              focusedIcon={iconData.focused}
75
+              label={props.labels[route.name]}
76
+              focused={state.index === index}
77
+              key={route.key}
78
+            />
79
+          );
80
+        }
81
+      )}
82
+    </Animated.View>
83
+  );
39 84
 }
40 85
 
41
-type StateType = {
42
-  translateY: any;
43
-};
44
-
45
-type validRoutes = 'proxiwash' | 'services' | 'planning' | 'planex';
46
-
47
-const TAB_ICONS = {
48
-  proxiwash: 'tshirt-crew',
49
-  services: 'account-circle',
50
-  planning: 'calendar-range',
51
-  planex: 'clock',
52
-};
53
-
54 86
 const styles = StyleSheet.create({
55
-  container: {
87
+  bar: {
56 88
     flexDirection: 'row',
57 89
     width: '100%',
90
+    height: 50,
58 91
     position: 'absolute',
59 92
     bottom: 0,
60 93
     left: 0,
61 94
   },
62 95
 });
63 96
 
64
-class CustomTabBar extends React.Component<PropsType, StateType> {
65
-  static TAB_BAR_HEIGHT = 48;
66
-
67
-  constructor(props: PropsType) {
68
-    super(props);
69
-    this.state = {
70
-      translateY: new Animated.Value(0),
71
-    };
72
-    // @ts-ignore
73
-    props.navigation.addListener('state', this.onRouteChange);
74
-  }
75
-
76
-  /**
77
-   * Navigates to the given route if it is different from the current one
78
-   *
79
-   * @param route Destination route
80
-   * @param currentIndex The current route index
81
-   * @param destIndex The destination route index
82
-   */
83
-  onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
84
-    const { navigation } = this.props;
85
-    if (currentIndex !== destIndex) {
86
-      navigation.navigate(route.name);
87
-    }
88
-  }
89
-
90
-  /**
91
-   * Navigates to tetris screen on home button long press
92
-   *
93
-   * @param route
94
-   */
95
-  onItemLongPress(route: RouteType) {
96
-    const { navigation } = this.props;
97
-    if (route.name === 'home') {
98
-      navigation.navigate('game-start');
99
-    }
100
-  }
101
-
102
-  /**
103
-   * Finds the active route and syncs the tab bar animation with the header bar
104
-   */
105
-  onRouteChange = () => {
106
-    const { props } = this;
107
-    props.state.routes.map(this.syncTabBar);
108
-  };
109
-
110
-  /**
111
-   * Gets an icon for the given route if it is not the home one as it uses a custom button
112
-   *
113
-   * @param route
114
-   * @param focused
115
-   * @returns {null}
116
-   */
117
-  getTabBarIcon = (route: RouteType, focused: boolean) => {
118
-    let icon = TAB_ICONS[route.name as validRoutes];
119
-    icon = focused ? icon : `${icon}-outline`;
120
-    if (route.name !== 'home') {
121
-      return icon;
122
-    }
123
-    return '';
124
-  };
125
-
126
-  /**
127
-   * Gets a tab icon render.
128
-   * If the given route is focused, it syncs the tab bar and header bar animations together
129
-   *
130
-   * @param route The route for the icon
131
-   * @param index The index of the current route
132
-   * @returns {*}
133
-   */
134
-  getRenderIcon = (route: RouteType, index: number) => {
135
-    const { props } = this;
136
-    const { state } = props;
137
-    const { options } = props.descriptors[route.key];
138
-    let label;
139
-    if (options.tabBarLabel != null) {
140
-      label = options.tabBarLabel;
141
-    } else if (options.title != null) {
142
-      label = options.title;
143
-    } else {
144
-      label = route.name;
145
-    }
146
-
147
-    const onPress = () => {
148
-      this.onItemPress(route, state.index, index);
149
-    };
150
-    const onLongPress = () => {
151
-      this.onItemLongPress(route);
152
-    };
153
-    const isFocused = state.index === index;
154
-
155
-    const color = isFocused
156
-      ? props.theme.colors.primary
157
-      : props.theme.colors.tabIcon;
158
-    if (route.name !== 'home') {
159
-      return (
160
-        <TabIcon
161
-          onPress={onPress}
162
-          onLongPress={onLongPress}
163
-          icon={this.getTabBarIcon(route, isFocused)}
164
-          color={color}
165
-          label={label as string}
166
-          focused={isFocused}
167
-          extraData={state.index > index}
168
-          key={route.key}
169
-        />
170
-      );
171
-    }
172
-    return (
173
-      <TabHomeIcon
174
-        onPress={onPress}
175
-        onLongPress={onLongPress}
176
-        focused={isFocused}
177
-        key={route.key}
178
-        tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
179
-      />
180
-    );
181
-  };
182
-
183
-  getIcons() {
184
-    const { props } = this;
185
-    return props.state.routes.map(this.getRenderIcon);
186
-  }
187
-
188
-  syncTabBar = (route: RouteType, index: number) => {
189
-    const { state } = this.props;
190
-    const isFocused = state.index === index;
191
-    if (isFocused) {
192
-      const stackState = route.state;
193
-      const stackRoute =
194
-        stackState && stackState.index != null
195
-          ? stackState.routes[stackState.index]
196
-          : null;
197
-      const params: { collapsible: Collapsible } | null | undefined = stackRoute
198
-        ? (stackRoute.params as { collapsible: Collapsible })
199
-        : null;
200
-      const collapsible = params != null ? params.collapsible : null;
201
-      if (collapsible != null) {
202
-        this.setState({
203
-          translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
204
-        });
205
-      }
206
-    }
207
-  };
208
-
209
-  render() {
210
-    const { props, state } = this;
211
-    const icons = this.getIcons();
212
-    return (
213
-      <Animated.View
214
-        style={{
215
-          height: CustomTabBar.TAB_BAR_HEIGHT,
216
-          backgroundColor: props.theme.colors.surface,
217
-          transform: [{ translateY: state.translateY }],
218
-          ...styles.container,
219
-        }}
220
-      >
221
-        {icons}
222
-      </Animated.View>
223
-    );
224
-  }
97
+function areEqual(
98
+  prevProps: BottomTabBarProps<any>,
99
+  nextProps: BottomTabBarProps<any>
100
+) {
101
+  return prevProps.state.index === nextProps.state.index;
225 102
 }
226 103
 
227
-export default withTheme(CustomTabBar);
104
+export default React.memo(CustomTabBar, areEqual);

+ 80
- 101
src/components/Tabbar/TabHomeIcon.tsx View File

@@ -1,135 +1,114 @@
1
-/*
2
- * Copyright (c) 2019 - 2020 Arnaud Vergnet.
3
- *
4
- * This file is part of Campus INSAT.
5
- *
6
- * Campus INSAT is free software: you can redistribute it and/or modify
7
- *  it under the terms of the GNU General Public License as published by
8
- * the Free Software Foundation, either version 3 of the License, or
9
- * (at your option) any later version.
10
- *
11
- * Campus INSAT is distributed in the hope that it will be useful,
12
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
- * GNU General Public License for more details.
15
- *
16
- * You should have received a copy of the GNU General Public License
17
- * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
- */
19
-
20
-import * as React from 'react';
21
-import { Image, StyleSheet, View } from 'react-native';
1
+import React from 'react';
2
+import { View, StyleSheet, Image } from 'react-native';
22 3
 import { FAB } from 'react-native-paper';
23 4
 import * as Animatable from 'react-native-animatable';
24
-const FOCUSED_ICON = require('../../../assets/tab-icon.png');
25
-const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
26 5
 
27
-type PropsType = {
6
+interface Props {
7
+  icon: string;
8
+  focusedIcon: string;
28 9
   focused: boolean;
29 10
   onPress: () => void;
30
-  onLongPress: () => void;
31
-  tabBarHeight: number;
32
-};
11
+}
33 12
 
34
-const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
13
+Animatable.initializeRegistryWithDefinitions({
14
+  fabFocusIn: {
15
+    0: {
16
+      // @ts-ignore
17
+      scale: 1,
18
+      translateY: 0,
19
+    },
20
+    0.4: {
21
+      // @ts-ignore
22
+      scale: 1.2,
23
+      translateY: -9,
24
+    },
25
+    0.6: {
26
+      // @ts-ignore
27
+      scale: 1.05,
28
+      translateY: -6,
29
+    },
30
+    0.8: {
31
+      // @ts-ignore
32
+      scale: 1.15,
33
+      translateY: -6,
34
+    },
35
+    1: {
36
+      // @ts-ignore
37
+      scale: 1.1,
38
+      translateY: -6,
39
+    },
40
+  },
41
+  fabFocusOut: {
42
+    0: {
43
+      // @ts-ignore
44
+      scale: 1.1,
45
+      translateY: -6,
46
+    },
47
+    1: {
48
+      // @ts-ignore
49
+      scale: 1,
50
+      translateY: 0,
51
+    },
52
+  },
53
+});
35 54
 
36 55
 const styles = StyleSheet.create({
37
-  container: {
56
+  outer: {
38 57
     flex: 1,
39 58
     justifyContent: 'center',
40 59
   },
41
-  subcontainer: {
60
+  inner: {
42 61
     position: 'absolute',
43 62
     bottom: 0,
44 63
     left: 0,
45 64
     width: '100%',
46
-    marginBottom: -15,
65
+    height: 60,
47 66
   },
48 67
   fab: {
49
-    marginTop: 15,
50 68
     marginLeft: 'auto',
51 69
     marginRight: 'auto',
52 70
   },
53 71
 });
54 72
 
55
-/**
56
- * Abstraction layer for Agenda component, using custom configuration
57
- */
58
-class TabHomeIcon extends React.Component<PropsType> {
59
-  constructor(props: PropsType) {
60
-    super(props);
61
-    Animatable.initializeRegistryWithDefinitions({
62
-      fabFocusIn: {
63
-        '0': {
64
-          // @ts-ignore
65
-          scale: 1,
66
-          translateY: 0,
67
-        },
68
-        '0.9': {
69
-          scale: 1.2,
70
-          translateY: -9,
71
-        },
72
-        '1': {
73
-          scale: 1.1,
74
-          translateY: -7,
75
-        },
76
-      },
77
-      fabFocusOut: {
78
-        '0': {
79
-          // @ts-ignore
80
-          scale: 1.1,
81
-          translateY: -6,
82
-        },
83
-        '1': {
84
-          scale: 1,
85
-          translateY: 0,
86
-        },
87
-      },
88
-    });
89
-  }
90
-
91
-  shouldComponentUpdate(nextProps: PropsType): boolean {
92
-    const { focused } = this.props;
93
-    return nextProps.focused !== focused;
94
-  }
73
+const FOCUSED_ICON = require('../../../assets/tab-icon.png');
74
+const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
95 75
 
96
-  getIconRender = ({ size, color }: { size: number; color: string }) => {
97
-    const { focused } = this.props;
76
+function TabHomeIcon(props: Props) {
77
+  const getImage = (iconProps: { size: number; color: string }) => {
98 78
     return (
99
-      <Image
100
-        source={focused ? FOCUSED_ICON : UNFOCUSED_ICON}
101
-        style={{
102
-          width: size,
103
-          height: size,
104
-          tintColor: color,
105
-        }}
106
-      />
79
+      <Animatable.View useNativeDriver={true} animation={'rubberBand'}>
80
+        <Image
81
+          source={props.focused ? FOCUSED_ICON : UNFOCUSED_ICON}
82
+          style={{
83
+            width: iconProps.size,
84
+            height: iconProps.size,
85
+            tintColor: iconProps.color,
86
+          }}
87
+        />
88
+      </Animatable.View>
107 89
     );
108 90
   };
109 91
 
110
-  render() {
111
-    const { props } = this;
112
-    return (
113
-      <View style={styles.container}>
114
-        <View
115
-          style={{
116
-            height: props.tabBarHeight + 30,
117
-            ...styles.subcontainer,
118
-          }}
92
+  return (
93
+    <View style={styles.outer}>
94
+      <View style={styles.inner}>
95
+        <Animatable.View
96
+          style={styles.fab}
97
+          useNativeDriver={true}
98
+          duration={props.focused ? 500 : 200}
99
+          animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
100
+          easing={'ease-out'}
119 101
         >
120
-          <AnimatedFAB
121
-            duration={200}
122
-            easing="ease-out"
123
-            animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
124
-            icon={this.getIconRender}
102
+          <FAB
125 103
             onPress={props.onPress}
126
-            onLongPress={props.onLongPress}
127
-            style={styles.fab}
104
+            animated={false}
105
+            icon={getImage}
106
+            color={'#fff'}
128 107
           />
129
-        </View>
108
+        </Animatable.View>
130 109
       </View>
131
-    );
132
-  }
110
+    </View>
111
+  );
133 112
 }
134 113
 
135 114
 export default TabHomeIcon;

+ 28
- 130
src/components/Tabbar/TabIcon.tsx View File

@@ -1,143 +1,41 @@
1
-/*
2
- * Copyright (c) 2019 - 2020 Arnaud Vergnet.
3
- *
4
- * This file is part of Campus INSAT.
5
- *
6
- * Campus INSAT is free software: you can redistribute it and/or modify
7
- *  it under the terms of the GNU General Public License as published by
8
- * the Free Software Foundation, either version 3 of the License, or
9
- * (at your option) any later version.
10
- *
11
- * Campus INSAT is distributed in the hope that it will be useful,
12
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
- * GNU General Public License for more details.
15
- *
16
- * You should have received a copy of the GNU General Public License
17
- * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
- */
1
+import React from 'react';
2
+import TabHomeIcon from './TabHomeIcon';
3
+import TabSideIcon from './TabSideIcon';
19 4
 
20
-import * as React from 'react';
21
-import { StyleSheet, View } from 'react-native';
22
-import { TouchableRipple, withTheme } from 'react-native-paper';
23
-import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
24
-import * as Animatable from 'react-native-animatable';
25
-import GENERAL_STYLES from '../../constants/Styles';
26
-
27
-type PropsType = {
5
+interface Props {
6
+  isMiddle: boolean;
28 7
   focused: boolean;
29
-  color: string;
30
-  label: string;
8
+  label: string | undefined;
31 9
   icon: string;
10
+  focusedIcon: string;
32 11
   onPress: () => void;
33
-  onLongPress: () => void;
34
-  theme: ReactNativePaper.Theme;
35
-  extraData: null | boolean | number | string;
36
-};
37
-
38
-const styles = StyleSheet.create({
39
-  container: {
40
-    flex: 1,
41
-    justifyContent: 'center',
42
-    borderRadius: 10,
43
-  },
44
-  text: {
45
-    marginLeft: 'auto',
46
-    marginRight: 'auto',
47
-    fontSize: 10,
48
-  },
49
-});
50
-
51
-/**
52
- * Abstraction layer for Agenda component, using custom configuration
53
- */
54
-class TabIcon extends React.Component<PropsType> {
55
-  firstRender: boolean;
56
-
57
-  constructor(props: PropsType) {
58
-    super(props);
59
-    Animatable.initializeRegistryWithDefinitions({
60
-      focusIn: {
61
-        '0': {
62
-          // @ts-ignore
63
-          scale: 1,
64
-          translateY: 0,
65
-        },
66
-        '0.9': {
67
-          scale: 1.3,
68
-          translateY: 7,
69
-        },
70
-        '1': {
71
-          scale: 1.2,
72
-          translateY: 6,
73
-        },
74
-      },
75
-      focusOut: {
76
-        '0': {
77
-          // @ts-ignore
78
-          scale: 1.2,
79
-          translateY: 6,
80
-        },
81
-        '1': {
82
-          scale: 1,
83
-          translateY: 0,
84
-        },
85
-      },
86
-    });
87
-    this.firstRender = true;
88
-  }
89
-
90
-  componentDidMount() {
91
-    this.firstRender = false;
92
-  }
12
+}
93 13
 
94
-  shouldComponentUpdate(nextProps: PropsType): boolean {
95
-    const { props } = this;
14
+function TabIcon(props: Props) {
15
+  if (props.isMiddle) {
96 16
     return (
97
-      nextProps.focused !== props.focused ||
98
-      nextProps.theme.dark !== props.theme.dark ||
99
-      nextProps.extraData !== props.extraData
17
+      <TabHomeIcon
18
+        icon={props.icon}
19
+        focusedIcon={props.focusedIcon}
20
+        focused={props.focused}
21
+        onPress={props.onPress}
22
+      />
100 23
     );
101
-  }
102
-
103
-  render() {
104
-    const { props } = this;
24
+  } else {
105 25
     return (
106
-      <TouchableRipple
26
+      <TabSideIcon
27
+        focused={props.focused}
28
+        label={props.label}
29
+        icon={props.icon}
30
+        focusedIcon={props.focusedIcon}
107 31
         onPress={props.onPress}
108
-        onLongPress={props.onLongPress}
109
-        rippleColor={props.theme.colors.primary}
110
-        borderless={true}
111
-        style={styles.container}
112
-      >
113
-        <View>
114
-          <Animatable.View
115
-            duration={200}
116
-            easing="ease-out"
117
-            animation={props.focused ? 'focusIn' : 'focusOut'}
118
-            useNativeDriver
119
-          >
120
-            <MaterialCommunityIcons
121
-              name={props.icon}
122
-              color={props.color}
123
-              size={26}
124
-              style={GENERAL_STYLES.centerHorizontal}
125
-            />
126
-          </Animatable.View>
127
-          <Animatable.Text
128
-            animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
129
-            useNativeDriver
130
-            style={{
131
-              color: props.color,
132
-              ...styles.text,
133
-            }}
134
-          >
135
-            {props.label}
136
-          </Animatable.Text>
137
-        </View>
138
-      </TouchableRipple>
32
+      />
139 33
     );
140 34
   }
141 35
 }
142 36
 
143
-export default withTheme(TabIcon);
37
+function areEqual(prevProps: Props, nextProps: Props) {
38
+  return prevProps.focused === nextProps.focused;
39
+}
40
+
41
+export default React.memo(TabIcon, areEqual);

+ 113
- 0
src/components/Tabbar/TabSideIcon.tsx View File

@@ -0,0 +1,113 @@
1
+import React from 'react';
2
+import { TouchableRipple, useTheme } from 'react-native-paper';
3
+import { StyleSheet, View } from 'react-native';
4
+import * as Animatable from 'react-native-animatable';
5
+import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
6
+import GENERAL_STYLES from '../../constants/Styles';
7
+
8
+interface Props {
9
+  focused: boolean;
10
+  label: string | undefined;
11
+  icon: string;
12
+  focusedIcon: string;
13
+  onPress: () => void;
14
+}
15
+
16
+Animatable.initializeRegistryWithDefinitions({
17
+  focusIn: {
18
+    0: {
19
+      // @ts-ignore
20
+      scale: 1,
21
+      translateY: 0,
22
+    },
23
+    0.4: {
24
+      // @ts-ignore
25
+      scale: 1.3,
26
+      translateY: 6,
27
+    },
28
+    0.6: {
29
+      // @ts-ignore
30
+      scale: 1.1,
31
+      translateY: 6,
32
+    },
33
+    0.8: {
34
+      // @ts-ignore
35
+      scale: 1.25,
36
+      translateY: 6,
37
+    },
38
+    1: {
39
+      // @ts-ignore
40
+      scale: 1.2,
41
+      translateY: 6,
42
+    },
43
+  },
44
+  focusOut: {
45
+    0: {
46
+      // @ts-ignore
47
+      scale: 1.2,
48
+      translateY: 6,
49
+    },
50
+    1: {
51
+      // @ts-ignore
52
+      scale: 1,
53
+      translateY: 0,
54
+    },
55
+  },
56
+});
57
+
58
+function TabSideIcon(props: Props) {
59
+  const theme = useTheme();
60
+  const color = props.focused ? theme.colors.primary : theme.colors.disabled;
61
+  let icon = props.focused ? props.focusedIcon : props.icon;
62
+  return (
63
+    <TouchableRipple
64
+      onPress={props.onPress}
65
+      borderless
66
+      rippleColor={theme.colors.primary}
67
+      style={{
68
+        ...styles.ripple,
69
+        borderTopEndRadius: theme.roundness,
70
+        borderTopStartRadius: theme.roundness,
71
+      }}
72
+    >
73
+      <View>
74
+        <Animatable.View
75
+          duration={props.focused ? 500 : 200}
76
+          easing="ease-out"
77
+          animation={props.focused ? 'focusIn' : 'focusOut'}
78
+          useNativeDriver
79
+        >
80
+          <MaterialCommunityIcons
81
+            name={icon}
82
+            color={color}
83
+            size={26}
84
+            style={GENERAL_STYLES.centerHorizontal}
85
+          />
86
+        </Animatable.View>
87
+        <Animatable.Text
88
+          animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
89
+          useNativeDriver
90
+          style={{
91
+            color: color,
92
+            ...styles.text,
93
+          }}
94
+        >
95
+          {props.label}
96
+        </Animatable.Text>
97
+      </View>
98
+    </TouchableRipple>
99
+  );
100
+}
101
+
102
+const styles = StyleSheet.create({
103
+  ripple: {
104
+    flex: 1,
105
+    justifyContent: 'center',
106
+  },
107
+  text: {
108
+    ...GENERAL_STYLES.centerHorizontal,
109
+    fontSize: 10,
110
+  },
111
+});
112
+
113
+export default TabSideIcon;

+ 33
- 0
src/components/providers/CollapsibleProvider.tsx View File

@@ -0,0 +1,33 @@
1
+import React, { useState } from 'react';
2
+import { Collapsible } from 'react-navigation-collapsible';
3
+import {
4
+  CollapsibleContext,
5
+  CollapsibleContextType,
6
+} from '../../utils/CollapsibleContext';
7
+
8
+type Props = {
9
+  children: React.ReactChild;
10
+};
11
+
12
+export default function CollapsibleProvider(props: Props) {
13
+  const setCollapsible = (collapsible: Collapsible) => {
14
+    setCurrentCollapsible((prevState) => ({
15
+      ...prevState,
16
+      collapsible,
17
+    }));
18
+  };
19
+
20
+  const [
21
+    currentCollapsible,
22
+    setCurrentCollapsible,
23
+  ] = useState<CollapsibleContextType>({
24
+    collapsible: undefined,
25
+    setCollapsible: setCollapsible,
26
+  });
27
+
28
+  return (
29
+    <CollapsibleContext.Provider value={currentCollapsible}>
30
+      {props.children}
31
+    </CollapsibleContext.Provider>
32
+  );
33
+}

+ 162
- 165
src/navigation/MainNavigator.tsx View File

@@ -18,12 +18,8 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import {
22
-  createStackNavigator,
23
-  TransitionPresets,
24
-} from '@react-navigation/stack';
21
+import { createStackNavigator } from '@react-navigation/stack';
25 22
 import i18n from 'i18n-js';
26
-import { Platform } from 'react-native';
27 23
 import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
28 24
 import AboutScreen from '../screens/About/AboutScreen';
29 25
 import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
@@ -40,10 +36,6 @@ import ProfileScreen from '../screens/Amicale/ProfileScreen';
40 36
 import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
41 37
 import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
42 38
 import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
43
-import {
44
-  CreateScreenCollapsibleStack,
45
-  getWebsiteStack,
46
-} from '../utils/CollapsibleUtils';
47 39
 import BugReportScreen from '../screens/Other/FeedbackScreen';
48 40
 import WebsiteScreen from '../screens/Services/WebsiteScreen';
49 41
 import EquipmentScreen, {
@@ -54,6 +46,7 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir
54 46
 import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
55 47
 import GameStartScreen from '../screens/Game/screens/GameStartScreen';
56 48
 import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen';
49
+import Test from '../screens/Test';
57 50
 
58 51
 export enum MainRoutes {
59 52
   Main = 'main',
@@ -83,7 +76,7 @@ export enum MainRoutes {
83 76
 
84 77
 type DefaultParams = { [key in MainRoutes]: object | undefined };
85 78
 
86
-export interface FullParamsList extends DefaultParams {
79
+export type FullParamsList = DefaultParams & {
87 80
   'login': { nextScreen: string };
88 81
   'equipment-confirm': {
89 82
     item?: DeviceType;
@@ -91,34 +84,22 @@ export interface FullParamsList extends DefaultParams {
91 84
   };
92 85
   'equipment-rent': { item?: DeviceType };
93 86
   'gallery': { images: Array<{ url: string }> };
94
-}
87
+};
95 88
 
96 89
 // Don't know why but TS is complaining without this
97 90
 // See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
98 91
 export type MainStackParamsList = FullParamsList &
99 92
   Record<string, object | undefined>;
100 93
 
101
-const modalTransition =
102
-  Platform.OS === 'ios'
103
-    ? TransitionPresets.ModalPresentationIOS
104
-    : TransitionPresets.ModalTransition;
105
-
106
-const defaultScreenOptions = {
107
-  gestureEnabled: true,
108
-  cardOverlayEnabled: true,
109
-  ...TransitionPresets.SlideFromRightIOS,
110
-};
111
-
112 94
 const MainStack = createStackNavigator<MainStackParamsList>();
113 95
 
114
-function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
96
+function MainStackComponent(props: {
97
+  createTabNavigator: () => React.ReactElement;
98
+}) {
115 99
   const { createTabNavigator } = props;
116 100
   return (
117
-    <MainStack.Navigator
118
-      initialRouteName={MainRoutes.Main}
119
-      headerMode="screen"
120
-      screenOptions={defaultScreenOptions}
121
-    >
101
+    <MainStack.Navigator initialRouteName={MainRoutes.Main} headerMode="screen">
102
+      <MainStack.Screen name={'test'} component={Test} />
122 103
       <MainStack.Screen
123 104
         name={MainRoutes.Main}
124 105
         component={createTabNavigator}
@@ -132,49 +113,53 @@ function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
132 113
         component={ImageGalleryScreen}
133 114
         options={{
134 115
           headerShown: false,
135
-          ...modalTransition,
136 116
         }}
137 117
       />
138
-      {CreateScreenCollapsibleStack(
139
-        MainRoutes.Settings,
140
-        MainStack,
141
-        SettingsScreen,
142
-        i18n.t('screens.settings.title')
143
-      )}
144
-      {CreateScreenCollapsibleStack(
145
-        MainRoutes.DashboardEdit,
146
-        MainStack,
147
-        DashboardEditScreen,
148
-        i18n.t('screens.settings.dashboardEdit.title')
149
-      )}
150
-      {CreateScreenCollapsibleStack(
151
-        MainRoutes.About,
152
-        MainStack,
153
-        AboutScreen,
154
-        i18n.t('screens.about.title')
155
-      )}
156
-      {CreateScreenCollapsibleStack(
157
-        MainRoutes.Dependencies,
158
-        MainStack,
159
-        AboutDependenciesScreen,
160
-        i18n.t('screens.about.libs')
161
-      )}
162
-      {CreateScreenCollapsibleStack(
163
-        MainRoutes.Debug,
164
-        MainStack,
165
-        DebugScreen,
166
-        i18n.t('screens.about.debug')
167
-      )}
168
-
169
-      {CreateScreenCollapsibleStack(
170
-        MainRoutes.GameStart,
171
-        MainStack,
172
-        GameStartScreen,
173
-        i18n.t('screens.game.title'),
174
-        true,
175
-        undefined,
176
-        'transparent'
177
-      )}
118
+      <MainStack.Screen
119
+        name={MainRoutes.Settings}
120
+        component={SettingsScreen}
121
+        options={{
122
+          title: i18n.t('screens.settings.title'),
123
+        }}
124
+      />
125
+      <MainStack.Screen
126
+        name={MainRoutes.DashboardEdit}
127
+        component={DashboardEditScreen}
128
+        options={{
129
+          title: i18n.t('screens.settings.dashboardEdit.title'),
130
+        }}
131
+      />
132
+      <MainStack.Screen
133
+        name={MainRoutes.About}
134
+        component={AboutScreen}
135
+        options={{
136
+          title: i18n.t('screens.about.title'),
137
+        }}
138
+      />
139
+      <MainStack.Screen
140
+        name={MainRoutes.Dependencies}
141
+        component={AboutDependenciesScreen}
142
+        options={{
143
+          title: i18n.t('screens.about.libs'),
144
+        }}
145
+      />
146
+      <MainStack.Screen
147
+        name={MainRoutes.Debug}
148
+        component={DebugScreen}
149
+        options={{
150
+          title: i18n.t('screens.about.debug'),
151
+        }}
152
+      />
153
+      <MainStack.Screen
154
+        name={MainRoutes.GameStart}
155
+        component={GameStartScreen}
156
+        options={{
157
+          title: i18n.t('screens.game.title'),
158
+          headerStyle: {
159
+            backgroundColor: 'transparent',
160
+          },
161
+        }}
162
+      />
178 163
       <MainStack.Screen
179 164
         name={MainRoutes.GameMain}
180 165
         component={GameMainScreen}
@@ -182,102 +167,114 @@ function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
182 167
           title: i18n.t('screens.game.title'),
183 168
         }}
184 169
       />
185
-      {CreateScreenCollapsibleStack(
186
-        MainRoutes.Login,
187
-        MainStack,
188
-        LoginScreen,
189
-        i18n.t('screens.login.title'),
190
-        true,
191
-        { headerTintColor: '#fff' },
192
-        'transparent'
193
-      )}
194
-      {getWebsiteStack('website', MainStack, WebsiteScreen, '')}
195
-
196
-      {CreateScreenCollapsibleStack(
197
-        MainRoutes.SelfMenu,
198
-        MainStack,
199
-        SelfMenuScreen,
200
-        i18n.t('screens.menu.title')
201
-      )}
202
-      {CreateScreenCollapsibleStack(
203
-        MainRoutes.Proximo,
204
-        MainStack,
205
-        ProximoMainScreen,
206
-        i18n.t('screens.proximo.title')
207
-      )}
208
-      {CreateScreenCollapsibleStack(
209
-        MainRoutes.ProximoList,
210
-        MainStack,
211
-        ProximoListScreen,
212
-        i18n.t('screens.proximo.articleList')
213
-      )}
214
-      {CreateScreenCollapsibleStack(
215
-        MainRoutes.ProximoAbout,
216
-        MainStack,
217
-        ProximoAboutScreen,
218
-        i18n.t('screens.proximo.title'),
219
-        true,
220
-        { ...modalTransition }
221
-      )}
222
-
223
-      {CreateScreenCollapsibleStack(
224
-        MainRoutes.Profile,
225
-        MainStack,
226
-        ProfileScreen,
227
-        i18n.t('screens.profile.title')
228
-      )}
229
-      {CreateScreenCollapsibleStack(
230
-        MainRoutes.ClubList,
231
-        MainStack,
232
-        ClubListScreen,
233
-        i18n.t('screens.clubs.title')
234
-      )}
235
-      {CreateScreenCollapsibleStack(
236
-        MainRoutes.ClubInformation,
237
-        MainStack,
238
-        ClubDisplayScreen,
239
-        i18n.t('screens.clubs.details'),
240
-        true,
241
-        { ...modalTransition }
242
-      )}
243
-      {CreateScreenCollapsibleStack(
244
-        MainRoutes.ClubAbout,
245
-        MainStack,
246
-        ClubAboutScreen,
247
-        i18n.t('screens.clubs.title'),
248
-        true,
249
-        { ...modalTransition }
250
-      )}
251
-      {CreateScreenCollapsibleStack(
252
-        MainRoutes.EquipmentList,
253
-        MainStack,
254
-        EquipmentScreen,
255
-        i18n.t('screens.equipment.title')
256
-      )}
257
-      {CreateScreenCollapsibleStack(
258
-        MainRoutes.EquipmentRent,
259
-        MainStack,
260
-        EquipmentLendScreen,
261
-        i18n.t('screens.equipment.book')
262
-      )}
263
-      {CreateScreenCollapsibleStack(
264
-        MainRoutes.EquipmentConfirm,
265
-        MainStack,
266
-        EquipmentConfirmScreen,
267
-        i18n.t('screens.equipment.confirm')
268
-      )}
269
-      {CreateScreenCollapsibleStack(
270
-        MainRoutes.Vote,
271
-        MainStack,
272
-        VoteScreen,
273
-        i18n.t('screens.vote.title')
274
-      )}
275
-      {CreateScreenCollapsibleStack(
276
-        MainRoutes.Feedback,
277
-        MainStack,
278
-        BugReportScreen,
279
-        i18n.t('screens.feedback.title')
280
-      )}
170
+      <MainStack.Screen
171
+        name={MainRoutes.Login}
172
+        component={LoginScreen}
173
+        options={{
174
+          title: i18n.t('screens.login.title'),
175
+          headerStyle: {
176
+            backgroundColor: 'transparent',
177
+          },
178
+        }}
179
+      />
180
+      <MainStack.Screen
181
+        name={'website'}
182
+        component={WebsiteScreen}
183
+        options={{
184
+          title: '',
185
+        }}
186
+      />
187
+      <MainStack.Screen
188
+        name={MainRoutes.SelfMenu}
189
+        component={SelfMenuScreen}
190
+        options={{
191
+          title: i18n.t('screens.menu.title'),
192
+        }}
193
+      />
194
+      <MainStack.Screen
195
+        name={MainRoutes.Proximo}
196
+        component={ProximoMainScreen}
197
+        options={{
198
+          title: i18n.t('screens.proximo.title'),
199
+        }}
200
+      />
201
+      <MainStack.Screen
202
+        name={MainRoutes.ProximoList}
203
+        component={ProximoListScreen}
204
+        options={{
205
+          title: i18n.t('screens.proximo.articleList'),
206
+        }}
207
+      />
208
+      <MainStack.Screen
209
+        name={MainRoutes.ProximoAbout}
210
+        component={ProximoAboutScreen}
211
+        options={{
212
+          title: i18n.t('screens.proximo.title'),
213
+        }}
214
+      />
215
+      <MainStack.Screen
216
+        name={MainRoutes.Profile}
217
+        component={ProfileScreen}
218
+        options={{
219
+          title: i18n.t('screens.profile.title'),
220
+        }}
221
+      />
222
+      <MainStack.Screen
223
+        name={MainRoutes.ClubList}
224
+        component={ClubListScreen}
225
+        options={{
226
+          title: i18n.t('screens.clubs.title'),
227
+        }}
228
+      />
229
+      <MainStack.Screen
230
+        name={MainRoutes.ClubInformation}
231
+        component={ClubDisplayScreen}
232
+        options={{
233
+          title: i18n.t('screens.clubs.details'),
234
+        }}
235
+      />
236
+      <MainStack.Screen
237
+        name={MainRoutes.ClubAbout}
238
+        component={ClubAboutScreen}
239
+        options={{
240
+          title: i18n.t('screens.clubs.title'),
241
+        }}
242
+      />
243
+      <MainStack.Screen
244
+        name={MainRoutes.EquipmentList}
245
+        component={EquipmentScreen}
246
+        options={{
247
+          title: i18n.t('screens.equipment.title'),
248
+        }}
249
+      />
250
+      <MainStack.Screen
251
+        name={MainRoutes.EquipmentRent}
252
+        component={EquipmentLendScreen}
253
+        options={{
254
+          title: i18n.t('screens.equipment.book'),
255
+        }}
256
+      />
257
+      <MainStack.Screen
258
+        name={MainRoutes.EquipmentConfirm}
259
+        component={EquipmentConfirmScreen}
260
+        options={{
261
+          title: i18n.t('screens.equipment.confirm'),
262
+        }}
263
+      />
264
+      <MainStack.Screen
265
+        name={MainRoutes.Vote}
266
+        component={VoteScreen}
267
+        options={{
268
+          title: i18n.t('screens.vote.title'),
269
+        }}
270
+      />
271
+      <MainStack.Screen
272
+        name={MainRoutes.Feedback}
273
+        component={BugReportScreen}
274
+        options={{
275
+          title: i18n.t('screens.feedback.title'),
276
+        }}
277
+      />
281 278
     </MainStack.Navigator>
282 279
   );
283 280
 }

+ 149
- 159
src/navigation/TabNavigator.tsx View File

@@ -18,16 +18,12 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import {
22
-  createStackNavigator,
23
-  TransitionPresets,
24
-} from '@react-navigation/stack';
21
+import { createStackNavigator } from '@react-navigation/stack';
25 22
 import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
26 23
 
27 24
 import { Title, useTheme } from 'react-native-paper';
28
-import { Platform, StyleSheet } from 'react-native';
25
+import { StyleSheet } from 'react-native';
29 26
 import i18n from 'i18n-js';
30
-import { createCollapsibleStack } from 'react-navigation-collapsible';
31 27
 import { View } from 'react-native-animatable';
32 28
 import HomeScreen from '../screens/Home/HomeScreen';
33 29
 import PlanningScreen from '../screens/Planning/PlanningScreen';
@@ -44,23 +40,8 @@ import CustomTabBar from '../components/Tabbar/CustomTabBar';
44 40
 import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
45 41
 import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
46 42
 import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
47
-import {
48
-  CreateScreenCollapsibleStack,
49
-  getWebsiteStack,
50
-} from '../utils/CollapsibleUtils';
51 43
 import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot';
52 44
 
53
-const modalTransition =
54
-  Platform.OS === 'ios'
55
-    ? TransitionPresets.ModalPresentationIOS
56
-    : TransitionPresets.ModalTransition;
57
-
58
-const defaultScreenOptions = {
59
-  gestureEnabled: true,
60
-  cardOverlayEnabled: true,
61
-  ...modalTransition,
62
-};
63
-
64 45
 const styles = StyleSheet.create({
65 46
   header: {
66 47
     flexDirection: 'row',
@@ -79,29 +60,22 @@ const ServicesStack = createStackNavigator();
79 60
 
80 61
 function ServicesStackComponent() {
81 62
   return (
82
-    <ServicesStack.Navigator
83
-      initialRouteName="index"
84
-      headerMode="screen"
85
-      screenOptions={defaultScreenOptions}
86
-    >
87
-      {CreateScreenCollapsibleStack(
88
-        'index',
89
-        ServicesStack,
90
-        WebsitesHomeScreen,
91
-        i18n.t('screens.services.title')
92
-      )}
93
-      {CreateScreenCollapsibleStack(
94
-        'services-section',
95
-        ServicesStack,
96
-        ServicesSectionScreen,
97
-        'SECTION'
98
-      )}
99
-      {CreateScreenCollapsibleStack(
100
-        'amicale-contact',
101
-        ServicesStack,
102
-        AmicaleContactScreen,
103
-        i18n.t('screens.amicaleAbout.title')
104
-      )}
63
+    <ServicesStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
64
+      <ServicesStack.Screen
65
+        name={'index'}
66
+        component={WebsitesHomeScreen}
67
+        options={{ title: i18n.t('screens.services.title') }}
68
+      />
69
+      <ServicesStack.Screen
70
+        name={'services-section'}
71
+        component={ServicesSectionScreen}
72
+        options={{ title: 'SECTION' }}
73
+      />
74
+      <ServicesStack.Screen
75
+        name={'amicale-contact'}
76
+        component={AmicaleContactScreen}
77
+        options={{ title: i18n.t('screens.amicaleAbout.title') }}
78
+      />
105 79
     </ServicesStack.Navigator>
106 80
   );
107 81
 }
@@ -110,23 +84,17 @@ const ProxiwashStack = createStackNavigator();
110 84
 
111 85
 function ProxiwashStackComponent() {
112 86
   return (
113
-    <ProxiwashStack.Navigator
114
-      initialRouteName="index"
115
-      headerMode="screen"
116
-      screenOptions={defaultScreenOptions}
117
-    >
118
-      {CreateScreenCollapsibleStack(
119
-        'index',
120
-        ProxiwashStack,
121
-        ProxiwashScreen,
122
-        i18n.t('screens.proxiwash.title')
123
-      )}
124
-      {CreateScreenCollapsibleStack(
125
-        'proxiwash-about',
126
-        ProxiwashStack,
127
-        ProxiwashAboutScreen,
128
-        i18n.t('screens.proxiwash.title')
129
-      )}
87
+    <ProxiwashStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
88
+      <ProxiwashStack.Screen
89
+        name={'index-contact'}
90
+        component={ProxiwashScreen}
91
+        options={{ title: i18n.t('screens.proxiwash.title') }}
92
+      />
93
+      <ProxiwashStack.Screen
94
+        name={'proxiwash-about'}
95
+        component={ProxiwashAboutScreen}
96
+        options={{ title: i18n.t('screens.proxiwash.title') }}
97
+      />
130 98
     </ProxiwashStack.Navigator>
131 99
   );
132 100
 }
@@ -135,22 +103,17 @@ const PlanningStack = createStackNavigator();
135 103
 
136 104
 function PlanningStackComponent() {
137 105
   return (
138
-    <PlanningStack.Navigator
139
-      initialRouteName="index"
140
-      headerMode="screen"
141
-      screenOptions={defaultScreenOptions}
142
-    >
106
+    <PlanningStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
143 107
       <PlanningStack.Screen
144
-        name="index"
108
+        name={'index'}
145 109
         component={PlanningScreen}
146 110
         options={{ title: i18n.t('screens.planning.title') }}
147 111
       />
148
-      {CreateScreenCollapsibleStack(
149
-        'planning-information',
150
-        PlanningStack,
151
-        PlanningDisplayScreen,
152
-        i18n.t('screens.planning.eventDetails')
153
-      )}
112
+      <PlanningStack.Screen
113
+        name={'planning-information'}
114
+        component={PlanningDisplayScreen}
115
+        options={{ title: i18n.t('screens.planning.eventDetails') }}
116
+      />
154 117
     </PlanningStack.Navigator>
155 118
   );
156 119
 }
@@ -167,73 +130,63 @@ function HomeStackComponent(
167 130
   }
168 131
   const { colors } = useTheme();
169 132
   return (
170
-    <HomeStack.Navigator
171
-      initialRouteName="index"
172
-      headerMode="screen"
173
-      screenOptions={defaultScreenOptions}
174
-    >
175
-      {createCollapsibleStack(
176
-        <HomeStack.Screen
177
-          name="index"
178
-          component={HomeScreen}
179
-          options={{
180
-            title: i18n.t('screens.home.title'),
181
-            headerStyle: {
182
-              backgroundColor: colors.surface,
183
-            },
184
-            headerTitle: () => (
185
-              <View style={styles.header}>
186
-                <Mascot
187
-                  style={styles.mascot}
188
-                  emotion={MASCOT_STYLE.RANDOM}
189
-                  animated
190
-                  entryAnimation={{
191
-                    animation: 'bounceIn',
192
-                    duration: 1000,
193
-                  }}
194
-                  loopAnimation={{
195
-                    animation: 'pulse',
196
-                    duration: 2000,
197
-                    iterationCount: 'infinite',
198
-                  }}
199
-                />
200
-                <Title style={styles.title}>
201
-                  {i18n.t('screens.home.title')}
202
-                </Title>
203
-              </View>
204
-            ),
205
-          }}
206
-          initialParams={params}
207
-        />,
208
-        {
209
-          collapsedColor: colors.surface,
210
-          useNativeDriver: true,
211
-        }
212
-      )}
133
+    <HomeStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
213 134
       <HomeStack.Screen
214
-        name="scanner"
135
+        name={'index'}
136
+        component={HomeScreen}
137
+        options={{
138
+          title: i18n.t('screens.home.title'),
139
+          headerStyle: {
140
+            backgroundColor: colors.surface,
141
+          },
142
+          headerTitle: (headerProps) => (
143
+            <View style={styles.header}>
144
+              <Mascot
145
+                style={styles.mascot}
146
+                emotion={MASCOT_STYLE.RANDOM}
147
+                animated
148
+                entryAnimation={{
149
+                  animation: 'bounceIn',
150
+                  duration: 1000,
151
+                }}
152
+                loopAnimation={{
153
+                  animation: 'pulse',
154
+                  duration: 2000,
155
+                  iterationCount: 'infinite',
156
+                }}
157
+              />
158
+              <Title style={styles.title}>{headerProps.children}</Title>
159
+            </View>
160
+          ),
161
+        }}
162
+        initialParams={params}
163
+      />
164
+      <HomeStack.Screen
165
+        name={'scanner'}
215 166
         component={ScannerScreen}
216 167
         options={{ title: i18n.t('screens.scanner.title') }}
217 168
       />
218
-
219
-      {CreateScreenCollapsibleStack(
220
-        'club-information',
221
-        HomeStack,
222
-        ClubDisplayScreen,
223
-        i18n.t('screens.clubs.details')
224
-      )}
225
-      {CreateScreenCollapsibleStack(
226
-        'feed-information',
227
-        HomeStack,
228
-        FeedItemScreen,
229
-        i18n.t('screens.home.feed')
230
-      )}
231
-      {CreateScreenCollapsibleStack(
232
-        'planning-information',
233
-        HomeStack,
234
-        PlanningDisplayScreen,
235
-        i18n.t('screens.planning.eventDetails')
236
-      )}
169
+      <HomeStack.Screen
170
+        name={'club-information'}
171
+        component={ClubDisplayScreen}
172
+        options={{
173
+          title: i18n.t('screens.clubs.details'),
174
+        }}
175
+      />
176
+      <HomeStack.Screen
177
+        name={'feed-information'}
178
+        component={FeedItemScreen}
179
+        options={{
180
+          title: i18n.t('screens.home.feed'),
181
+        }}
182
+      />
183
+      <HomeStack.Screen
184
+        name={'planning-information'}
185
+        component={PlanningDisplayScreen}
186
+        options={{
187
+          title: i18n.t('screens.planning.eventDetails'),
188
+        }}
189
+      />
237 190
     </HomeStack.Navigator>
238 191
   );
239 192
 }
@@ -242,23 +195,21 @@ const PlanexStack = createStackNavigator();
242 195
 
243 196
 function PlanexStackComponent() {
244 197
   return (
245
-    <PlanexStack.Navigator
246
-      initialRouteName="index"
247
-      headerMode="screen"
248
-      screenOptions={defaultScreenOptions}
249
-    >
250
-      {getWebsiteStack(
251
-        'index',
252
-        PlanexStack,
253
-        PlanexScreen,
254
-        i18n.t('screens.planex.title')
255
-      )}
256
-      {CreateScreenCollapsibleStack(
257
-        'group-select',
258
-        PlanexStack,
259
-        GroupSelectionScreen,
260
-        ''
261
-      )}
198
+    <PlanexStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
199
+      <PlanexStack.Screen
200
+        name={'index'}
201
+        component={PlanexScreen}
202
+        options={{
203
+          title: i18n.t('screens.planex.title'),
204
+        }}
205
+      />
206
+      <PlanexStack.Screen
207
+        name={'group-select'}
208
+        component={GroupSelectionScreen}
209
+        options={{
210
+          title: '',
211
+        }}
212
+      />
262 213
     </PlanexStack.Navigator>
263 214
   );
264 215
 }
@@ -270,6 +221,34 @@ type PropsType = {
270 221
   defaultHomeData: { [key: string]: string };
271 222
 };
272 223
 
224
+const ICONS: {
225
+  [key: string]: {
226
+    normal: string;
227
+    focused: string;
228
+  };
229
+} = {
230
+  services: {
231
+    normal: 'account-circle-outline',
232
+    focused: 'account-circle',
233
+  },
234
+  proxiwash: {
235
+    normal: 'tshirt-crew-outline',
236
+    focused: 'tshirt-crew',
237
+  },
238
+  home: {
239
+    normal: '',
240
+    focused: '',
241
+  },
242
+  planning: {
243
+    normal: 'calendar-range-outline',
244
+    focused: 'calendar-range',
245
+  },
246
+  planex: {
247
+    normal: 'clock-outline',
248
+    focused: 'clock',
249
+  },
250
+};
251
+
273 252
 export default class TabNavigator extends React.Component<PropsType> {
274 253
   defaultRoute: string;
275 254
   createHomeStackComponent: () => any;
@@ -287,33 +266,44 @@ export default class TabNavigator extends React.Component<PropsType> {
287 266
   }
288 267
 
289 268
   render() {
269
+    const LABELS: {
270
+      [key: string]: string;
271
+    } = {
272
+      services: i18n.t('screens.services.title'),
273
+      proxiwash: i18n.t('screens.proxiwash.title'),
274
+      home: i18n.t('screens.home.title'),
275
+      planning: i18n.t('screens.planning.title'),
276
+      planex: i18n.t('screens.planex.title'),
277
+    };
290 278
     return (
291 279
       <Tab.Navigator
292 280
         initialRouteName={this.defaultRoute}
293
-        tabBar={(tabProps) => <CustomTabBar {...tabProps} />}
281
+        tabBar={(tabProps) => (
282
+          <CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
283
+        )}
294 284
       >
295 285
         <Tab.Screen
296
-          name="services"
286
+          name={'services'}
297 287
           component={ServicesStackComponent}
298 288
           options={{ title: i18n.t('screens.services.title') }}
299 289
         />
300 290
         <Tab.Screen
301
-          name="proxiwash"
291
+          name={'proxiwash'}
302 292
           component={ProxiwashStackComponent}
303 293
           options={{ title: i18n.t('screens.proxiwash.title') }}
304 294
         />
305 295
         <Tab.Screen
306
-          name="home"
296
+          name={'home'}
307 297
           component={this.createHomeStackComponent}
308 298
           options={{ title: i18n.t('screens.home.title') }}
309 299
         />
310 300
         <Tab.Screen
311
-          name="planning"
301
+          name={'planning'}
312 302
           component={PlanningStackComponent}
313 303
           options={{ title: i18n.t('screens.planning.title') }}
314 304
         />
315 305
         <Tab.Screen
316
-          name="planex"
306
+          name={'planex'}
317 307
           component={PlanexStackComponent}
318 308
           options={{ title: i18n.t('screens.planex.title') }}
319 309
         />

+ 4
- 2
src/screens/Amicale/Clubs/ClubDisplayScreen.tsx View File

@@ -31,7 +31,9 @@ import i18n from 'i18n-js';
31 31
 import { StackNavigationProp } from '@react-navigation/stack';
32 32
 import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
33 33
 import CustomHTML from '../../../components/Overrides/CustomHTML';
34
-import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
34
+import CustomTabBar, {
35
+  TAB_BAR_HEIGHT,
36
+} from '../../../components/Tabbar/CustomTabBar';
35 37
 import type { ClubCategoryType, ClubType } from './ClubListScreen';
36 38
 import { ERROR_TYPE } from '../../../utils/WebData';
37 39
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
@@ -174,7 +176,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
174 176
     return (
175 177
       <Card
176 178
         style={{
177
-          marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20,
179
+          marginBottom: TAB_BAR_HEIGHT + 20,
178 180
           ...styles.card,
179 181
         }}
180 182
       >

+ 1
- 1
src/screens/Amicale/LoginScreen.tsx View File

@@ -441,7 +441,7 @@ class LoginScreen extends React.Component<Props, StateType> {
441 441
           enabled
442 442
           keyboardVerticalOffset={100}
443 443
         >
444
-          <CollapsibleScrollView>
444
+          <CollapsibleScrollView headerColors={'transparent'}>
445 445
             <View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
446 446
             <MascotPopup
447 447
               visible={mascotDialogVisible}

+ 4
- 4
src/screens/Home/FeedItemScreen.tsx View File

@@ -25,7 +25,9 @@ import { StackNavigationProp } from '@react-navigation/stack';
25 25
 import MaterialHeaderButtons, {
26 26
   Item,
27 27
 } from '../../components/Overrides/CustomHeaderButton';
28
-import CustomTabBar from '../../components/Tabbar/CustomTabBar';
28
+import CustomTabBar, {
29
+  TAB_BAR_HEIGHT,
30
+} from '../../components/Tabbar/CustomTabBar';
29 31
 import type { FeedItemType } from './HomeScreen';
30 32
 import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
31 33
 import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
@@ -117,9 +119,7 @@ class FeedItemScreen extends React.Component<PropsType> {
117 119
             style={styles.button}
118 120
           />
119 121
         ) : null}
120
-        <Card.Content
121
-          style={{ paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20 }}
122
-        >
122
+        <Card.Content style={{ paddingBottom: TAB_BAR_HEIGHT + 20 }}>
123 123
           {this.displayData.message !== undefined ? (
124 124
             <Autolink
125 125
               text={this.displayData.message}

+ 4
- 2
src/screens/Home/ScannerScreen.tsx View File

@@ -26,7 +26,9 @@ import i18n from 'i18n-js';
26 26
 import { PERMISSIONS, request, RESULTS } from 'react-native-permissions';
27 27
 import URLHandler from '../../utils/URLHandler';
28 28
 import AlertDialog from '../../components/Dialogs/AlertDialog';
29
-import CustomTabBar from '../../components/Tabbar/CustomTabBar';
29
+import CustomTabBar, {
30
+  TAB_BAR_HEIGHT,
31
+} from '../../components/Tabbar/CustomTabBar';
30 32
 import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
31 33
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
32 34
 import MascotPopup from '../../components/Mascot/MascotPopup';
@@ -218,7 +220,7 @@ class ScannerScreen extends React.Component<{}, StateType> {
218 220
       <View
219 221
         style={{
220 222
           ...styles.container,
221
-          marginBottom: CustomTabBar.TAB_BAR_HEIGHT,
223
+          marginBottom: TAB_BAR_HEIGHT,
222 224
         }}
223 225
       >
224 226
         {state.hasPermission ? this.getScanner() : this.getPermissionScreen()}

+ 23
- 35
src/screens/Planex/PlanexScreen.tsx View File

@@ -54,6 +54,7 @@ type StateType = {
54 54
   dialogTitle: string | React.ReactNode;
55 55
   dialogMessage: string;
56 56
   currentGroup: PlanexGroupType;
57
+  injectJS: string;
57 58
 };
58 59
 
59 60
 const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
@@ -153,20 +154,14 @@ const styles = StyleSheet.create({
153 154
  * This screen uses a webview to render the page
154 155
  */
155 156
 class PlanexScreen extends React.Component<PropsType, StateType> {
156
-  webScreenRef: { current: null | WebViewScreen };
157
-
158 157
   barRef: { current: null | AnimatedBottomBar };
159 158
 
160
-  customInjectedJS: string;
161
-
162 159
   /**
163 160
    * Defines custom injected JavaScript to improve the page display on mobile
164 161
    */
165 162
   constructor(props: PropsType) {
166 163
     super(props);
167
-    this.webScreenRef = React.createRef();
168 164
     this.barRef = React.createRef();
169
-    this.customInjectedJS = '';
170 165
     let currentGroupString = AsyncStorageManager.getString(
171 166
       AsyncStorageManager.PREFERENCES.planexCurrentGroup.key
172 167
     );
@@ -184,8 +179,8 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
184 179
       dialogTitle: '',
185 180
       dialogMessage: '',
186 181
       currentGroup,
182
+      injectJS: '',
187 183
     };
188
-    this.generateInjectedJS(currentGroup.id);
189 184
   }
190 185
 
191 186
   /**
@@ -197,20 +192,6 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
197 192
   }
198 193
 
199 194
   /**
200
-   * Only update the screen if the dark theme changed
201
-   *
202
-   * @param nextProps
203
-   * @returns {boolean}
204
-   */
205
-  shouldComponentUpdate(nextProps: PropsType): boolean {
206
-    const { props, state } = this;
207
-    if (nextProps.theme.dark !== props.theme.dark) {
208
-      this.generateInjectedJS(state.currentGroup.id);
209
-    }
210
-    return true;
211
-  }
212
-
213
-  /**
214 195
    * Gets the Webview, with an error view on top if no group is selected.
215 196
    *
216 197
    * @returns {*}
@@ -218,6 +199,7 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
218 199
   getWebView() {
219 200
     const { props, state } = this;
220 201
     const showWebview = state.currentGroup.id !== -1;
202
+    console.log(state.injectJS);
221 203
 
222 204
     return (
223 205
       <View style={GENERAL_STYLES.flex}>
@@ -230,10 +212,9 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
230 212
           />
231 213
         ) : null}
232 214
         <WebViewScreen
233
-          ref={this.webScreenRef}
234
-          navigation={props.navigation}
235 215
           url={PLANEX_URL}
236
-          customJS={this.customInjectedJS}
216
+          initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
217
+          injectJS={this.state.injectJS}
237 218
           onMessage={this.onMessage}
238 219
           onScroll={this.onScroll}
239 220
           showAdvancedControls={false}
@@ -269,9 +250,13 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
269 250
     } else {
270 251
       command = `$('#calendar').fullCalendar('${action}', '${data}')`;
271 252
     }
272
-    if (this.webScreenRef.current != null) {
273
-      this.webScreenRef.current.injectJavaScript(`${command};true;`);
274
-    } // Injected javascript must end with true
253
+    // String must resolve to true to prevent crash on iOS
254
+    command += ';true;';
255