Browse Source

Use context to handle preferences

This is not tested, expect crashes
Arnaud Vergnet 2 years ago
parent
commit
00f9428972
39 changed files with 1243 additions and 1954 deletions
  1. 16
    114
      App.tsx
  2. 1
    1
      src/components/Lists/CardList/CardList.tsx
  3. 1
    1
      src/components/Lists/CardList/CardListItem.tsx
  4. 1
    1
      src/components/Lists/CardList/ImageListItem.tsx
  5. 1
    4
      src/components/Lists/DashboardEdit/DashboardEditAccordion.tsx
  6. 1
    1
      src/components/Lists/DashboardEdit/DashboardEditItem.tsx
  7. 8
    9
      src/components/Mascot/MascotPopup.tsx
  8. 0
    4
      src/components/Overrides/CustomIntroSlider.tsx
  9. 8
    4
      src/components/Screens/PlanexWebview.tsx
  10. 0
    25
      src/context/preferencesContext.ts
  11. 97
    0
      src/context/preferencesContext.tsx
  12. 0
    269
      src/managers/AsyncStorageManager.ts
  13. 0
    38
      src/managers/DashboardManager.ts
  14. 0
    371
      src/managers/ServicesManager.ts
  15. 31
    5
      src/navigation/MainNavigator.tsx
  16. 81
    61
      src/navigation/TabNavigator.tsx
  17. 62
    122
      src/screens/About/DebugScreen.tsx
  18. 2
    9
      src/screens/Amicale/Equipment/EquipmentListScreen.tsx
  19. 7
    13
      src/screens/Amicale/LoginScreen.tsx
  20. 8
    4
      src/screens/Amicale/ProfileScreen.tsx
  21. 2
    9
      src/screens/Amicale/VoteScreen.tsx
  22. 2
    1
      src/screens/Game/screens/GameMainScreen.tsx
  23. 10
    9
      src/screens/Game/screens/GameStartScreen.tsx
  24. 170
    238
      src/screens/Home/HomeScreen.tsx
  25. 41
    0
      src/screens/Intro/IntroScreen.tsx
  26. 61
    0
      src/screens/MainApp.tsx
  27. 47
    86
      src/screens/Other/Settings/DashboardEditScreen.tsx
  28. 170
    252
      src/screens/Other/Settings/SettingsScreen.tsx
  29. 27
    26
      src/screens/Planex/GroupSelectionScreen.tsx
  30. 27
    52
      src/screens/Planex/PlanexScreen.tsx
  31. 0
    2
      src/screens/Planning/PlanningScreen.tsx
  32. 35
    32
      src/screens/Proxiwash/ProxiwashScreen.tsx
  33. 6
    8
      src/screens/Services/ServicesScreen.tsx
  34. 1
    1
      src/screens/Services/ServicesSectionScreen.tsx
  35. 4
    7
      src/utils/Notifications.ts
  36. 294
    38
      src/utils/Services.ts
  37. 2
    122
      src/utils/Themes.ts
  38. 10
    12
      src/utils/Utils.ts
  39. 9
    3
      src/utils/asyncStorage.ts

+ 16
- 114
App.tsx View File

@@ -18,27 +18,21 @@
18 18
  */
19 19
 
20 20
 import React from 'react';
21
-import { LogBox, Platform, SafeAreaView, View } from 'react-native';
22
-import { NavigationContainer } from '@react-navigation/native';
23
-import { Provider as PaperProvider } from 'react-native-paper';
21
+import { LogBox, Platform } from 'react-native';
24 22
 import { setSafeBounceHeight } from 'react-navigation-collapsible';
25 23
 import SplashScreen from 'react-native-splash-screen';
26
-import { OverflowMenuProvider } from 'react-navigation-header-buttons';
27
-import AsyncStorageManager from './src/managers/AsyncStorageManager';
28
-import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
29
-import ThemeManager from './src/managers/ThemeManager';
30
-import MainNavigator from './src/navigation/MainNavigator';
31
-import AprilFoolsManager from './src/managers/AprilFoolsManager';
32
-import Update from './src/constants/Update';
33 24
 import ConnectionManager from './src/managers/ConnectionManager';
34 25
 import type { ParsedUrlDataType } from './src/utils/URLHandler';
35 26
 import URLHandler from './src/utils/URLHandler';
36
-import { setupStatusBar } from './src/utils/Utils';
37 27
 import initLocales from './src/utils/Locales';
38 28
 import { NavigationContainerRef } from '@react-navigation/core';
39
-import GENERAL_STYLES from './src/constants/Styles';
40
-import CollapsibleProvider from './src/components/providers/CollapsibleProvider';
41
-import CacheProvider from './src/components/providers/CacheProvider';
29
+import {
30
+  defaultPreferences,
31
+  PreferenceKeys,
32
+  retrievePreferences,
33
+} from './src/utils/asyncStorage';
34
+import PreferencesProvider from './src/components/providers/PreferencesProvider';
35
+import MainApp from './src/screens/MainApp';
42 36
 
43 37
 // Native optimizations https://reactnavigation.org/docs/react-native-screens
44 38
 // Crashes app when navigating away from webview on android 9+
@@ -52,10 +46,6 @@ LogBox.ignoreLogs([
52 46
 
53 47
 type StateType = {
54 48
   isLoading: boolean;
55
-  showIntro: boolean;
56
-  showUpdate: boolean;
57
-  showAprilFools: boolean;
58
-  currentTheme: ReactNativePaper.Theme | undefined;
59 49
 };
60 50
 
61 51
 export default class App extends React.Component<{}, StateType> {
@@ -71,10 +61,6 @@ export default class App extends React.Component<{}, StateType> {
71 61
     super(props);
72 62
     this.state = {
73 63
       isLoading: true,
74
-      showIntro: true,
75
-      showUpdate: true,
76
-      showAprilFools: false,
77
-      currentTheme: undefined,
78 64
     };
79 65
     initLocales();
80 66
     this.navigatorRef = React.createRef();
@@ -115,66 +101,11 @@ export default class App extends React.Component<{}, StateType> {
115 101
   };
116 102
 
117 103
   /**
118
-   * Updates the current theme
119
-   */
120
-  onUpdateTheme = () => {
121
-    this.setState({
122
-      currentTheme: ThemeManager.getCurrentTheme(),
123
-    });
124
-    setupStatusBar();
125
-  };
126
-
127
-  /**
128
-   * Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
129
-   */
130
-  onIntroDone = () => {
131
-    this.setState({
132
-      showIntro: false,
133
-      showUpdate: false,
134
-      showAprilFools: false,
135
-    });
136
-    AsyncStorageManager.set(
137
-      AsyncStorageManager.PREFERENCES.showIntro.key,
138
-      false
139
-    );
140
-    AsyncStorageManager.set(
141
-      AsyncStorageManager.PREFERENCES.updateNumber.key,
142
-      Update.number
143
-    );
144
-    AsyncStorageManager.set(
145
-      AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
146
-      false
147
-    );
148
-  };
149
-
150
-  /**
151 104
    * Async loading is done, finish processing startup data
152 105
    */
153 106
   onLoadFinished = () => {
154
-    // Only show intro if this is the first time starting the app
155
-    ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
156
-    // Status bar goes dark if set too fast on ios
157
-    if (Platform.OS === 'ios') {
158
-      setTimeout(setupStatusBar, 1000);
159
-    } else {
160
-      setupStatusBar();
161
-    }
162
-
163 107
     this.setState({
164 108
       isLoading: false,
165
-      currentTheme: ThemeManager.getCurrentTheme(),
166
-      showIntro: AsyncStorageManager.getBool(
167
-        AsyncStorageManager.PREFERENCES.showIntro.key
168
-      ),
169
-      showUpdate:
170
-        AsyncStorageManager.getNumber(
171
-          AsyncStorageManager.PREFERENCES.updateNumber.key
172
-        ) !== Update.number,
173
-      showAprilFools:
174
-        AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
175
-        AsyncStorageManager.getBool(
176
-          AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key
177
-        ),
178 109
     });
179 110
     SplashScreen.hide();
180 111
   };
@@ -186,7 +117,7 @@ export default class App extends React.Component<{}, StateType> {
186 117
    */
187 118
   loadAssetsAsync() {
188 119
     Promise.all([
189
-      AsyncStorageManager.getInstance().loadPreferences(),
120
+      retrievePreferences(Object.values(PreferenceKeys), defaultPreferences),
190 121
       ConnectionManager.getInstance().recoverLogin(),
191 122
     ])
192 123
       .then(this.onLoadFinished)
@@ -201,43 +132,14 @@ export default class App extends React.Component<{}, StateType> {
201 132
     if (state.isLoading) {
202 133
       return null;
203 134
     }
204
-    if (state.showIntro || state.showUpdate || state.showAprilFools) {
205
-      return (
206
-        <CustomIntroSlider
207
-          onDone={this.onIntroDone}
208
-          isUpdate={state.showUpdate && !state.showIntro}
209
-          isAprilFools={state.showAprilFools && !state.showIntro}
210
-        />
211
-      );
212
-    }
213 135
     return (
214
-      <PaperProvider theme={state.currentTheme}>
215
-        <CollapsibleProvider>
216
-          <CacheProvider>
217
-            <OverflowMenuProvider>
218
-              <View
219
-                style={{
220
-                  backgroundColor: ThemeManager.getCurrentTheme().colors
221
-                    .background,
222
-                  ...GENERAL_STYLES.flex,
223
-                }}
224
-              >
225
-                <SafeAreaView style={GENERAL_STYLES.flex}>
226
-                  <NavigationContainer
227
-                    theme={state.currentTheme}
228
-                    ref={this.navigatorRef}
229
-                  >
230
-                    <MainNavigator
231
-                      defaultHomeRoute={this.defaultHomeRoute}
232
-                      defaultHomeData={this.defaultHomeData}
233
-                    />
234
-                  </NavigationContainer>
235
-                </SafeAreaView>
236
-              </View>
237
-            </OverflowMenuProvider>
238
-          </CacheProvider>
239
-        </CollapsibleProvider>
240
-      </PaperProvider>
136
+      <PreferencesProvider initialPreferences={defaultPreferences}>
137
+        <MainApp
138
+          ref={this.navigatorRef}
139
+          defaultHomeData={this.defaultHomeData}
140
+          defaultHomeRoute={this.defaultHomeRoute}
141
+        />
142
+      </PreferencesProvider>
241 143
     );
242 144
   }
243 145
 }

+ 1
- 1
src/components/Lists/CardList/CardList.tsx View File

@@ -21,7 +21,7 @@ import * as React from 'react';
21 21
 import { Animated, Dimensions, ViewStyle } from 'react-native';
22 22
 import ImageListItem from './ImageListItem';
23 23
 import CardListItem from './CardListItem';
24
-import type { ServiceItemType } from '../../../managers/ServicesManager';
24
+import { ServiceItemType } from '../../../utils/Services';
25 25
 
26 26
 type PropsType = {
27 27
   dataset: Array<ServiceItemType>;

+ 1
- 1
src/components/Lists/CardList/CardListItem.tsx View File

@@ -20,8 +20,8 @@
20 20
 import * as React from 'react';
21 21
 import { Caption, Card, Paragraph, TouchableRipple } from 'react-native-paper';
22 22
 import { StyleSheet, View } from 'react-native';
23
-import type { ServiceItemType } from '../../../managers/ServicesManager';
24 23
 import GENERAL_STYLES from '../../../constants/Styles';
24
+import { ServiceItemType } from '../../../utils/Services';
25 25
 
26 26
 type PropsType = {
27 27
   item: ServiceItemType;

+ 1
- 1
src/components/Lists/CardList/ImageListItem.tsx View File

@@ -20,8 +20,8 @@
20 20
 import * as React from 'react';
21 21
 import { Text, TouchableRipple } from 'react-native-paper';
22 22
 import { Image, StyleSheet, View } from 'react-native';
23
-import type { ServiceItemType } from '../../../managers/ServicesManager';
24 23
 import GENERAL_STYLES from '../../../constants/Styles';
24
+import { ServiceItemType } from '../../../utils/Services';
25 25
 
26 26
 type PropsType = {
27 27
   item: ServiceItemType;

+ 1
- 4
src/components/Lists/DashboardEdit/DashboardEditAccordion.tsx View File

@@ -23,10 +23,7 @@ import { FlatList, Image, StyleSheet, View } from 'react-native';
23 23
 import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
24 24
 import DashboardEditItem from './DashboardEditItem';
25 25
 import AnimatedAccordion from '../../Animations/AnimatedAccordion';
26
-import type {
27
-  ServiceCategoryType,
28
-  ServiceItemType,
29
-} from '../../../managers/ServicesManager';
26
+import { ServiceCategoryType, ServiceItemType } from '../../../utils/Services';
30 27
 
31 28
 type PropsType = {
32 29
   item: ServiceCategoryType;

+ 1
- 1
src/components/Lists/DashboardEdit/DashboardEditItem.tsx View File

@@ -20,7 +20,7 @@
20 20
 import * as React from 'react';
21 21
 import { Image, StyleSheet } from 'react-native';
22 22
 import { List, useTheme } from 'react-native-paper';
23
-import type { ServiceItemType } from '../../../managers/ServicesManager';
23
+import { ServiceItemType } from '../../../utils/Services';
24 24
 
25 25
 type PropsType = {
26 26
   item: ServiceItemType;

+ 8
- 9
src/components/Mascot/MascotPopup.tsx View File

@@ -28,17 +28,17 @@ import {
28 28
   View,
29 29
 } from 'react-native';
30 30
 import Mascot from './Mascot';
31
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
32 31
 import GENERAL_STYLES from '../../constants/Styles';
33 32
 import MascotSpeechBubble, {
34 33
   MascotSpeechBubbleProps,
35 34
 } from './MascotSpeechBubble';
36 35
 import { useMountEffect } from '../../utils/customHooks';
36
+import { useRoute } from '@react-navigation/core';
37
+import { useShouldShowMascot } from '../../context/preferencesContext';
37 38
 
38 39
 type PropsType = MascotSpeechBubbleProps & {
39 40
   emotion: number;
40 41
   visible?: boolean;
41
-  prefKey?: string;
42 42
 };
43 43
 
44 44
 const styles = StyleSheet.create({
@@ -61,13 +61,14 @@ const BUBBLE_HEIGHT = Dimensions.get('window').height / 3;
61 61
  * Component used to display a popup with the mascot.
62 62
  */
63 63
 function MascotPopup(props: PropsType) {
64
+  const route = useRoute();
65
+  const { shouldShow, setShouldShow } = useShouldShowMascot(route.name);
66
+
64 67
   const isVisible = () => {
65 68
     if (props.visible !== undefined) {
66 69
       return props.visible;
67
-    } else if (props.prefKey != null) {
68
-      return AsyncStorageManager.getBool(props.prefKey);
69 70
     } else {
70
-      return false;
71
+      return shouldShow;
71 72
     }
72 73
   };
73 74
 
@@ -164,10 +165,8 @@ function MascotPopup(props: PropsType) {
164 165
   };
165 166
 
166 167
   const onDismiss = (callback?: () => void) => {
167
-    if (props.prefKey != null) {
168
-      AsyncStorageManager.set(props.prefKey, false);
169
-      setDialogVisible(false);
170
-    }
168
+    setShouldShow(false);
169
+    setDialogVisible(false);
171 170
     if (callback) {
172 171
       callback();
173 172
     }

+ 0
- 4
src/components/Overrides/CustomIntroSlider.tsx View File

@@ -32,7 +32,6 @@ import LinearGradient from 'react-native-linear-gradient';
32 32
 import * as Animatable from 'react-native-animatable';
33 33
 import { Card } from 'react-native-paper';
34 34
 import Update from '../../constants/Update';
35
-import ThemeManager from '../../managers/ThemeManager';
36 35
 import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
37 36
 import MascotIntroWelcome from '../Intro/MascotIntroWelcome';
38 37
 import IntroIcon from '../Intro/IconIntro';
@@ -289,9 +288,6 @@ export default class CustomIntroSlider extends React.Component<
289 288
 
290 289
   onDone = () => {
291 290
     const { props } = this;
292
-    CustomIntroSlider.setStatusBarColor(
293
-      ThemeManager.getCurrentTheme().colors.surface
294
-    );
295 291
     props.onDone();
296 292
   };
297 293
 

+ 8
- 4
src/components/Screens/PlanexWebview.tsx View File

@@ -3,11 +3,11 @@ import { StyleSheet, View } from 'react-native';
3 3
 import GENERAL_STYLES from '../../constants/Styles';
4 4
 import Urls from '../../constants/Urls';
5 5
 import DateManager from '../../managers/DateManager';
6
-import ThemeManager from '../../managers/ThemeManager';
7 6
 import { PlanexGroupType } from '../../screens/Planex/GroupSelectionScreen';
8 7
 import ErrorView from './ErrorView';
9 8
 import WebViewScreen from './WebViewScreen';
10 9
 import i18n from 'i18n-js';
10
+import { useTheme } from 'react-native-paper';
11 11
 
12 12
 type Props = {
13 13
   currentGroup?: PlanexGroupType;
@@ -86,7 +86,10 @@ const INJECT_STYLE_DARK = `$('head').append('<style>${CUSTOM_CSS_DARK}</style>')
86 86
  *
87 87
  * @param groupID The current group selected
88 88
  */
89
-const generateInjectedJS = (group: PlanexGroupType | undefined) => {
89
+const generateInjectedJS = (
90
+  group: PlanexGroupType | undefined,
91
+  darkMode: boolean
92
+) => {
90 93
   let customInjectedJS = `$(document).ready(function() {
91 94
       ${OBSERVE_MUTATIONS_INJECTED}
92 95
       ${INJECT_STYLE}
@@ -97,7 +100,7 @@ const generateInjectedJS = (group: PlanexGroupType | undefined) => {
97 100
   if (DateManager.isWeekend(new Date())) {
98 101
     customInjectedJS += `calendar.next();`;
99 102
   }
100
-  if (ThemeManager.getNightMode()) {
103
+  if (darkMode) {
101 104
     customInjectedJS += INJECT_STYLE_DARK;
102 105
   }
103 106
   customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
@@ -105,11 +108,12 @@ const generateInjectedJS = (group: PlanexGroupType | undefined) => {
105 108
 };
106 109
 
107 110
 function PlanexWebview(props: Props) {
111
+  const theme = useTheme();
108 112
   return (
109 113
     <View style={GENERAL_STYLES.flex}>
110 114
       <WebViewScreen
111 115
         url={Urls.planex.planning}
112
-        initialJS={generateInjectedJS(props.currentGroup)}
116
+        initialJS={generateInjectedJS(props.currentGroup, theme.dark)}
113 117
         injectJS={props.injectJS}
114 118
         onMessage={props.onMessage}
115 119
         showAdvancedControls={false}

+ 0
- 25
src/context/preferencesContext.ts View File

@@ -1,25 +0,0 @@
1
-import React, { useContext } from 'react';
2
-import {
3
-  defaultPreferences,
4
-  PreferenceKeys,
5
-  PreferencesType,
6
-} from '../utils/asyncStorage';
7
-
8
-export type PreferencesContextType = {
9
-  preferences: PreferencesType;
10
-  updatePreferences: (
11
-    key: PreferenceKeys,
12
-    value: number | string | boolean | object | Array<any>
13
-  ) => void;
14
-  resetPreferences: () => void;
15
-};
16
-
17
-export const PreferencesContext = React.createContext<PreferencesContextType>({
18
-  preferences: defaultPreferences,
19
-  updatePreferences: () => undefined,
20
-  resetPreferences: () => undefined,
21
-});
22
-
23
-export function usePreferences() {
24
-  return useContext(PreferencesContext);
25
-}

+ 97
- 0
src/context/preferencesContext.tsx View File

@@ -0,0 +1,97 @@
1
+import { useNavigation } from '@react-navigation/core';
2
+import React, { useContext } from 'react';
3
+import { Appearance } from 'react-native-appearance';
4
+import {
5
+  defaultPreferences,
6
+  getPreferenceBool,
7
+  getPreferenceObject,
8
+  isValidPreferenceKey,
9
+  PreferenceKeys,
10
+  PreferencesType,
11
+} from '../utils/asyncStorage';
12
+import {
13
+  getAmicaleServices,
14
+  getINSAServices,
15
+  getSpecialServices,
16
+  getStudentServices,
17
+} from '../utils/Services';
18
+
19
+const colorScheme = Appearance.getColorScheme();
20
+
21
+export type PreferencesContextType = {
22
+  preferences: PreferencesType;
23
+  updatePreferences: (
24
+    key: PreferenceKeys,
25
+    value: number | string | boolean | object | Array<any>
26
+  ) => void;
27
+  resetPreferences: () => void;
28
+};
29
+
30
+export const PreferencesContext = React.createContext<PreferencesContextType>({
31
+  preferences: defaultPreferences,
32
+  updatePreferences: () => undefined,
33
+  resetPreferences: () => undefined,
34
+});
35
+
36
+export function usePreferences() {
37
+  return useContext(PreferencesContext);
38
+}
39
+
40
+export function useShouldShowMascot(route: string) {
41
+  const { preferences, updatePreferences } = usePreferences();
42
+  const key = route + 'ShowMascot';
43
+  let shouldShow = false;
44
+  if (isValidPreferenceKey(key)) {
45
+    shouldShow = getPreferenceBool(key, preferences) !== false;
46
+  }
47
+
48
+  const setShouldShow = (show: boolean) => {
49
+    if (isValidPreferenceKey(key)) {
50
+      updatePreferences(key, show);
51
+    } else {
52
+      console.log('Invalid preference key: ' + key);
53
+    }
54
+  };
55
+
56
+  return { shouldShow, setShouldShow };
57
+}
58
+
59
+export function useDarkTheme() {
60
+  const { preferences } = usePreferences();
61
+  return (
62
+    (getPreferenceBool(PreferenceKeys.nightMode, preferences) !== false &&
63
+      (getPreferenceBool(PreferenceKeys.nightModeFollowSystem, preferences) ===
64
+        false ||
65
+        colorScheme === 'no-preference')) ||
66
+    (getPreferenceBool(PreferenceKeys.nightModeFollowSystem, preferences) !==
67
+      false &&
68
+      colorScheme === 'dark')
69
+  );
70
+}
71
+
72
+export function useCurrentDashboard() {
73
+  const { preferences, updatePreferences } = usePreferences();
74
+  const navigation = useNavigation();
75
+  const dashboardIdList = getPreferenceObject(
76
+    PreferenceKeys.dashboardItems,
77
+    preferences
78
+  ) as Array<string>;
79
+
80
+  const updateCurrentDashboard = (newList: Array<string>) => {
81
+    updatePreferences(PreferenceKeys.dashboardItems, newList);
82
+  };
83
+
84
+  const allDatasets = [
85
+    ...getAmicaleServices(navigation.navigate),
86
+    ...getStudentServices(navigation.navigate),
87
+    ...getINSAServices(navigation.navigate),
88
+    ...getSpecialServices(navigation.navigate),
89
+  ];
90
+  return {
91
+    currentDashboard: allDatasets.filter((item) =>
92
+      dashboardIdList.includes(item.key)
93
+    ),
94
+    currentDashboardIdList: dashboardIdList,
95
+    updateCurrentDashboard: updateCurrentDashboard,
96
+  };
97
+}

+ 0
- 269
src/managers/AsyncStorageManager.ts View File

@@ -1,269 +0,0 @@
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 AsyncStorage from '@react-native-async-storage/async-storage';
21
-import { SERVICES_KEY } from './ServicesManager';
22
-
23
-/**
24
- * Singleton used to manage preferences.
25
- * Preferences are fetched at the start of the app and saved in an instance object.
26
- * This allows for a synchronous access to saved data.
27
- */
28
-
29
-export default class AsyncStorageManager {
30
-  static instance: AsyncStorageManager | null = null;
31
-
32
-  static PREFERENCES: { [key: string]: { key: string; default: string } } = {
33
-    debugUnlocked: {
34
-      key: 'debugUnlocked',
35
-      default: '0',
36
-    },
37
-    showIntro: {
38
-      key: 'showIntro',
39
-      default: '1',
40
-    },
41
-    updateNumber: {
42
-      key: 'updateNumber',
43
-      default: '0',
44
-    },
45
-    proxiwashNotifications: {
46
-      key: 'proxiwashNotifications',
47
-      default: '5',
48
-    },
49
-    nightModeFollowSystem: {
50
-      key: 'nightModeFollowSystem',
51
-      default: '1',
52
-    },
53
-    nightMode: {
54
-      key: 'nightMode',
55
-      default: '1',
56
-    },
57
-    defaultStartScreen: {
58
-      key: 'defaultStartScreen',
59
-      default: 'home',
60
-    },
61
-    servicesShowMascot: {
62
-      key: 'servicesShowMascot',
63
-      default: '1',
64
-    },
65
-    proxiwashShowMascot: {
66
-      key: 'proxiwashShowMascot',
67
-      default: '1',
68
-    },
69
-    homeShowMascot: {
70
-      key: 'homeShowMascot',
71
-      default: '1',
72
-    },
73
-    eventsShowMascot: {
74
-      key: 'eventsShowMascot',
75
-      default: '1',
76
-    },
77
-    planexShowMascot: {
78
-      key: 'planexShowMascot',
79
-      default: '1',
80
-    },
81
-    loginShowMascot: {
82
-      key: 'loginShowMascot',
83
-      default: '1',
84
-    },
85
-    voteShowMascot: {
86
-      key: 'voteShowMascot',
87
-      default: '1',
88
-    },
89
-    equipmentShowMascot: {
90
-      key: 'equipmentShowMascot',
91
-      default: '1',
92
-    },
93
-    gameStartMascot: {
94
-      key: 'gameStartMascot',
95
-      default: '1',
96
-    },
97
-    proxiwashWatchedMachines: {
98
-      key: 'proxiwashWatchedMachines',
99
-      default: '[]',
100
-    },
101
-    showAprilFoolsStart: {
102
-      key: 'showAprilFoolsStart',
103
-      default: '1',
104
-    },
105
-    planexCurrentGroup: {
106
-      key: 'planexCurrentGroup',
107
-      default: '',
108
-    },
109
-    planexFavoriteGroups: {
110
-      key: 'planexFavoriteGroups',
111
-      default: '[]',
112
-    },
113
-    dashboardItems: {
114
-      key: 'dashboardItems',
115
-      default: JSON.stringify([
116
-        SERVICES_KEY.EMAIL,
117
-        SERVICES_KEY.WASHERS,
118
-        SERVICES_KEY.PROXIMO,
119
-        SERVICES_KEY.TUTOR_INSA,
120
-        SERVICES_KEY.RU,
121
-      ]),
122
-    },
123
-    gameScores: {
124
-      key: 'gameScores',
125
-      default: '[]',
126
-    },
127
-    selectedWash: {
128
-      key: 'selectedWash',
129
-      default: 'washinsa',
130
-    },
131
-  };
132
-
133
-  private currentPreferences: { [key: string]: string };
134
-
135
-  constructor() {
136
-    this.currentPreferences = {};
137
-  }
138
-
139
-  /**
140
-   * Get this class instance or create one if none is found
141
-   * @returns {AsyncStorageManager}
142
-   */
143
-  static getInstance(): AsyncStorageManager {
144
-    if (AsyncStorageManager.instance == null) {
145
-      AsyncStorageManager.instance = new AsyncStorageManager();
146
-    }
147
-    return AsyncStorageManager.instance;
148
-  }
149
-
150
-  /**
151
-   * Saves the value associated to the given key to preferences.
152
-   *
153
-   * @param key
154
-   * @param value
155
-   */
156
-  static set(
157
-    key: string,
158
-    value: number | string | boolean | object | Array<any>
159
-  ) {
160
-    AsyncStorageManager.getInstance().setPreference(key, value);
161
-  }
162
-
163
-  /**
164
-   * Gets the string value of the given preference
165
-   *
166
-   * @param key
167
-   * @returns {string}
168
-   */
169
-  static getString(key: string): string {
170
-    const value = AsyncStorageManager.getInstance().getPreference(key);
171
-    return value != null ? value : '';
172
-  }
173
-
174
-  /**
175
-   * Gets the boolean value of the given preference
176
-   *
177
-   * @param key
178
-   * @returns {boolean}
179
-   */
180
-  static getBool(key: string): boolean {
181
-    const value = AsyncStorageManager.getString(key);
182
-    return value === '1' || value === 'true';
183
-  }
184
-
185
-  /**
186
-   * Gets the number value of the given preference
187
-   *
188
-   * @param key
189
-   * @returns {number}
190
-   */
191
-  static getNumber(key: string): number {
192
-    return parseFloat(AsyncStorageManager.getString(key));
193
-  }
194
-
195
-  /**
196
-   * Gets the object value of the given preference
197
-   *
198
-   * @param key
199
-   * @returns {{...}}
200
-   */
201
-  static getObject<T>(key: string): T {
202
-    return JSON.parse(AsyncStorageManager.getString(key));
203
-  }
204
-
205
-  /**
206
-   * Set preferences object current values from AsyncStorage.
207
-   * This function should be called at the app's start.
208
-   *
209
-   * @return {Promise<void>}
210
-   */
211
-  async loadPreferences() {
212
-    return new Promise((resolve: (val: void) => void) => {
213
-      const prefKeys: Array<string> = [];
214
-      // Get all available keys
215
-      Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => {
216
-        prefKeys.push(key);
217
-      });
218
-      // Get corresponding values
219
-      AsyncStorage.multiGet(prefKeys).then((resultArray) => {
220
-        // Save those values for later use
221
-        resultArray.forEach((item: [string, string | null]) => {
222
-          const key = item[0];
223
-          let val = item[1];
224
-          if (val === null) {
225
-            val = AsyncStorageManager.PREFERENCES[key].default;
226
-          }
227
-          this.currentPreferences[key] = val;
228
-        });
229
-        resolve();
230
-      });
231
-    });
232
-  }
233
-
234
-  /**
235
-   * Saves the value associated to the given key to preferences.
236
-   * This updates the preferences object and saves it to AsyncStorage.
237
-   *
238
-   * @param key
239
-   * @param value
240
-   */
241
-  setPreference(
242
-    key: string,
243
-    value: number | string | boolean | object | Array<any>
244
-  ) {
245
-    if (AsyncStorageManager.PREFERENCES[key] != null) {
246
-      let convertedValue;
247
-      if (typeof value === 'string') {
248
-        convertedValue = value;
249
-      } else if (typeof value === 'boolean' || typeof value === 'number') {
250
-        convertedValue = value.toString();
251
-      } else {
252
-        convertedValue = JSON.stringify(value);
253
-      }
254
-      this.currentPreferences[key] = convertedValue;
255
-      AsyncStorage.setItem(key, convertedValue);
256
-    }
257
-  }
258
-
259
-  /**
260
-   * Gets the value at the given key.
261
-   * If the key is not available, returns null
262
-   *
263
-   * @param key
264
-   * @returns {string|null}
265
-   */
266
-  getPreference(key: string): string | null {
267
-    return this.currentPreferences[key];
268
-  }
269
-}

+ 0
- 38
src/managers/DashboardManager.ts View File

@@ -1,38 +0,0 @@
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 type { ServiceItemType } from './ServicesManager';
21
-import ServicesManager from './ServicesManager';
22
-import { getSublistWithIds } from '../utils/Services';
23
-import AsyncStorageManager from './AsyncStorageManager';
24
-
25
-export default class DashboardManager extends ServicesManager {
26
-  getCurrentDashboard(): Array<ServiceItemType | null> {
27
-    const dashboardIdList = AsyncStorageManager.getObject<Array<string>>(
28
-      AsyncStorageManager.PREFERENCES.dashboardItems.key
29
-    );
30
-    const allDatasets = [
31
-      ...this.amicaleDataset,
32
-      ...this.studentsDataset,
33
-      ...this.insaDataset,
34
-      ...this.specialDataset,
35
-    ];
36
-    return getSublistWithIds(dashboardIdList, allDatasets);
37
-  }
38
-}

+ 0
- 371
src/managers/ServicesManager.ts View File

@@ -1,371 +0,0 @@
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 i18n from 'i18n-js';
21
-import { StackNavigationProp } from '@react-navigation/stack';
22
-import ConnectionManager from './ConnectionManager';
23
-import type { FullDashboardType } from '../screens/Home/HomeScreen';
24
-import getStrippedServicesList from '../utils/Services';
25
-import Urls from '../constants/Urls';
26
-
27
-const AMICALE_LOGO = require('../../assets/amicale.png');
28
-
29
-export const SERVICES_KEY = {
30
-  CLUBS: 'clubs',
31
-  PROFILE: 'profile',
32
-  EQUIPMENT: 'equipment',
33
-  AMICALE_WEBSITE: 'amicale_website',
34
-  VOTE: 'vote',
35
-  PROXIMO: 'proximo',
36
-  WIKETUD: 'wiketud',
37
-  ELUS_ETUDIANTS: 'elus_etudiants',
38
-  TUTOR_INSA: 'tutor_insa',
39
-  RU: 'ru',
40
-  AVAILABLE_ROOMS: 'available_rooms',
41
-  BIB: 'bib',
42
-  EMAIL: 'email',
43
-  ENT: 'ent',
44
-  INSA_ACCOUNT: 'insa_account',
45
-  WASHERS: 'washers',
46
-  DRYERS: 'dryers',
47
-};
48
-
49
-export const SERVICES_CATEGORIES_KEY = {
50
-  AMICALE: 'amicale',
51
-  STUDENTS: 'students',
52
-  INSA: 'insa',
53
-  SPECIAL: 'special',
54
-};
55
-
56
-export type ServiceItemType = {
57
-  key: string;
58
-  title: string;
59
-  subtitle: string;
60
-  image: string | number;
61
-  onPress: () => void;
62
-  badgeFunction?: (dashboard: FullDashboardType) => number;
63
-};
64
-
65
-export type ServiceCategoryType = {
66
-  key: string;
67
-  title: string;
68
-  subtitle: string;
69
-  image: string | number;
70
-  content: Array<ServiceItemType>;
71
-};
72
-
73
-export default class ServicesManager {
74
-  navigation: StackNavigationProp<any>;
75
-
76
-  amicaleDataset: Array<ServiceItemType>;
77
-
78
-  studentsDataset: Array<ServiceItemType>;
79
-
80
-  insaDataset: Array<ServiceItemType>;
81
-
82
-  specialDataset: Array<ServiceItemType>;
83
-
84
-  categoriesDataset: Array<ServiceCategoryType>;
85
-
86
-  constructor(nav: StackNavigationProp<any>) {
87
-    this.navigation = nav;
88
-    this.amicaleDataset = [
89
-      {
90
-        key: SERVICES_KEY.CLUBS,
91
-        title: i18n.t('screens.clubs.title'),
92
-        subtitle: i18n.t('screens.services.descriptions.clubs'),
93
-        image: Urls.images.clubs,
94
-        onPress: (): void => this.onAmicaleServicePress('club-list'),
95
-      },
96
-      {
97
-        key: SERVICES_KEY.PROFILE,
98
-        title: i18n.t('screens.profile.title'),
99
-        subtitle: i18n.t('screens.services.descriptions.profile'),
100
-        image: Urls.images.profile,
101
-        onPress: (): void => this.onAmicaleServicePress('profile'),
102
-      },
103
-      {
104
-        key: SERVICES_KEY.EQUIPMENT,
105
-        title: i18n.t('screens.equipment.title'),
106
-        subtitle: i18n.t('screens.services.descriptions.equipment'),
107
-        image: Urls.images.equipment,
108
-        onPress: (): void => this.onAmicaleServicePress('equipment-list'),
109
-      },
110
-      {
111
-        key: SERVICES_KEY.AMICALE_WEBSITE,
112
-        title: i18n.t('screens.websites.amicale'),
113
-        subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
114
-        image: Urls.images.amicale,
115
-        onPress: (): void =>
116
-          nav.navigate('website', {
117
-            host: Urls.websites.amicale,
118
-            title: i18n.t('screens.websites.amicale'),
119
-          }),
120
-      },
121
-      {
122
-        key: SERVICES_KEY.VOTE,
123
-        title: i18n.t('screens.vote.title'),
124
-        subtitle: i18n.t('screens.services.descriptions.vote'),
125
-        image: Urls.images.vote,
126
-        onPress: (): void => this.onAmicaleServicePress('vote'),
127
-      },
128
-    ];
129
-    this.studentsDataset = [
130
-      {
131
-        key: SERVICES_KEY.PROXIMO,
132
-        title: i18n.t('screens.proximo.title'),
133
-        subtitle: i18n.t('screens.services.descriptions.proximo'),
134
-        image: Urls.images.proximo,
135
-        onPress: (): void => nav.navigate('proximo'),
136
-        badgeFunction: (dashboard: FullDashboardType): number =>
137
-          dashboard.proximo_articles,
138
-      },
139
-      {
140
-        key: SERVICES_KEY.WIKETUD,
141
-        title: 'Wiketud',
142
-        subtitle: i18n.t('screens.services.descriptions.wiketud'),
143
-        image: Urls.images.wiketud,
144
-        onPress: (): void =>
145
-          nav.navigate('website', {
146
-            host: Urls.websites.wiketud,
147
-            title: 'Wiketud',
148
-          }),
149
-      },
150
-      {
151
-        key: SERVICES_KEY.ELUS_ETUDIANTS,
152
-        title: 'Élus Étudiants',
153
-        subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
154
-        image: Urls.images.elusEtudiants,
155
-        onPress: (): void =>
156
-          nav.navigate('website', {
157
-            host: Urls.websites.elusEtudiants,
158
-            title: 'Élus Étudiants',
159
-          }),
160
-      },
161
-      {
162
-        key: SERVICES_KEY.TUTOR_INSA,
163
-        title: "Tutor'INSA",
164
-        subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
165
-        image: Urls.images.tutorInsa,
166
-        onPress: (): void =>
167
-          nav.navigate('website', {
168
-            host: Urls.websites.tutorInsa,
169
-            title: "Tutor'INSA",
170
-          }),
171
-        badgeFunction: (dashboard: FullDashboardType): number =>
172
-          dashboard.available_tutorials,
173
-      },
174
-    ];
175
-    this.insaDataset = [
176
-      {
177
-        key: SERVICES_KEY.RU,
178
-        title: i18n.t('screens.menu.title'),
179
-        subtitle: i18n.t('screens.services.descriptions.self'),
180
-        image: Urls.images.menu,
181
-        onPress: (): void => nav.navigate('self-menu'),
182
-        badgeFunction: (dashboard: FullDashboardType): number =>
183
-          dashboard.today_menu.length,
184
-      },
185
-      {
186
-        key: SERVICES_KEY.AVAILABLE_ROOMS,
187
-        title: i18n.t('screens.websites.rooms'),
188
-        subtitle: i18n.t('screens.services.descriptions.availableRooms'),
189
-        image: Urls.images.availableRooms,
190
-        onPress: (): void =>
191
-          nav.navigate('website', {
192
-            host: Urls.websites.availableRooms,
193
-            title: i18n.t('screens.websites.rooms'),
194
-          }),
195
-      },
196
-      {
197
-        key: SERVICES_KEY.BIB,
198
-        title: i18n.t('screens.websites.bib'),
199
-        subtitle: i18n.t('screens.services.descriptions.bib'),
200
-        image: Urls.images.bib,
201
-        onPress: (): void =>
202
-          nav.navigate('website', {
203
-            host: Urls.websites.bib,
204
-            title: i18n.t('screens.websites.bib'),
205
-          }),
206
-      },
207
-      {
208
-        key: SERVICES_KEY.EMAIL,
209
-        title: i18n.t('screens.websites.mails'),
210
-        subtitle: i18n.t('screens.services.descriptions.mails'),
211
-        image: Urls.images.bluemind,
212
-        onPress: (): void =>
213
-          nav.navigate('website', {
214
-            host: Urls.websites.bluemind,
215
-            title: i18n.t('screens.websites.mails'),
216
-          }),
217
-      },
218
-      {
219
-        key: SERVICES_KEY.ENT,
220
-        title: i18n.t('screens.websites.ent'),
221
-        subtitle: i18n.t('screens.services.descriptions.ent'),
222
-        image: Urls.images.ent,
223
-        onPress: (): void =>
224
-          nav.navigate('website', {
225
-            host: Urls.websites.ent,
226
-            title: i18n.t('screens.websites.ent'),
227
-          }),
228
-      },
229
-      {
230
-        key: SERVICES_KEY.INSA_ACCOUNT,
231
-        title: i18n.t('screens.insaAccount.title'),
232
-        subtitle: i18n.t('screens.services.descriptions.insaAccount'),
233
-        image: Urls.images.insaAccount,
234
-        onPress: (): void =>
235
-          nav.navigate('website', {
236
-            host: Urls.websites.insaAccount,
237
-            title: i18n.t('screens.insaAccount.title'),
238
-          }),
239
-      },
240
-    ];
241
-    this.specialDataset = [
242
-      {
243
-        key: SERVICES_KEY.WASHERS,
244
-        title: i18n.t('screens.proxiwash.washers'),
245
-        subtitle: i18n.t('screens.services.descriptions.washers'),
246
-        image: Urls.images.washer,
247
-        onPress: (): void => nav.navigate('proxiwash'),
248
-        badgeFunction: (dashboard: FullDashboardType): number =>
249
-          dashboard.available_washers,
250
-      },
251
-      {
252
-        key: SERVICES_KEY.DRYERS,
253
-        title: i18n.t('screens.proxiwash.dryers'),
254
-        subtitle: i18n.t('screens.services.descriptions.washers'),
255
-        image: Urls.images.dryer,
256
-        onPress: (): void => nav.navigate('proxiwash'),
257
-        badgeFunction: (dashboard: FullDashboardType): number =>
258
-          dashboard.available_dryers,
259
-      },
260
-    ];
261
-    this.categoriesDataset = [
262
-      {
263
-        key: SERVICES_CATEGORIES_KEY.AMICALE,
264
-        title: i18n.t('screens.services.categories.amicale'),
265
-        subtitle: i18n.t('screens.services.more'),
266
-        image: AMICALE_LOGO,
267
-        content: this.amicaleDataset,
268
-      },
269
-      {
270
-        key: SERVICES_CATEGORIES_KEY.STUDENTS,
271
-        title: i18n.t('screens.services.categories.students'),
272
-        subtitle: i18n.t('screens.services.more'),
273
-        image: 'account-group',
274
-        content: this.studentsDataset,
275
-      },
276
-      {
277
-        key: SERVICES_CATEGORIES_KEY.INSA,
278
-        title: i18n.t('screens.services.categories.insa'),
279
-        subtitle: i18n.t('screens.services.more'),
280
-        image: 'school',
281
-        content: this.insaDataset,
282
-      },
283
-      {
284
-        key: SERVICES_CATEGORIES_KEY.SPECIAL,
285
-        title: i18n.t('screens.services.categories.special'),
286
-        subtitle: i18n.t('screens.services.categories.special'),
287
-        image: 'star',
288
-        content: this.specialDataset,
289
-      },
290
-    ];
291
-  }
292
-
293
-  /**
294
-   * Redirects the user to the login screen if he is not logged in
295
-   *
296
-   * @param route
297
-   * @returns {null}
298
-   */
299
-  onAmicaleServicePress(route: string) {
300
-    if (ConnectionManager.getInstance().isLoggedIn()) {
301
-      this.navigation.navigate(route);
302
-    } else {
303
-      this.navigation.navigate('login', { nextScreen: route });
304
-    }
305
-  }
306
-
307
-  /**
308
-   * Gets the list of amicale's services
309
-   *
310
-   * @param excludedItems Ids of items to exclude from the returned list
311
-   * @returns {Array<ServiceItemType>}
312
-   */
313
-  getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
314
-    if (excludedItems != null) {
315
-      return getStrippedServicesList(excludedItems, this.amicaleDataset);
316
-    }
317
-    return this.amicaleDataset;
318
-  }
319
-
320
-  /**
321
-   * Gets the list of students' services
322
-   *
323
-   * @param excludedItems Ids of items to exclude from the returned list
324
-   * @returns {Array<ServiceItemType>}
325
-   */
326
-  getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
327
-    if (excludedItems != null) {
328
-      return getStrippedServicesList(excludedItems, this.studentsDataset);
329
-    }
330
-    return this.studentsDataset;
331
-  }
332
-
333
-  /**
334
-   * Gets the list of INSA's services
335
-   *
336
-   * @param excludedItems Ids of items to exclude from the returned list
337
-   * @returns {Array<ServiceItemType>}
338
-   */
339
-  getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
340
-    if (excludedItems != null) {
341
-      return getStrippedServicesList(excludedItems, this.insaDataset);
342
-    }
343
-    return this.insaDataset;
344
-  }
345
-
346
-  /**
347
-   * Gets the list of special services
348
-   *
349
-   * @param excludedItems Ids of items to exclude from the returned list
350
-   * @returns {Array<ServiceItemType>}
351
-   */
352
-  getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
353
-    if (excludedItems != null) {
354
-      return getStrippedServicesList(excludedItems, this.specialDataset);
355
-    }
356
-    return this.specialDataset;
357
-  }
358
-
359
-  /**
360
-   * Gets all services sorted by category
361
-   *
362
-   * @param excludedItems Ids of categories to exclude from the returned list
363
-   * @returns {Array<ServiceCategoryType>}
364
-   */
365
-  getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
366
-    if (excludedItems != null) {
367
-      return getStrippedServicesList(excludedItems, this.categoriesDataset);
368
-    }
369
-    return this.categoriesDataset;
370
-  }
371
-}

+ 31
- 5
src/navigation/MainNavigator.tsx View File

@@ -46,16 +46,20 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir
46 46
 import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
47 47
 import GameStartScreen from '../screens/Game/screens/GameStartScreen';
48 48
 import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen';
49
+import { usePreferences } from '../context/preferencesContext';
50
+import { getPreferenceBool, PreferenceKeys } from '../utils/asyncStorage';
51
+import IntroScreen from '../screens/Intro/IntroScreen';
49 52
 
50 53
 export enum MainRoutes {
51 54
   Main = 'main',
55
+  Intro = 'Intro',
52 56
   Gallery = 'gallery',
53 57
   Settings = 'settings',
54 58
   DashboardEdit = 'dashboard-edit',
55 59
   About = 'about',
56 60
   Dependencies = 'dependencies',
57 61
   Debug = 'debug',
58
-  GameStart = 'game-start',
62
+  GameStart = 'game',
59 63
   GameMain = 'game-main',
60 64
   Login = 'login',
61 65
   SelfMenu = 'self-menu',
@@ -66,11 +70,12 @@ export enum MainRoutes {
66 70
   ClubList = 'club-list',
67 71
   ClubInformation = 'club-information',
68 72
   ClubAbout = 'club-about',
69
-  EquipmentList = 'equipment-list',
73
+  EquipmentList = 'equipment',
70 74
   EquipmentRent = 'equipment-rent',
71 75
   EquipmentConfirm = 'equipment-confirm',
72 76
   Vote = 'vote',
73 77
   Feedback = 'feedback',
78
+  Website = 'website',
74 79
 }
75 80
 
76 81
 type DefaultParams = { [key in MainRoutes]: object | undefined };
@@ -96,13 +101,31 @@ export type MainStackParamsList = FullParamsList &
96 101
 
97 102
 const MainStack = createStackNavigator<MainStackParamsList>();
98 103
 
104
+function getIntroScreens() {
105
+  return (
106
+    <>
107
+      <MainStack.Screen
108
+        name={MainRoutes.Intro}
109
+        component={IntroScreen}
110
+        options={{
111
+          headerShown: false,
112
+        }}
113
+      />
114
+    </>
115
+  );
116
+}
117
+
99 118
 function MainStackComponent(props: {
119
+  showIntro: boolean;
100 120
   createTabNavigator: () => React.ReactElement;
101 121
 }) {
102
-  const { createTabNavigator } = props;
122
+  const { showIntro, createTabNavigator } = props;
123
+  if (showIntro) {
124
+    return getIntroScreens();
125
+  }
103 126
   return (
104 127
     <MainStack.Navigator
105
-      initialRouteName={MainRoutes.Main}
128
+      initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main}
106 129
       headerMode={'screen'}
107 130
     >
108 131
       <MainStack.Screen
@@ -183,7 +206,7 @@ function MainStackComponent(props: {
183 206
         }}
184 207
       />
185 208
       <MainStack.Screen
186
-        name={'website'}
209
+        name={MainRoutes.Website}
187 210
         component={WebsiteScreen}
188 211
         options={{
189 212
           title: '',
@@ -290,8 +313,11 @@ type PropsType = {
290 313
 };
291 314
 
292 315
 export default function MainNavigator(props: PropsType) {
316
+  const { preferences } = usePreferences();
317
+  const showIntro = getPreferenceBool(PreferenceKeys.showIntro, preferences);
293 318
   return (
294 319
     <MainStackComponent
320
+      showIntro={showIntro !== false}
295 321
       createTabNavigator={() => <TabNavigator {...props} />}
296 322
     />
297 323
   );

+ 81
- 61
src/navigation/TabNavigator.tsx View File

@@ -31,7 +31,6 @@ import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
31 31
 import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
32 32
 import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
33 33
 import PlanexScreen from '../screens/Planex/PlanexScreen';
34
-import AsyncStorageManager from '../managers/AsyncStorageManager';
35 34
 import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
36 35
 import ScannerScreen from '../screens/Home/ScannerScreen';
37 36
 import FeedItemScreen from '../screens/Home/FeedItemScreen';
@@ -41,6 +40,8 @@ import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
41 40
 import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
42 41
 import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
43 42
 import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot';
43
+import { usePreferences } from '../context/preferencesContext';
44
+import { getPreferenceString, PreferenceKeys } from '../utils/asyncStorage';
44 45
 
45 46
 const styles = StyleSheet.create({
46 47
   header: {
@@ -56,6 +57,20 @@ const styles = StyleSheet.create({
56 57
   },
57 58
 });
58 59
 
60
+type DefaultParams = { [key in TabRoutes]: object | undefined };
61
+
62
+export type FullParamsList = DefaultParams & {
63
+  [TabRoutes.Home]: {
64
+    nextScreen: string;
65
+    data: Record<string, object | undefined>;
66
+  };
67
+};
68
+
69
+// Don't know why but TS is complaining without this
70
+// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
71
+export type TabStackParamsList = FullParamsList &
72
+  Record<string, object | undefined>;
73
+
59 74
 const ServicesStack = createStackNavigator();
60 75
 
61 76
 function ServicesStackComponent() {
@@ -214,7 +229,7 @@ function PlanexStackComponent() {
214 229
   );
215 230
 }
216 231
 
217
-const Tab = createBottomTabNavigator();
232
+const Tab = createBottomTabNavigator<TabStackParamsList>();
218 233
 
219 234
 type PropsType = {
220 235
   defaultHomeRoute: string | null;
@@ -249,65 +264,70 @@ const ICONS: {
249 264
   },
250 265
 };
251 266
 
252
-export default class TabNavigator extends React.Component<PropsType> {
253
-  defaultRoute: string;
254
-  createHomeStackComponent: () => any;
255
-
256
-  constructor(props: PropsType) {
257
-    super(props);
258
-    this.defaultRoute = 'home';
259
-    if (!props.defaultHomeRoute) {
260
-      this.defaultRoute = AsyncStorageManager.getString(
261
-        AsyncStorageManager.PREFERENCES.defaultStartScreen.key
262
-      ).toLowerCase();
263
-    }
264
-    this.createHomeStackComponent = () =>
265
-      HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
267
+export default function TabNavigator(props: PropsType) {
268
+  const { preferences } = usePreferences();
269
+  let defaultRoute = getPreferenceString(
270
+    PreferenceKeys.defaultStartScreen,
271
+    preferences
272
+  );
273
+  if (!defaultRoute) {
274
+    defaultRoute = 'home';
275
+  } else {
276
+    defaultRoute = defaultRoute.toLowerCase();
266 277
   }
267 278
 
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
-    };
278
-    return (
279
-      <Tab.Navigator
280
-        initialRouteName={this.defaultRoute}
281
-        tabBar={(tabProps) => (
282
-          <CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
283
-        )}
284
-      >
285
-        <Tab.Screen
286
-          name={'services'}
287
-          component={ServicesStackComponent}
288
-          options={{ title: i18n.t('screens.services.title') }}
289
-        />
290
-        <Tab.Screen
291
-          name={'proxiwash'}
292
-          component={ProxiwashStackComponent}
293
-          options={{ title: i18n.t('screens.proxiwash.title') }}
294
-        />
295
-        <Tab.Screen
296
-          name={'home'}
297
-          component={this.createHomeStackComponent}
298
-          options={{ title: i18n.t('screens.home.title') }}
299
-        />
300
-        <Tab.Screen
301
-          name={'planning'}
302
-          component={PlanningStackComponent}
303
-          options={{ title: i18n.t('screens.planning.title') }}
304
-        />
305
-        <Tab.Screen
306
-          name={'planex'}
307
-          component={PlanexStackComponent}
308
-          options={{ title: i18n.t('screens.planex.title') }}
309
-        />
310
-      </Tab.Navigator>
311
-    );
312
-  }
279
+  const createHomeStackComponent = () =>
280
+    HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
281
+
282
+  const LABELS: {
283
+    [key: string]: string;
284
+  } = {
285
+    services: i18n.t('screens.services.title'),
286
+    proxiwash: i18n.t('screens.proxiwash.title'),
287
+    home: i18n.t('screens.home.title'),
288
+    planning: i18n.t('screens.planning.title'),
289
+    planex: i18n.t('screens.planex.title'),
290
+  };
291
+  return (
292
+    <Tab.Navigator
293
+      initialRouteName={defaultRoute}
294
+      tabBar={(tabProps) => (
295
+        <CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
296
+      )}
297
+    >
298
+      <Tab.Screen
299
+        name={'services'}
300
+        component={ServicesStackComponent}
301
+        options={{ title: i18n.t('screens.services.title') }}
302
+      />
303
+      <Tab.Screen
304
+        name={'proxiwash'}
305
+        component={ProxiwashStackComponent}
306
+        options={{ title: i18n.t('screens.proxiwash.title') }}
307
+      />
308
+      <Tab.Screen
309
+        name={'home'}
310
+        component={createHomeStackComponent}
311
+        options={{ title: i18n.t('screens.home.title') }}
312
+      />
313
+      <Tab.Screen
314
+        name={'events'}
315
+        component={PlanningStackComponent}
316
+        options={{ title: i18n.t('screens.planning.title') }}
317
+      />
318
+      <Tab.Screen
319
+        name={'planex'}
320
+        component={PlanexStackComponent}
321
+        options={{ title: i18n.t('screens.planex.title') }}
322
+      />
323
+    </Tab.Navigator>
324
+  );
325
+}
326
+
327
+export enum TabRoutes {
328
+  Services = 'services',
329
+  Proxiwash = 'proxiwash',
330
+  Home = 'home',
331
+  Planning = 'events',
332
+  Planex = 'planex',
313 333
 }

+ 62
- 122
src/screens/About/DebugScreen.tsx View File

@@ -17,7 +17,7 @@
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, { useRef, useState } from 'react';
21 21
 import { StyleSheet, View } from 'react-native';
22 22
 import {
23 23
   Button,
@@ -25,12 +25,17 @@ import {
25 25
   Subheading,
26 26
   TextInput,
27 27
   Title,
28
-  withTheme,
28
+  useTheme,
29 29
 } from 'react-native-paper';
30 30
 import { Modalize } from 'react-native-modalize';
31 31
 import CustomModal from '../../components/Overrides/CustomModal';
32
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
33 32
 import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
33
+import { usePreferences } from '../../context/preferencesContext';
34
+import {
35
+  defaultPreferences,
36
+  isValidPreferenceKey,
37
+  PreferenceKeys,
38
+} from '../../utils/asyncStorage';
34 39
 
35 40
 type PreferenceItemType = {
36 41
   key: string;
@@ -38,15 +43,6 @@ type PreferenceItemType = {
38 43
   current: string;
39 44
 };
40 45
 
41
-type PropsType = {
42
-  theme: ReactNativePaper.Theme;
43
-};
44
-
45
-type StateType = {
46
-  modalCurrentDisplayItem: PreferenceItemType | null;
47
-  currentPreferences: Array<PreferenceItemType>;
48
-};
49
-
50 46
 const styles = StyleSheet.create({
51 47
   container: {
52 48
     flex: 1,
@@ -62,47 +58,35 @@ const styles = StyleSheet.create({
62 58
  * Class defining the Debug screen.
63 59
  * This screen allows the user to get and modify information on the app/device.
64 60
  */
65
-class DebugScreen extends React.Component<PropsType, StateType> {
66
-  modalRef: { current: Modalize | null };
67
-
68
-  modalInputValue: string;
69
-
70
-  /**
71
-   * Copies user preferences to state for easier manipulation
72
-   *
73
-   * @param props
74
-   */
75
-  constructor(props: PropsType) {
76
-    super(props);
77
-    this.modalRef = React.createRef<Modalize>();
78
-    this.modalInputValue = '';
79
-    const currentPreferences: Array<PreferenceItemType> = [];
80
-    Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => {
81
-      const newObject: PreferenceItemType = { ...object };
82
-      newObject.current = AsyncStorageManager.getString(newObject.key);
83
-      currentPreferences.push(newObject);
84
-    });
85
-    this.state = {
86
-      modalCurrentDisplayItem: null,
87
-      currentPreferences,
61
+function DebugScreen() {
62
+  const theme = useTheme();
63
+  const { preferences, updatePreferences } = usePreferences();
64
+  const modalRef = useRef<Modalize>(null);
65
+
66
+  const [modalInputValue, setModalInputValue] = useState<string>('');
67
+  const [
68
+    modalCurrentDisplayItem,
69
+    setModalCurrentDisplayItem,
70
+  ] = useState<PreferenceItemType | null>(null);
71
+
72
+  const currentPreferences: Array<PreferenceItemType> = [];
73
+  Object.values(PreferenceKeys).forEach((key) => {
74
+    const newObject: PreferenceItemType = {
75
+      key: key,
76
+      current: preferences[key],
77
+      default: defaultPreferences[key],
88 78
     };
89
-  }
79
+    currentPreferences.push(newObject);
80
+  });
90 81
 
91
-  /**
92
-   * Gets the edit modal content
93
-   *
94
-   * @return {*}
95
-   */
96
-  getModalContent() {
97
-    const { props, state } = this;
82
+  const getModalContent = () => {
98 83
     let key = '';
99 84
     let defaultValue = '';
100 85
     let current = '';
101
-    if (state.modalCurrentDisplayItem) {
102
-      key = state.modalCurrentDisplayItem.key;
103
-      defaultValue = state.modalCurrentDisplayItem.default;
104
-      defaultValue = state.modalCurrentDisplayItem.default;
105
-      current = state.modalCurrentDisplayItem.current;
86
+    if (modalCurrentDisplayItem) {
87
+      key = modalCurrentDisplayItem.key;
88
+      defaultValue = modalCurrentDisplayItem.default;
89
+      current = modalCurrentDisplayItem.current;
106 90
     }
107 91
 
108 92
     return (
@@ -110,19 +94,14 @@ class DebugScreen extends React.Component<PropsType, StateType> {
110 94
         <Title>{key}</Title>
111 95
         <Subheading>Default: {defaultValue}</Subheading>
112 96
         <Subheading>Current: {current}</Subheading>
113
-        <TextInput
114
-          label="New Value"
115
-          onChangeText={(text: string) => {
116
-            this.modalInputValue = text;
117
-          }}
118
-        />
97
+        <TextInput label={'New Value'} onChangeText={setModalInputValue} />
119 98
         <View style={styles.buttonContainer}>
120 99
           <Button
121 100
             mode="contained"
122 101
             dark
123
-            color={props.theme.colors.success}
102
+            color={theme.colors.success}
124 103
             onPress={() => {
125
-              this.saveNewPrefs(key, this.modalInputValue);
104
+              saveNewPrefs(key, modalInputValue);
126 105
             }}
127 106
           >
128 107
             Save new value
@@ -130,9 +109,9 @@ class DebugScreen extends React.Component<PropsType, StateType> {
130 109
           <Button
131 110
             mode="contained"
132 111
             dark
133
-            color={props.theme.colors.danger}
112
+            color={theme.colors.danger}
134 113
             onPress={() => {
135
-              this.saveNewPrefs(key, defaultValue);
114
+              saveNewPrefs(key, defaultValue);
136 115
             }}
137 116
           >
138 117
             Reset to default
@@ -140,85 +119,46 @@ class DebugScreen extends React.Component<PropsType, StateType> {
140 119
         </View>
141 120
       </View>
142 121
     );
143
-  }
122
+  };
144 123
 
145
-  getRenderItem = ({ item }: { item: PreferenceItemType }) => {
124
+  const getRenderItem = ({ item }: { item: PreferenceItemType }) => {
146 125
     return (
147 126
       <List.Item
148 127
         title={item.key}
149 128
         description="Click to edit"
150 129
         onPress={() => {
151
-          this.showEditModal(item);
130
+          showEditModal(item);
152 131
         }}
153 132
       />
154 133
     );
155 134
   };
156 135
 
157
-  /**
158
-   * Shows the edit modal
159
-   *
160
-   * @param item
161
-   */
162
-  showEditModal(item: PreferenceItemType) {
163
-    this.setState({
164
-      modalCurrentDisplayItem: item,
165
-    });
166
-    if (this.modalRef.current) {
167
-      this.modalRef.current.open();
136
+  const showEditModal = (item: PreferenceItemType) => {
137
+    setModalCurrentDisplayItem(item);
138
+    if (modalRef.current) {
139
+      modalRef.current.open();
168 140
     }
169
-  }
141
+  };
170 142
 
171
-  /**
172
-   * Finds the index of the given key in the preferences array
173
-   *
174
-   * @param key THe key to find the index of
175
-   * @returns {number}
176
-   */
177
-  findIndexOfKey(key: string): number {
178
-    const { currentPreferences } = this.state;
179
-    let index = -1;
180
-    for (let i = 0; i < currentPreferences.length; i += 1) {
181
-      if (currentPreferences[i].key === key) {
182
-        index = i;
183
-        break;
184
-      }
143
+  const saveNewPrefs = (key: string, value: string) => {
144
+    if (isValidPreferenceKey(key)) {
145
+      updatePreferences(key, value);
185 146
     }
186
-    return index;
187
-  }
188
-
189
-  /**
190
-   * Saves the new value of the given preference
191
-   *
192
-   * @param key The pref key
193
-   * @param value The pref value
194
-   */
195
-  saveNewPrefs(key: string, value: string) {
196
-    this.setState((prevState: StateType): {
197
-      currentPreferences: Array<PreferenceItemType>;
198
-    } => {
199
-      const currentPreferences = [...prevState.currentPreferences];
200
-      currentPreferences[this.findIndexOfKey(key)].current = value;
201
-      return { currentPreferences };
202
-    });
203
-    AsyncStorageManager.set(key, value);
204
-    if (this.modalRef.current) {
205
-      this.modalRef.current.close();
147
+    if (modalRef.current) {
148
+      modalRef.current.close();
206 149
     }
207
-  }
150
+  };
208 151
 
209
-  render() {
210
-    const { state } = this;
211
-    return (
212
-      <View>
213
-        <CustomModal ref={this.modalRef}>{this.getModalContent()}</CustomModal>
214
-        <CollapsibleFlatList
215
-          data={state.currentPreferences}
216
-          extraData={state.currentPreferences}
217
-          renderItem={this.getRenderItem}
218
-        />
219
-      </View>
220
-    );
221
-  }
152
+  return (
153
+    <View>
154
+      <CustomModal ref={modalRef}>{getModalContent()}</CustomModal>
155
+      <CollapsibleFlatList
156
+        data={currentPreferences}
157
+        extraData={currentPreferences}
158
+        renderItem={getRenderItem}
159
+      />
160
+    </View>
161
+  );
222 162
 }
223 163
 
224
-export default withTheme(DebugScreen);
164
+export default DebugScreen;

+ 2
- 9
src/screens/Amicale/Equipment/EquipmentListScreen.tsx View File

@@ -25,7 +25,6 @@ import i18n from 'i18n-js';
25 25
 import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
26 26
 import MascotPopup from '../../../components/Mascot/MascotPopup';
27 27
 import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
28
-import AsyncStorageManager from '../../../managers/AsyncStorageManager';
29 28
 import GENERAL_STYLES from '../../../constants/Styles';
30 29
 import ConnectionManager from '../../../managers/ConnectionManager';
31 30
 import { ApiRejectType } from '../../../utils/WebData';
@@ -36,7 +35,7 @@ type PropsType = {
36 35
 };
37 36
 
38 37
 type StateType = {
39
-  mascotDialogVisible: boolean;
38
+  mascotDialogVisible: boolean | undefined;
40 39
 };
41 40
 
42 41
 export type DeviceType = {
@@ -75,9 +74,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
75 74
     super(props);
76 75
     this.userRents = null;
77 76
     this.state = {
78
-      mascotDialogVisible: AsyncStorageManager.getBool(
79
-        AsyncStorageManager.PREFERENCES.equipmentShowMascot.key
80
-      ),
77
+      mascotDialogVisible: undefined,
81 78
     };
82 79
   }
83 80
 
@@ -145,10 +142,6 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
145 142
   };
146 143
 
147 144
   hideMascotDialog = () => {
148
-    AsyncStorageManager.set(
149
-      AsyncStorageManager.PREFERENCES.equipmentShowMascot.key,
150
-      false
151
-    );
152 145
     this.setState({ mascotDialogVisible: false });
153 146
   };
154 147
 

+ 7
- 13
src/screens/Amicale/LoginScreen.tsx View File

@@ -31,7 +31,6 @@ import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
31 31
 import LinearGradient from 'react-native-linear-gradient';
32 32
 import ConnectionManager from '../../managers/ConnectionManager';
33 33
 import ErrorDialog from '../../components/Dialogs/ErrorDialog';
34
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
35 34
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
36 35
 import MascotPopup from '../../components/Mascot/MascotPopup';
37 36
 import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
@@ -56,7 +55,7 @@ type StateType = {
56 55
   loading: boolean;
57 56
   dialogVisible: boolean;
58 57
   dialogError: ApiRejectType;
59
-  mascotDialogVisible: boolean;
58
+  mascotDialogVisible: boolean | undefined;
60 59
 };
61 60
 
62 61
 const ICON_AMICALE = require('../../../assets/amicale.png');
@@ -118,9 +117,7 @@ class LoginScreen extends React.Component<Props, StateType> {
118 117
       loading: false,
119 118
       dialogVisible: false,
120 119
       dialogError: { status: REQUEST_STATUS.SUCCESS },
121
-      mascotDialogVisible: AsyncStorageManager.getBool(
122
-        AsyncStorageManager.PREFERENCES.loginShowMascot.key
123
-      ),
120
+      mascotDialogVisible: undefined,
124 121
     };
125 122
   }
126 123
 
@@ -321,10 +318,6 @@ class LoginScreen extends React.Component<Props, StateType> {
321 318
   };
322 319
 
323 320
   hideMascotDialog = () => {
324
-    AsyncStorageManager.set(
325
-      AsyncStorageManager.PREFERENCES.loginShowMascot.key,
326
-      false
327
-    );
328 321
     this.setState({ mascotDialogVisible: false });
329 322
   };
330 323
 
@@ -357,10 +350,11 @@ class LoginScreen extends React.Component<Props, StateType> {
357 350
   handleSuccess = () => {
358 351
     const { navigation } = this.props;
359 352
     // Do not show the home login banner again
360
-    AsyncStorageManager.set(
361
-      AsyncStorageManager.PREFERENCES.homeShowMascot.key,
362
-      false
363
-    );
353
+    // TODO
354
+    // AsyncStorageManager.set(
355
+    //   AsyncStorageManager.PREFERENCES.homeShowMascot.key,
356
+    //   false
357
+    // );
364 358
     if (this.nextScreen == null) {
365 359
       navigation.goBack();
366 360
     } else {

+ 8
- 4
src/screens/Amicale/ProfileScreen.tsx View File

@@ -36,13 +36,16 @@ import MaterialHeaderButtons, {
36 36
 } from '../../components/Overrides/CustomHeaderButton';
37 37
 import CardList from '../../components/Lists/CardList/CardList';
38 38
 import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot';
39
-import ServicesManager, { SERVICES_KEY } from '../../managers/ServicesManager';
40 39
 import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
41
-import type { ServiceItemType } from '../../managers/ServicesManager';
42 40
 import GENERAL_STYLES from '../../constants/Styles';
43 41
 import Urls from '../../constants/Urls';
44 42
 import RequestScreen from '../../components/Screens/RequestScreen';
45 43
 import ConnectionManager from '../../managers/ConnectionManager';
44
+import {
45
+  getAmicaleServices,
46
+  ServiceItemType,
47
+  SERVICES_KEY,
48
+} from '../../utils/Services';
46 49
 
47 50
 type PropsType = {
48 51
   navigation: StackNavigationProp<any>;
@@ -100,8 +103,9 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
100 103
     super(props);
101 104
     this.data = undefined;
102 105
     this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
103
-    const services = new ServicesManager(props.navigation);
104
-    this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
106
+    this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [
107
+      SERVICES_KEY.PROFILE,
108
+    ]);
105 109
     this.state = {
106 110
       dialogVisible: false,
107 111
     };

+ 2
- 9
src/screens/Amicale/VoteScreen.tsx View File

@@ -28,7 +28,6 @@ import VoteResults from '../../components/Amicale/Vote/VoteResults';
28 28
 import VoteWait from '../../components/Amicale/Vote/VoteWait';
29 29
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
30 30
 import MascotPopup from '../../components/Mascot/MascotPopup';
31
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
32 31
 import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
33 32
 import GENERAL_STYLES from '../../constants/Styles';
34 33
 import ConnectionManager from '../../managers/ConnectionManager';
@@ -118,7 +117,7 @@ type PropsType = {};
118 117
 
119 118
 type StateType = {
120 119
   hasVoted: boolean;
121
-  mascotDialogVisible: boolean;
120
+  mascotDialogVisible: boolean | undefined;
122 121
 };
123 122
 
124 123
 const styles = StyleSheet.create({
@@ -154,9 +153,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
154 153
     this.dates = undefined;
155 154
     this.state = {
156 155
       hasVoted: false,
157
-      mascotDialogVisible: AsyncStorageManager.getBool(
158
-        AsyncStorageManager.PREFERENCES.voteShowMascot.key
159
-      ),
156
+      mascotDialogVisible: undefined,
160 157
     };
161 158
     this.hasVoted = false;
162 159
     this.today = new Date();
@@ -328,10 +325,6 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
328 325
   };
329 326
 
330 327
   hideMascotDialog = () => {
331
-    AsyncStorageManager.set(
332
-      AsyncStorageManager.PREFERENCES.voteShowMascot.key,
333
-      false
334
-    );
335 328
     this.setState({ mascotDialogVisible: false });
336 329
   };
337 330
 

+ 2
- 1
src/screens/Game/screens/GameMainScreen.tsx View File

@@ -33,6 +33,7 @@ import MaterialHeaderButtons, {
33 33
 import type { OptionsDialogButtonType } from '../../../components/Dialogs/OptionsDialog';
34 34
 import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
35 35
 import GENERAL_STYLES from '../../../constants/Styles';
36
+import { MainRoutes } from '../../../navigation/MainNavigator';
36 37
 
37 38
 type PropsType = {
38 39
   navigation: StackNavigationProp<any>;
@@ -200,7 +201,7 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
200 201
       gameScore: score,
201 202
     });
202 203
     if (!isRestart) {
203
-      props.navigation.replace('game-start', {
204
+      props.navigation.replace(MainRoutes.GameStart, {
204 205
         score: state.gameScore,
205 206
         level: state.gameLevel,
206 207
         time: state.gameTime,

+ 10
- 9
src/screens/Game/screens/GameStartScreen.tsx View File

@@ -35,7 +35,6 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI
35 35
 import LinearGradient from 'react-native-linear-gradient';
36 36
 import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
37 37
 import MascotPopup from '../../../components/Mascot/MascotPopup';
38
-import AsyncStorageManager from '../../../managers/AsyncStorageManager';
39 38
 import type { GridType } from '../components/GridComponent';
40 39
 import GridComponent from '../components/GridComponent';
41 40
 import GridManager from '../logic/GridManager';
@@ -152,9 +151,11 @@ class GameStartScreen extends React.Component<PropsType> {
152 151
     super(props);
153 152
     this.isHighScore = false;
154 153
     this.gridManager = new GridManager(4, 4, props.theme);
155
-    this.scores = AsyncStorageManager.getObject(
156
-      AsyncStorageManager.PREFERENCES.gameScores.key
157
-    );
154
+    // TODO
155
+    // this.scores = AsyncStorageManager.getObject(
156
+    //   AsyncStorageManager.PREFERENCES.gameScores.key
157
+    // );
158
+    this.scores = [];
158 159
     this.scores.sort((a: number, b: number): number => b - a);
159 160
     if (props.route.params != null) {
160 161
       this.recoverGameScore();
@@ -448,10 +449,11 @@ class GameStartScreen extends React.Component<PropsType> {
448 449
       if (this.scores.length > 3) {
449 450
         this.scores.splice(3, 1);
450 451
       }
451
-      AsyncStorageManager.set(
452
-        AsyncStorageManager.PREFERENCES.gameScores.key,
453
-        this.scores
454
-      );
452
+      // TODO
453
+      // AsyncStorageManager.set(
454
+      //   AsyncStorageManager.PREFERENCES.gameScores.key,
455
+      //   this.scores
456
+      // );
455 457
     }
456 458
   }
457 459
 
@@ -472,7 +474,6 @@ class GameStartScreen extends React.Component<PropsType> {
472 474
           <CollapsibleScrollView headerColors={'transparent'}>
473 475
             {this.getMainContent()}
474 476
             <MascotPopup
475
-              prefKey={AsyncStorageManager.PREFERENCES.gameStartMascot.key}
476 477
               title={i18n.t('screens.game.mascotDialog.title')}
477 478
               message={i18n.t('screens.game.mascotDialog.message')}
478 479
               icon="gamepad-variant"

+ 170
- 238
src/screens/Home/HomeScreen.tsx View File

@@ -17,7 +17,7 @@
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, { useLayoutEffect, useRef, useState } from 'react';
21 21
 import {
22 22
   FlatList,
23 23
   NativeScrollEvent,
@@ -26,9 +26,13 @@ import {
26 26
   StyleSheet,
27 27
 } from 'react-native';
28 28
 import i18n from 'i18n-js';
29
-import { Headline, withTheme } from 'react-native-paper';
30
-import { CommonActions } from '@react-navigation/native';
31
-import { StackNavigationProp } from '@react-navigation/stack';
29
+import { Headline, useTheme } from 'react-native-paper';
30
+import {
31
+  CommonActions,
32
+  useFocusEffect,
33
+  useNavigation,
34
+} from '@react-navigation/native';
35
+import { StackScreenProps } from '@react-navigation/stack';
32 36
 import * as Animatable from 'react-native-animatable';
33 37
 import { View } from 'react-native-animatable';
34 38
 import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
@@ -44,16 +48,17 @@ import MaterialHeaderButtons, {
44 48
 import AnimatedFAB from '../../components/Animations/AnimatedFAB';
45 49
 import ConnectionManager from '../../managers/ConnectionManager';
46 50
 import LogoutDialog from '../../components/Amicale/LogoutDialog';
47
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
48 51
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
49 52
 import MascotPopup from '../../components/Mascot/MascotPopup';
50
-import DashboardManager from '../../managers/DashboardManager';
51
-import type { ServiceItemType } from '../../managers/ServicesManager';
52 53
 import { getDisplayEvent, getFutureEvents } from '../../utils/Home';
53 54
 import type { PlanningEventType } from '../../utils/Planning';
54 55
 import GENERAL_STYLES from '../../constants/Styles';
55 56
 import Urls from '../../constants/Urls';
56 57
 import { readData } from '../../utils/WebData';
58
+import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
59
+import { ServiceItemType } from '../../utils/Services';
60
+import { useCurrentDashboard } from '../../context/preferencesContext';
61
+import { MainRoutes } from '../../navigation/MainNavigator';
57 62
 
58 63
 const FEED_ITEM_HEIGHT = 500;
59 64
 
@@ -88,15 +93,7 @@ type RawDashboardType = {
88 93
   dashboard: FullDashboardType;
89 94
 };
90 95
 
91
-type PropsType = {
92
-  navigation: StackNavigationProp<any>;
93
-  route: { params: { nextScreen: string; data: object } };
94
-  theme: ReactNativePaper.Theme;
95
-};
96
-
97
-type StateType = {
98
-  dialogVisible: boolean;
99
-};
96
+type Props = StackScreenProps<TabStackParamsList, TabRoutes.Home>;
100 97
 
101 98
 const styles = StyleSheet.create({
102 99
   dashboardRow: {
@@ -127,106 +124,94 @@ const styles = StyleSheet.create({
127 124
   },
128 125
 });
129 126
 
127
+const sortFeedTime = (a: FeedItemType, b: FeedItemType): number =>
128
+  b.time - a.time;
129
+
130
+const generateNewsFeed = (rawFeed: RawNewsFeedType): Array<FeedItemType> => {
131
+  const finalFeed: Array<FeedItemType> = [];
132
+  Object.keys(rawFeed).forEach((key: string) => {
133
+    const category: Array<FeedItemType> | null = rawFeed[key];
134
+    if (category != null && category.length > 0) {
135
+      finalFeed.push(...category);
136
+    }
137
+  });
138
+  finalFeed.sort(sortFeedTime);
139
+  return finalFeed;
140
+};
141
+
130 142
 /**
131 143
  * Class defining the app's home screen
132 144
  */
133
-class HomeScreen extends React.Component<PropsType, StateType> {
134
-  static sortFeedTime = (a: FeedItemType, b: FeedItemType): number =>
135
-    b.time - a.time;
136
-
137
-  static generateNewsFeed(rawFeed: RawNewsFeedType): Array<FeedItemType> {
138
-    const finalFeed: Array<FeedItemType> = [];
139
-    Object.keys(rawFeed).forEach((key: string) => {
140
-      const category: Array<FeedItemType> | null = rawFeed[key];
141
-      if (category != null && category.length > 0) {
142
-        finalFeed.push(...category);
145
+function HomeScreen(props: Props) {
146
+  const theme = useTheme();
147
+  const navigation = useNavigation();
148
+
149
+  const [dialogVisible, setDialogVisible] = useState(false);
150
+  const fabRef = useRef<AnimatedFAB>(null);
151
+
152
+  const [isLoggedIn, setIsLoggedIn] = useState(
153
+    ConnectionManager.getInstance().isLoggedIn()
154
+  );
155
+  const { currentDashboard } = useCurrentDashboard();
156
+
157
+  let homeDashboard: FullDashboardType | null = null;
158
+
159
+  useLayoutEffect(() => {
160
+    const getHeaderButton = () => {
161
+      let onPressLog = () =>
162
+        navigation.navigate('login', { nextScreen: 'profile' });
163
+      let logIcon = 'login';
164
+      let logColor = theme.colors.primary;
165
+      if (isLoggedIn) {
166
+        onPressLog = () => showDisconnectDialog();
167
+        logIcon = 'logout';
168
+        logColor = theme.colors.text;
143 169
       }
144
-    });
145
-    finalFeed.sort(HomeScreen.sortFeedTime);
146
-    return finalFeed;
147
-  }
148 170
 
149
-  isLoggedIn: boolean | null;
150
-
151
-  fabRef: { current: null | AnimatedFAB };
152
-
153
-  currentNewFeed: Array<FeedItemType>;
154
-
155
-  currentDashboard: FullDashboardType | null;
156
-
157
-  dashboardManager: DashboardManager;
158
-
159
-  constructor(props: PropsType) {
160
-    super(props);
161
-    this.fabRef = React.createRef();
162
-    this.dashboardManager = new DashboardManager(props.navigation);
163
-    this.currentNewFeed = [];
164
-    this.currentDashboard = null;
165
-    this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
166
-    props.navigation.setOptions({
167
-      headerRight: this.getHeaderButton,
168
-    });
169
-    this.state = {
170
-      dialogVisible: false,
171
+      return (
172
+        <MaterialHeaderButtons>
173
+          <Item
174
+            title={'log'}
175
+            iconName={logIcon}
176
+            color={logColor}
177
+            onPress={onPressLog}
178
+          />
179
+          <Item
180
+            title={i18n.t('screens.settings.title')}
181
+            iconName={'cog'}
182
+            onPress={() => navigation.navigate(MainRoutes.Settings)}
183
+          />
184
+        </MaterialHeaderButtons>
185
+      );
171 186
     };
172
-  }
173
-
174
-  componentDidMount() {
175
-    const { props } = this;
176
-    props.navigation.addListener('focus', this.onScreenFocus);
177
-    // Handle link open when home is focused
178
-    props.navigation.addListener('state', this.handleNavigationParams);
179
-  }
180
-
181
-  /**
182
-   * Updates login state and navigation parameters on screen focus
183
-   */
184
-  onScreenFocus = () => {
185
-    const { props } = this;
186
-    if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
187
-      this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
188
-      props.navigation.setOptions({
189
-        headerRight: this.getHeaderButton,
190
-      });
191
-    }
192
-    // handle link open when home is not focused or created
193
-    this.handleNavigationParams();
194
-  };
195
-
196
-  /**
197
-   * Gets header buttons based on login state
198
-   *
199
-   * @returns {*}
200
-   */
201
-  getHeaderButton = () => {
202
-    const { props } = this;
203
-    let onPressLog = (): void =>
204
-      props.navigation.navigate('login', { nextScreen: 'profile' });
205
-    let logIcon = 'login';
206
-    let logColor = props.theme.colors.primary;
207
-    if (this.isLoggedIn) {
208
-      onPressLog = (): void => this.showDisconnectDialog();
209
-      logIcon = 'logout';
210
-      logColor = props.theme.colors.text;
211
-    }
187
+    navigation.setOptions({
188
+      headerRight: getHeaderButton,
189
+    });
190
+    // eslint-disable-next-line react-hooks/exhaustive-deps
191
+  }, [navigation, isLoggedIn]);
192
+
193
+  useFocusEffect(
194
+    React.useCallback(() => {
195
+      const handleNavigationParams = () => {
196
+        const { route } = props;
197
+        if (route.params != null) {
198
+          if (route.params.nextScreen != null) {
199
+            navigation.navigate(route.params.nextScreen, route.params.data);
200
+            // reset params to prevent infinite loop
201
+            navigation.dispatch(CommonActions.setParams({ nextScreen: null }));
202
+          }
203
+        }
204
+      };
212 205
 
213
-    const onPressSettings = (): void => props.navigation.navigate('settings');
214
-    return (
215
-      <MaterialHeaderButtons>
216
-        <Item
217
-          title="log"
218
-          iconName={logIcon}
219
-          color={logColor}
220
-          onPress={onPressLog}
221
-        />
222
-        <Item
223
-          title={i18n.t('screens.settings.title')}
224
-          iconName="cog"
225
-          onPress={onPressSettings}
226
-        />
227
-      </MaterialHeaderButtons>
228
-    );
229
-  };
206
+      if (ConnectionManager.getInstance().isLoggedIn() !== isLoggedIn) {
207
+        setIsLoggedIn(ConnectionManager.getInstance().isLoggedIn());
208
+      }
209
+      // handle link open when home is not focused or created
210
+      handleNavigationParams();
211
+      return () => {};
212
+      // eslint-disable-next-line react-hooks/exhaustive-deps
213
+    }, [isLoggedIn])
214
+  );
230 215
 
231 216
   /**
232 217
    * Gets the event dashboard render item.
@@ -235,7 +220,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
235 220
    * @param content
236 221
    * @return {*}
237 222
    */
238
-  getDashboardEvent(content: Array<PlanningEventType>) {
223
+  const getDashboardEvent = (content: Array<PlanningEventType>) => {
239 224
     const futureEvents = getFutureEvents(content);
240 225
     const displayEvent = getDisplayEvent(futureEvents);
241 226
     // const clickPreviewAction = () =>
@@ -246,15 +231,15 @@ class HomeScreen extends React.Component<PropsType, StateType> {
246 231
     return (
247 232
       <DashboardItem
248 233
         eventNumber={futureEvents.length}
249
-        clickAction={this.onEventContainerClick}
234
+        clickAction={onEventContainerClick}
250 235
       >
251 236
         <PreviewEventDashboardItem
252 237
           event={displayEvent}
253
-          clickAction={this.onEventContainerClick}
238
+          clickAction={onEventContainerClick}
254 239
         />
255 240
       </DashboardItem>
256 241
     );
257
-  }
242
+  };
258 243
 
259 244
   /**
260 245
    * Gets a dashboard item with a row of shortcut buttons.
@@ -262,16 +247,16 @@ class HomeScreen extends React.Component<PropsType, StateType> {
262 247
    * @param content
263 248
    * @return {*}
264 249
    */
265
-  getDashboardRow(content: Array<ServiceItemType | null>) {
250
+  const getDashboardRow = (content: Array<ServiceItemType | null>) => {
266 251
     return (
267 252
       <FlatList
268 253
         data={content}
269
-        renderItem={this.getDashboardRowRenderItem}
254
+        renderItem={getDashboardRowRenderItem}
270 255
         horizontal
271 256
         contentContainerStyle={styles.dashboardRow}
272 257
       />
273 258
     );
274
-  }
259
+  };
275 260
 
276 261
   /**
277 262
    * Gets a dashboard shortcut item
@@ -279,15 +264,19 @@ class HomeScreen extends React.Component<PropsType, StateType> {
279 264
    * @param item
280 265
    * @returns {*}
281 266
    */
282
-  getDashboardRowRenderItem = ({ item }: { item: ServiceItemType | null }) => {
267
+  const getDashboardRowRenderItem = ({
268
+    item,
269
+  }: {
270
+    item: ServiceItemType | null;
271
+  }) => {
283 272
     if (item != null) {
284 273
       return (
285 274
         <SmallDashboardItem
286 275
           image={item.image}
287 276
           onPress={item.onPress}
288 277
           badgeCount={
289
-            this.currentDashboard != null && item.badgeFunction != null
290
-              ? item.badgeFunction(this.currentDashboard)
278
+            homeDashboard != null && item.badgeFunction != null
279
+              ? item.badgeFunction(homeDashboard)
291 280
               : undefined
292 281
           }
293 282
         />
@@ -296,29 +285,13 @@ class HomeScreen extends React.Component<PropsType, StateType> {
296 285
     return <SmallDashboardItem />;
297 286
   };
298 287
 
299
-  /**
300
-   * Gets a render item for the given feed object
301
-   *
302
-   * @param item The feed item to display
303
-   * @return {*}
304
-   */
305
-  getFeedItem(item: FeedItemType) {
306
-    return <FeedItem item={item} height={FEED_ITEM_HEIGHT} />;
307
-  }
308
-
309
-  /**
310
-   * Gets a FlatList render item
311
-   *
312
-   * @param item The item to display
313
-   * @param section The current section
314
-   * @return {*}
315
-   */
316
-  getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item);
288
+  const getRenderItem = ({ item }: { item: FeedItemType }) => (
289
+    <FeedItem item={item} height={FEED_ITEM_HEIGHT} />
290
+  );
317 291
 
318
-  getRenderSectionHeader = (data: {
292
+  const getRenderSectionHeader = (data: {
319 293
     section: SectionListData<FeedItemType>;
320 294
   }) => {
321
-    const { props } = this;
322 295
     const icon = data.section.icon;
323 296
     if (data.section.data.length > 0) {
324 297
       return (
@@ -330,7 +303,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
330 303
         <Headline
331 304
           style={{
332 305
             ...styles.sectionHeaderEmpty,
333
-            color: props.theme.colors.textDisabled,
306
+            color: theme.colors.textDisabled,
334 307
           }}
335 308
         >
336 309
           {data.section.title}
@@ -339,7 +312,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
339 312
           <MaterialCommunityIcons
340 313
             name={icon}
341 314
             size={100}
342
-            color={props.theme.colors.textDisabled}
315
+            color={theme.colors.textDisabled}
343 316
             style={GENERAL_STYLES.center}
344 317
           />
345 318
         ) : null}
@@ -347,7 +320,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
347 320
     );
348 321
   };
349 322
 
350
-  getListHeader = (fetchedData: RawDashboardType | undefined) => {
323
+  const getListHeader = (fetchedData: RawDashboardType | undefined) => {
351 324
     let dashboard = null;
352 325
     if (fetchedData != null) {
353 326
       dashboard = fetchedData.dashboard;
@@ -355,41 +328,17 @@ class HomeScreen extends React.Component<PropsType, StateType> {
355 328
     return (
356 329
       <Animatable.View animation="fadeInDown" duration={500} useNativeDriver>
357 330
         <ActionsDashBoardItem />
358
-        {this.getDashboardRow(this.dashboardManager.getCurrentDashboard())}
359
-        {this.getDashboardEvent(
360
-          dashboard == null ? [] : dashboard.today_events
361
-        )}
331
+        {getDashboardRow(currentDashboard)}
332
+        {getDashboardEvent(dashboard == null ? [] : dashboard.today_events)}
362 333
       </Animatable.View>
363 334
     );
364 335
   };
365 336
 
366
-  /**
367
-   * Navigates to the a new screen if navigation parameters specify one
368
-   */
369
-  handleNavigationParams = () => {
370
-    const { props } = this;
371
-    if (props.route.params != null) {
372
-      if (props.route.params.nextScreen != null) {
373
-        props.navigation.navigate(
374
-          props.route.params.nextScreen,
375
-          props.route.params.data
376
-        );
377
-        // reset params to prevent infinite loop
378
-        props.navigation.dispatch(
379
-          CommonActions.setParams({ nextScreen: null })
380
-        );
381
-      }
382
-    }
383
-  };
337
+  const showDisconnectDialog = () => setDialogVisible(true);
384 338
 
385
-  showDisconnectDialog = (): void => this.setState({ dialogVisible: true });
339
+  const hideDisconnectDialog = () => setDialogVisible(false);
386 340
 
387
-  hideDisconnectDialog = (): void => this.setState({ dialogVisible: false });
388
-
389
-  openScanner = () => {
390
-    const { props } = this;
391
-    props.navigation.navigate('scanner');
392
-  };
341
+  const openScanner = () => navigation.navigate('scanner');
393 342
 
394 343
   /**
395 344
    * Creates the dataset to be used in the FlatList
@@ -398,7 +347,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
398 347
    * @param isLoading
399 348
    * @return {*}
400 349
    */
401
-  createDataset = (
350
+  const createDataset = (
402 351
     fetchedData: RawDashboardType | undefined,
403 352
     isLoading: boolean
404 353
   ): Array<{
@@ -407,21 +356,20 @@ class HomeScreen extends React.Component<PropsType, StateType> {
407 356
     icon?: string;
408 357
     id: string;
409 358
   }> => {
359
+    let currentNewFeed: Array<FeedItemType> = [];
410 360
     if (fetchedData) {
411 361
       if (fetchedData.news_feed) {
412
-        this.currentNewFeed = HomeScreen.generateNewsFeed(
413
-          fetchedData.news_feed
414
-        );
362
+        currentNewFeed = generateNewsFeed(fetchedData.news_feed);
415 363
       }
416 364
       if (fetchedData.dashboard) {
417
-        this.currentDashboard = fetchedData.dashboard;
365
+        homeDashboard = fetchedData.dashboard;
418 366
       }
419 367
     }
420
-    if (this.currentNewFeed.length > 0) {
368
+    if (currentNewFeed.length > 0) {
421 369
       return [
422 370
         {
423 371
           title: i18n.t('screens.home.feedTitle'),
424
-          data: this.currentNewFeed,
372
+          data: currentNewFeed,
425 373
           id: SECTIONS_ID[1],
426 374
         },
427 375
       ];
@@ -438,14 +386,11 @@ class HomeScreen extends React.Component<PropsType, StateType> {
438 386
     ];
439 387
   };
440 388
 
441
-  onEventContainerClick = () => {
442
-    const { props } = this;
443
-    props.navigation.navigate('planning');
444
-  };
389
+  const onEventContainerClick = () => navigation.navigate(TabRoutes.Planning);
445 390
 
446
-  onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
447
-    if (this.fabRef.current) {
448
-      this.fabRef.current.onScroll(event);
391
+  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
392
+    if (fabRef.current) {
393
+      fabRef.current.onScroll(event);
449 394
     }
450 395
   };
451 396
 
@@ -453,63 +398,50 @@ class HomeScreen extends React.Component<PropsType, StateType> {
453 398
    * Callback when pressing the login button on the banner.
454 399
    * This hides the banner and takes the user to the login page.
455 400
    */
456
-  onLogin = () => {
457
-    const { props } = this;
458
-    props.navigation.navigate('login', {
401
+  const onLogin = () =>
402
+    navigation.navigate(MainRoutes.Login, {
459 403
       nextScreen: 'profile',
460 404
     });
461
-  };
462 405
 
463
-  render() {
464
-    const { props, state } = this;
465
-    return (
466
-      <View style={GENERAL_STYLES.flex}>
467
-        <View style={styles.content}>
468
-          <WebSectionList
469
-            request={() => readData<RawDashboardType>(Urls.app.dashboard)}
470
-            createDataset={this.createDataset}
471
-            autoRefreshTime={REFRESH_TIME}
472
-            refreshOnFocus={true}
473
-            renderItem={this.getRenderItem}
474
-            itemHeight={FEED_ITEM_HEIGHT}
475
-            onScroll={this.onScroll}
476
-            renderSectionHeader={this.getRenderSectionHeader}
477
-            renderListHeaderComponent={this.getListHeader}
478
-          />
479
-        </View>
480
-        {!this.isLoggedIn ? (
481
-          <MascotPopup
482
-            prefKey={AsyncStorageManager.PREFERENCES.homeShowMascot.key}
483
-            title={i18n.t('screens.home.mascotDialog.title')}
484
-            message={i18n.t('screens.home.mascotDialog.message')}
485
-            icon="human-greeting"
486
-            buttons={{
487
-              action: {
488
-                message: i18n.t('screens.home.mascotDialog.login'),
489
-                icon: 'login',
490
-                onPress: this.onLogin,
491
-              },
492
-              cancel: {
493
-                message: i18n.t('screens.home.mascotDialog.later'),
494
-                icon: 'close',
495
-                color: props.theme.colors.warning,
496
-              },
497
-            }}
498
-            emotion={MASCOT_STYLE.CUTE}
499
-          />
500
-        ) : null}
501
-        <AnimatedFAB
502
-          ref={this.fabRef}
503
-          icon="qrcode-scan"
504
-          onPress={this.openScanner}
505
-        />
506
-        <LogoutDialog
507
-          visible={state.dialogVisible}
508
-          onDismiss={this.hideDisconnectDialog}
406
+  return (
407
+    <View style={GENERAL_STYLES.flex}>
408
+      <View style={styles.content}>
409
+        <WebSectionList
410
+          request={() => readData<RawDashboardType>(Urls.app.dashboard)}
411
+          createDataset={createDataset}
412
+          autoRefreshTime={REFRESH_TIME}
413
+          refreshOnFocus={true}
414
+          renderItem={getRenderItem}
415
+          itemHeight={FEED_ITEM_HEIGHT}
416
+          onScroll={onScroll}
417
+          renderSectionHeader={getRenderSectionHeader}
418
+          renderListHeaderComponent={getListHeader}
509 419
         />
510 420
       </View>
511
-    );
512
-  }
421
+      {!isLoggedIn ? (
422
+        <MascotPopup
423
+          title={i18n.t('screens.home.mascotDialog.title')}
424
+          message={i18n.t('screens.home.mascotDialog.message')}
425
+          icon="human-greeting"
426
+          buttons={{
427
+            action: {
428
+              message: i18n.t('screens.home.mascotDialog.login'),
429
+              icon: 'login',
430
+              onPress: onLogin,
431
+            },
432
+            cancel: {
433
+              message: i18n.t('screens.home.mascotDialog.later'),
434
+              icon: 'close',
435
+              color: theme.colors.warning,
436
+            },
437
+          }}
438
+          emotion={MASCOT_STYLE.CUTE}
439
+        />
440
+      ) : null}
441
+      <AnimatedFAB ref={fabRef} icon="qrcode-scan" onPress={openScanner} />
442
+      <LogoutDialog visible={dialogVisible} onDismiss={hideDisconnectDialog} />
443
+    </View>
444
+  );
513 445
 }
514 446
 
515
-export default withTheme(HomeScreen);
447
+export default HomeScreen;

+ 41
- 0
src/screens/Intro/IntroScreen.tsx View File

@@ -0,0 +1,41 @@
1
+import React from 'react';
2
+import CustomIntroSlider from '../../components/Overrides/CustomIntroSlider';
3
+import Update from '../../constants/Update';
4
+import { usePreferences } from '../../context/preferencesContext';
5
+import AprilFoolsManager from '../../managers/AprilFoolsManager';
6
+import {
7
+  getPreferenceBool,
8
+  getPreferenceNumber,
9
+  PreferenceKeys,
10
+} from '../../utils/asyncStorage';
11
+
12
+export default function IntroScreen() {
13
+  const { preferences, updatePreferences } = usePreferences();
14
+
15
+  const onDone = () => {
16
+    updatePreferences(PreferenceKeys.showIntro, false);
17
+    updatePreferences(PreferenceKeys.updateNumber, Update.number);
18
+    updatePreferences(PreferenceKeys.showAprilFoolsStart, false);
19
+  };
20
+
21
+  const showIntro =
22
+    getPreferenceBool(PreferenceKeys.showIntro, preferences) !== false;
23
+
24
+  const isUpdate =
25
+    getPreferenceNumber(PreferenceKeys.updateNumber, preferences) !==
26
+      Update.number && !showIntro;
27
+
28
+  const isAprilFools =
29
+    AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
30
+    getPreferenceBool(PreferenceKeys.showAprilFoolsStart, preferences) !==
31
+      false &&
32
+    !showIntro;
33
+
34
+  return (
35
+    <CustomIntroSlider
36
+      onDone={onDone}
37
+      isUpdate={isUpdate}
38
+      isAprilFools={isAprilFools}
39
+    />
40
+  );
41
+}

+ 61
- 0
src/screens/MainApp.tsx View File

@@ -0,0 +1,61 @@
1
+import React, { Ref, useEffect } from 'react';
2
+import {
3
+  NavigationContainer,
4
+  NavigationContainerRef,
5
+} from '@react-navigation/native';
6
+import { Provider as PaperProvider } from 'react-native-paper';
7
+import GENERAL_STYLES from '../constants/Styles';
8
+import CollapsibleProvider from '../components/providers/CollapsibleProvider';
9
+import CacheProvider from '../components/providers/CacheProvider';
10
+import { OverflowMenuProvider } from 'react-navigation-header-buttons';
11
+import MainNavigator from '../navigation/MainNavigator';
12
+import { Platform, SafeAreaView, View } from 'react-native';
13
+import { useDarkTheme } from '../context/preferencesContext';
14
+import { CustomDarkTheme, CustomWhiteTheme } from '../utils/Themes';
15
+import { setupStatusBar } from '../utils/Utils';
16
+
17
+type Props = {
18
+  defaultHomeRoute: string | null;
19
+  defaultHomeData: { [key: string]: string };
20
+};
21
+
22
+function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) {
23
+  const darkTheme = useDarkTheme();
24
+  const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme;
25
+
26
+  useEffect(() => {
27
+    if (Platform.OS === 'ios') {
28
+      setTimeout(setupStatusBar, 1000);
29
+    } else {
30
+      setupStatusBar(theme);
31
+    }
32
+  }, [theme]);
33
+
34
+  return (
35
+    <PaperProvider theme={theme}>
36
+      <CollapsibleProvider>
37
+        <CacheProvider>
38
+          <OverflowMenuProvider>
39
+            <View
40
+              style={{
41
+                backgroundColor: theme.colors.background,
42
+                ...GENERAL_STYLES.flex,
43
+              }}
44
+            >
45
+              <SafeAreaView style={GENERAL_STYLES.flex}>
46
+                <NavigationContainer theme={theme} ref={ref}>
47
+                  <MainNavigator
48
+                    defaultHomeRoute={props.defaultHomeRoute}
49
+                    defaultHomeData={props.defaultHomeData}
50
+                  />
51
+                </NavigationContainer>
52
+              </SafeAreaView>
53
+            </View>
54
+          </OverflowMenuProvider>
55
+        </CacheProvider>
56
+      </CollapsibleProvider>
57
+    </PaperProvider>
58
+  );
59
+}
60
+
61
+export default React.forwardRef(MainApp);

+ 47
- 86
src/screens/Other/Settings/DashboardEditScreen.tsx View File

@@ -17,31 +17,21 @@
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 { StackNavigationProp } from '@react-navigation/stack';
20
+import React, { useRef, useState } from 'react';
22 21
 import { Button, Card, Paragraph } from 'react-native-paper';
23 22
 import { FlatList, StyleSheet } from 'react-native';
24 23
 import { View } from 'react-native-animatable';
25 24
 import i18n from 'i18n-js';
26
-import type {
27
-  ServiceCategoryType,
28
-  ServiceItemType,
29
-} from '../../../managers/ServicesManager';
30
-import DashboardManager from '../../../managers/DashboardManager';
31 25
 import DashboardEditAccordion from '../../../components/Lists/DashboardEdit/DashboardEditAccordion';
32 26
 import DashboardEditPreviewItem from '../../../components/Lists/DashboardEdit/DashboardEditPreviewItem';
33
-import AsyncStorageManager from '../../../managers/AsyncStorageManager';
34 27
 import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
35
-
36
-type PropsType = {
37
-  navigation: StackNavigationProp<any>;
38
-};
39
-
40
-type StateType = {
41
-  currentDashboard: Array<ServiceItemType | null>;
42
-  currentDashboardIdList: Array<string>;
43
-  activeItem: number;
44
-};
28
+import {
29
+  getCategories,
30
+  ServiceCategoryType,
31
+  ServiceItemType,
32
+} from '../../../utils/Services';
33
+import { useNavigation } from '@react-navigation/core';
34
+import { useCurrentDashboard } from '../../../context/preferencesContext';
45 35
 
46 36
 const styles = StyleSheet.create({
47 37
   dashboardContainer: {
@@ -71,85 +61,71 @@ const styles = StyleSheet.create({
71 61
 /**
72 62
  * Class defining the Settings screen. This screen shows controls to modify app preferences.
73 63
  */
74
-class DashboardEditScreen extends React.Component<PropsType, StateType> {
75
-  content: Array<ServiceCategoryType>;
76
-
77
-  initialDashboard: Array<ServiceItemType | null>;
78
-
79
-  initialDashboardIdList: Array<string>;
80
-
81
-  constructor(props: PropsType) {
82
-    super(props);
83
-    const dashboardManager = new DashboardManager(props.navigation);
84
-    this.initialDashboardIdList = AsyncStorageManager.getObject(
85
-      AsyncStorageManager.PREFERENCES.dashboardItems.key
86
-    );
87
-    this.initialDashboard = dashboardManager.getCurrentDashboard();
88
-    this.state = {
89
-      currentDashboard: [...this.initialDashboard],
90
-      currentDashboardIdList: [...this.initialDashboardIdList],
91
-      activeItem: 0,
92
-    };
93
-    this.content = dashboardManager.getCategories();
94
-  }
95
-
96
-  getDashboardRowRenderItem = ({
64
+function DashboardEditScreen() {
65
+  const navigation = useNavigation();
66
+
67
+  const {
68
+    currentDashboard,
69
+    currentDashboardIdList,
70
+    updateCurrentDashboard,
71
+  } = useCurrentDashboard();
72
+  const initialDashboard = useRef(currentDashboardIdList);
73
+  const [activeItem, setActiveItem] = useState(0);
74
+
75
+  const getDashboardRowRenderItem = ({
97 76
     item,
98 77
     index,
99 78
   }: {
100 79
     item: ServiceItemType | null;
101 80
     index: number;
102 81
   }) => {
103
-    const { activeItem } = this.state;
104 82
     return (
105 83
       <DashboardEditPreviewItem
106 84
         image={item?.image}
107 85
         onPress={() => {
108
-          this.setState({ activeItem: index });
86
+          setActiveItem(index);
109 87
         }}
110 88
         isActive={activeItem === index}
111 89
       />
112 90
     );
113 91
   };
114 92
 
115
-  getDashboard(content: Array<ServiceItemType | null>) {
93
+  const getDashboard = (content: Array<ServiceItemType | null>) => {
116 94
     return (
117 95
       <FlatList
118 96
         data={content}
119
-        extraData={this.state}
120
-        renderItem={this.getDashboardRowRenderItem}
97
+        extraData={activeItem}
98
+        renderItem={getDashboardRowRenderItem}
121 99
         horizontal
122 100
         contentContainerStyle={styles.dashboard}
123 101
       />
124 102
     );
125
-  }
103
+  };
126 104
 
127
-  getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
128
-    const { currentDashboardIdList } = this.state;
105
+  const getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
129 106
     return (
130 107
       <DashboardEditAccordion
131 108
         item={item}
132
-        onPress={this.updateDashboard}
109
+        onPress={updateDashboard}
133 110
         activeDashboard={currentDashboardIdList}
134 111
       />
135 112
     );
136 113
   };
137 114
 
138
-  getListHeader() {
139
-    const { currentDashboard } = this.state;
115
+  const getListHeader = () => {
140 116
     return (
141 117
       <Card style={styles.card}>
142 118
         <Card.Content>
143 119
           <View style={styles.buttonContainer}>
144 120
             <Button
145
-              mode="contained"
146
-              onPress={this.undoDashboard}
121
+              mode={'contained'}
122
+              onPress={undoDashboard}
147 123
               style={styles.button}
148 124
             >
149 125
               {i18n.t('screens.settings.dashboardEdit.undo')}
150 126
             </Button>
151 127
             <View style={styles.dashboardContainer}>
152
-              {this.getDashboard(currentDashboard)}
128
+              {getDashboard(currentDashboard)}
153 129
             </View>
154 130
           </View>
155 131
           <Paragraph style={styles.text}>
@@ -158,43 +134,28 @@ class DashboardEditScreen extends React.Component<PropsType, StateType> {
158 134
         </Card.Content>
159 135
       </Card>
160 136
     );
161
-  }
137
+  };
162 138
 
163
-  updateDashboard = (service: ServiceItemType) => {
164
-    const { currentDashboard, currentDashboardIdList, activeItem } = this.state;
165
-    currentDashboard[activeItem] = service;
166
-    currentDashboardIdList[activeItem] = service.key;
167
-    this.setState({
168
-      currentDashboard,
169
-      currentDashboardIdList,
170
-    });
171
-    AsyncStorageManager.set(
172
-      AsyncStorageManager.PREFERENCES.dashboardItems.key,
173
-      currentDashboardIdList
139
+  const updateDashboard = (service: ServiceItemType) => {
140
+    updateCurrentDashboard(
141
+      currentDashboardIdList.map((id, index) =>
142
+        index === activeItem ? service.key : id
143
+      )
174 144
     );
175 145
   };
176 146
 
177
-  undoDashboard = () => {
178
-    this.setState({
179
-      currentDashboard: [...this.initialDashboard],
180
-      currentDashboardIdList: [...this.initialDashboardIdList],
181
-    });
182
-    AsyncStorageManager.set(
183
-      AsyncStorageManager.PREFERENCES.dashboardItems.key,
184
-      this.initialDashboardIdList
185
-    );
147
+  const undoDashboard = () => {
148
+    updateCurrentDashboard(initialDashboard.current);
186 149
   };
187 150
 
188
-  render() {
189
-    return (
190
-      <CollapsibleFlatList
191
-        data={this.content}
192
-        renderItem={this.getRenderItem}
193
-        ListHeaderComponent={this.getListHeader()}
194
-        style={{}}
195
-      />
196
-    );
197
-  }
151
+  return (
152
+    <CollapsibleFlatList
153
+      data={getCategories(navigation.navigate)}
154
+      renderItem={getRenderItem}
155
+      ListHeaderComponent={getListHeader()}
156
+      style={{}}
157
+    />
158
+  );
198 159
 }
199 160
 
200 161
 export default DashboardEditScreen;

+ 170
- 252
src/screens/Other/Settings/SettingsScreen.tsx View File

@@ -26,28 +26,20 @@ import {
26 26
   List,
27 27
   Switch,
28 28
   ToggleButton,
29
-  withTheme,
29
+  useTheme,
30 30
 } from 'react-native-paper';
31 31
 import { Appearance } from 'react-native-appearance';
32
-import { StackNavigationProp } from '@react-navigation/stack';
33
-import ThemeManager from '../../../managers/ThemeManager';
34
-import AsyncStorageManager from '../../../managers/AsyncStorageManager';
35 32
 import CustomSlider from '../../../components/Overrides/CustomSlider';
36 33
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
37 34
 import GENERAL_STYLES from '../../../constants/Styles';
38
-
39
-type PropsType = {
40
-  navigation: StackNavigationProp<any>;
41
-  theme: ReactNativePaper.Theme;
42
-};
43
-
44
-type StateType = {
45
-  nightMode: boolean;
46
-  nightModeFollowSystem: boolean;
47
-  startScreenPickerSelected: string;
48
-  selectedWash: string;
49
-  isDebugUnlocked: boolean;
50
-};
35
+import { usePreferences } from '../../../context/preferencesContext';
36
+import { useNavigation } from '@react-navigation/core';
37
+import {
38
+  getPreferenceBool,
39
+  getPreferenceNumber,
40
+  getPreferenceString,
41
+  PreferenceKeys,
42
+} from '../../../utils/asyncStorage';
51 43
 
52 44
 const styles = StyleSheet.create({
53 45
   slider: {
@@ -66,98 +58,67 @@ const styles = StyleSheet.create({
66 58
 /**
67 59
  * Class defining the Settings screen. This screen shows controls to modify app preferences.
68 60
  */
69
-class SettingsScreen extends React.Component<PropsType, StateType> {
70
-  savedNotificationReminder: number;
71
-
72
-  /**
73
-   * Loads user preferences into state
74
-   */
75
-  constructor(props: PropsType) {
76
-    super(props);
77
-    const notifReminder = AsyncStorageManager.getString(
78
-      AsyncStorageManager.PREFERENCES.proxiwashNotifications.key
79
-    );
80
-    this.savedNotificationReminder = parseInt(notifReminder, 10);
81
-    if (Number.isNaN(this.savedNotificationReminder)) {
82
-      this.savedNotificationReminder = 0;
83
-    }
61
+function SettingsScreen() {
62
+  const navigation = useNavigation();
63
+  const theme = useTheme();
64
+  const { preferences, updatePreferences } = usePreferences();
84 65
 
85
-    this.state = {
86
-      nightMode: ThemeManager.getNightMode(),
87
-      nightModeFollowSystem:
88
-        AsyncStorageManager.getBool(
89
-          AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key
90
-        ) && Appearance.getColorScheme() !== 'no-preference',
91
-      startScreenPickerSelected: AsyncStorageManager.getString(
92
-        AsyncStorageManager.PREFERENCES.defaultStartScreen.key
93
-      ),
94
-      selectedWash: AsyncStorageManager.getString(
95
-        AsyncStorageManager.PREFERENCES.selectedWash.key
96
-      ),
97
-      isDebugUnlocked: AsyncStorageManager.getBool(
98
-        AsyncStorageManager.PREFERENCES.debugUnlocked.key
99
-      ),
100
-    };
101
-  }
66
+  const nightMode = getPreferenceBool(
67
+    PreferenceKeys.nightMode,
68
+    preferences
69
+  ) as boolean;
70
+  const nightModeFollowSystem =
71
+    (getPreferenceBool(
72
+      PreferenceKeys.nightModeFollowSystem,
73
+      preferences
74
+    ) as boolean) && Appearance.getColorScheme() !== 'no-preference';
75
+  const startScreenPickerSelected = getPreferenceString(
76
+    PreferenceKeys.defaultStartScreen,
77
+    preferences
78
+  ) as string;
79
+  const selectedWash = getPreferenceString(
80
+    PreferenceKeys.selectedWash,
81
+    preferences
82
+  ) as string;
83
+  const isDebugUnlocked = getPreferenceBool(
84
+    PreferenceKeys.debugUnlocked,
85
+    preferences
86
+  ) as boolean;
87
+  const notif = getPreferenceNumber(
88
+    PreferenceKeys.proxiwashNotifications,
89
+    preferences
90
+  );
91
+  const savedNotificationReminder = !notif || Number.isNaN(notif) ? 0 : notif;
102 92
 
103
-  /**
104
-   * Saves the value for the proxiwash reminder notification time
105
-   *
106
-   * @param value The value to store
107
-   */
108
-  onProxiwashNotifPickerValueChange = (value: number) => {
109
-    AsyncStorageManager.set(
110
-      AsyncStorageManager.PREFERENCES.proxiwashNotifications.key,
111
-      value
112
-    );
93
+  const onProxiwashNotifPickerValueChange = (value: number) => {
94
+    updatePreferences(PreferenceKeys.proxiwashNotifications, value);
113 95
   };
114 96
 
115
-  /**
116
-   * Saves the value for the proxiwash reminder notification time
117
-   *
118
-   * @param value The value to store
119
-   */
120
-  onStartScreenPickerValueChange = (value: string) => {
97
+  const onStartScreenPickerValueChange = (value: string) => {
121 98
     if (value != null) {
122
-      this.setState({ startScreenPickerSelected: value });
123
-      AsyncStorageManager.set(
124
-        AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
125
-        value
126
-      );
99
+      updatePreferences(PreferenceKeys.defaultStartScreen, value);
127 100
     }
128 101
   };
129 102
 
130
-  /**
131
-   * Returns a picker allowing the user to select the proxiwash reminder notification time
132
-   *
133
-   * @returns {React.Node}
134
-   */
135
-  getProxiwashNotifPicker() {
136
-    const { theme } = this.props;
103
+  const getProxiwashNotifPicker = () => {
137 104
     return (
138 105
       <CustomSlider
139 106
         style={styles.slider}
140 107
         minimumValue={0}
141 108
         maximumValue={10}
142 109
         step={1}
143
-        value={this.savedNotificationReminder}
144
-        onValueChange={this.onProxiwashNotifPickerValueChange}
110
+        value={savedNotificationReminder}
111
+        onValueChange={onProxiwashNotifPickerValueChange}
145 112
         thumbTintColor={theme.colors.primary}
146 113
         minimumTrackTintColor={theme.colors.primary}
147 114
       />
148 115
     );
149
-  }
116
+  };
150 117
 
151
-  /**
152
-   * Returns a radio picker allowing the user to select the proxiwash
153
-   *
154
-   * @returns {React.Node}
155
-   */
156
-  getProxiwashChangePicker() {
157
-    const { selectedWash } = this.state;
118
+  const getProxiwashChangePicker = () => {
158 119
     return (
159 120
       <RadioButton.Group
160
-        onValueChange={this.onSelectWashValueChange}
121
+        onValueChange={onSelectWashValueChange}
161 122
         value={selectedWash}
162 123
       >
163 124
         <RadioButton.Item
@@ -170,18 +131,12 @@ class SettingsScreen extends React.Component<PropsType, StateType> {
170 131
         />
171 132
       </RadioButton.Group>
172 133
     );
173
-  }
134
+  };
174 135
 
175
-  /**
176
-   * Returns a picker allowing the user to select the start screen
177
-   *
178
-   * @returns {React.Node}
179
-   */
180
-  getStartScreenPicker() {
181
-    const { startScreenPickerSelected } = this.state;
136
+  const getStartScreenPicker = () => {
182 137
     return (
183 138
       <ToggleButton.Row
184
-        onValueChange={this.onStartScreenPickerValueChange}
139
+        onValueChange={onStartScreenPickerValueChange}
185 140
         value={startScreenPickerSelected}
186 141
         style={GENERAL_STYLES.centerHorizontal}
187 142
       >
@@ -192,30 +147,17 @@ class SettingsScreen extends React.Component<PropsType, StateType> {
192 147
         <ToggleButton icon="clock" value="planex" />
193 148
       </ToggleButton.Row>
194 149
     );
195
-  }
150
+  };
196 151
 
197
-  /**
198
-   * Toggles night mode and saves it to preferences
199
-   */
200
-  onToggleNightMode = () => {
201
-    const { nightMode } = this.state;
202
-    ThemeManager.getInstance().setNightMode(!nightMode);
203
-    this.setState({ nightMode: !nightMode });
152
+  const onToggleNightMode = () => {
153
+    updatePreferences(PreferenceKeys.nightMode, !nightMode);
204 154
   };
205 155
 
206
-  onToggleNightModeFollowSystem = () => {
207
-    const { nightModeFollowSystem } = this.state;
208
-    const value = !nightModeFollowSystem;
209
-    this.setState({ nightModeFollowSystem: value });
210
-    AsyncStorageManager.set(
211
-      AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
212
-      value
156
+  const onToggleNightModeFollowSystem = () => {
157
+    updatePreferences(
158
+      PreferenceKeys.nightModeFollowSystem,
159
+      !nightModeFollowSystem
213 160
     );
214
-    if (value) {
215
-      const nightMode = Appearance.getColorScheme() === 'dark';
216
-      ThemeManager.getInstance().setNightMode(nightMode);
217
-      this.setState({ nightMode });
218
-    }
219 161
   };
220 162
 
221 163
   /**
@@ -228,13 +170,13 @@ class SettingsScreen extends React.Component<PropsType, StateType> {
228 170
    * @param state The current state of the switch
229 171
    * @returns {React.Node}
230 172
    */
231
-  static getToggleItem(
173
+  const getToggleItem = (
232 174
     onPressCallback: () => void,
233 175
     icon: string,
234 176
     title: string,
235 177
     subtitle: string,
236 178
     state: boolean
237
-  ) {
179
+  ) => {
238 180
     return (
239 181
       <List.Item
240 182
         title={title}
@@ -245,16 +187,15 @@ class SettingsScreen extends React.Component<PropsType, StateType> {
245 187
         right={() => <Switch value={state} onValueChange={onPressCallback} />}
246 188
       />
247 189
     );
248
-  }
190
+  };
249 191
 
250
-  getNavigateItem(
192
+  const getNavigateItem = (
251 193
     route: string,
252 194
     icon: string,
253 195
     title: string,
254 196
     subtitle: string,
255 197
     onLongPress?: () => void
256
-  ) {
257
-    const { navigation } = this.props;
198
+  ) => {
258 199
     return (
259 200
       <List.Item
260 201
         title={title}
@@ -275,144 +216,121 @@ class SettingsScreen extends React.Component<PropsType, StateType> {
275 216
         onLongPress={onLongPress}
276 217
       />
277 218
     );
278
-  }
219
+  };
279 220
 
280
-  /**
281
-   * Saves the value for the proxiwash selected wash
282
-   *
283
-   * @param value The value to store
284
-   */
285
-  onSelectWashValueChange = (value: string) => {
221
+  const onSelectWashValueChange = (value: string) => {
286 222
     if (value != null) {
287
-      this.setState({ selectedWash: value });
288
-      AsyncStorageManager.set(
289
-        AsyncStorageManager.PREFERENCES.selectedWash.key,
290
-        value
291
-      );
223
+      updatePreferences(PreferenceKeys.selectedWash, value);
292 224
     }
293 225
   };
294 226
 
295
-  /**
296
-   * Unlocks debug mode and saves its state to user preferences
297
-   */
298
-  unlockDebugMode = () => {
299
-    this.setState({ isDebugUnlocked: true });
300
-    AsyncStorageManager.set(
301
-      AsyncStorageManager.PREFERENCES.debugUnlocked.key,
302
-      true
303
-    );
227
+  const unlockDebugMode = () => {
228
+    updatePreferences(PreferenceKeys.debugUnlocked, true);
304 229
   };
305 230
 
306
-  render() {
307
-    const { nightModeFollowSystem, nightMode, isDebugUnlocked } = this.state;
308
-    return (
309
-      <CollapsibleScrollView>
310
-        <Card style={styles.card}>
311
-          <Card.Title title={i18n.t('screens.settings.generalCard')} />
312
-          <List.Section>
313
-            {Appearance.getColorScheme() !== 'no-preference'
314
-              ? SettingsScreen.getToggleItem(
315
-                  this.onToggleNightModeFollowSystem,
316
-                  'theme-light-dark',
317
-                  i18n.t('screens.settings.nightModeAuto'),
318
-                  i18n.t('screens.settings.nightModeAutoSub'),
319
-                  nightModeFollowSystem
320
-                )
321
-              : null}
322
-            {Appearance.getColorScheme() === 'no-preference' ||
323
-            !nightModeFollowSystem
324
-              ? SettingsScreen.getToggleItem(
325
-                  this.onToggleNightMode,
326
-                  'theme-light-dark',
327
-                  i18n.t('screens.settings.nightMode'),
328
-                  nightMode
329
-                    ? i18n.t('screens.settings.nightModeSubOn')
330
-                    : i18n.t('screens.settings.nightModeSubOff'),
331
-                  nightMode
332
-                )
333
-              : null}
334
-            <List.Item
335
-              title={i18n.t('screens.settings.startScreen')}
336
-              description={i18n.t('screens.settings.startScreenSub')}
337
-              left={(props) => (
338
-                <List.Icon
339
-                  color={props.color}
340
-                  style={props.style}
341
-                  icon="power"
342
-                />
343
-              )}
344
-            />
345
-            {this.getStartScreenPicker()}
346
-            {this.getNavigateItem(
347
-              'dashboard-edit',
348
-              'view-dashboard',
349
-              i18n.t('screens.settings.dashboard'),
350
-              i18n.t('screens.settings.dashboardSub')
231
+  return (
232
+    <CollapsibleScrollView>
233
+      <Card style={styles.card}>
234
+        <Card.Title title={i18n.t('screens.settings.generalCard')} />
235
+        <List.Section>
236
+          {Appearance.getColorScheme() !== 'no-preference'
237
+            ? getToggleItem(
238
+                onToggleNightModeFollowSystem,
239
+                'theme-light-dark',
240
+                i18n.t('screens.settings.nightModeAuto'),
241
+                i18n.t('screens.settings.nightModeAutoSub'),
242
+                nightModeFollowSystem
243
+              )
244
+            : null}
245
+          {Appearance.getColorScheme() === 'no-preference' ||
246
+          !nightModeFollowSystem
247
+            ? getToggleItem(
248
+                onToggleNightMode,
249
+                'theme-light-dark',
250
+                i18n.t('screens.settings.nightMode'),
251
+                nightMode
252
+                  ? i18n.t('screens.settings.nightModeSubOn')
253
+                  : i18n.t('screens.settings.nightModeSubOff'),
254
+                nightMode
255
+              )
256
+            : null}
257
+          <List.Item
258
+            title={i18n.t('screens.settings.startScreen')}
259
+            description={i18n.t('screens.settings.startScreenSub')}
260
+            left={(props) => (
261
+              <List.Icon color={props.color} style={props.style} icon="power" />
351 262
             )}
352
-          </List.Section>
353
-        </Card>
354
-        <Card style={styles.card}>
355
-          <Card.Title title="Proxiwash" />
356
-          <List.Section>
357
-            <List.Item
358
-              title={i18n.t('screens.settings.proxiwashNotifReminder')}
359
-              description={i18n.t('screens.settings.proxiwashNotifReminderSub')}
360
-              left={(props) => (
361
-                <List.Icon
362
-                  color={props.color}
363
-                  style={props.style}
364
-                  icon="washing-machine"
365
-                />
366
-              )}
367
-            />
368
-            <View style={styles.pickerContainer}>
369
-              {this.getProxiwashNotifPicker()}
370
-            </View>
371
-            <List.Item
372
-              title={i18n.t('screens.settings.proxiwashChangeWash')}
373
-              description={i18n.t('screens.settings.proxiwashChangeWashSub')}
374
-              left={(props) => (
375
-                <List.Icon
376
-                  color={props.color}
377
-                  style={props.style}
378
-                  icon="washing-machine"
379
-                />
380
-              )}
381
-            />
382
-            <View style={styles.pickerContainer}>
383
-              {this.getProxiwashChangePicker()}
384
-            </View>
385
-          </List.Section>
386
-        </Card>
387
-        <Card style={styles.card}>
388
-          <Card.Title title={i18n.t('screens.settings.information')} />
389
-          <List.Section>
390
-            {isDebugUnlocked
391
-              ? this.getNavigateItem(
392
-                  'debug',
393
-                  'bug-check',
394
-                  i18n.t('screens.debug.title'),
395
-                  ''
396
-                )
397
-              : null}
398
-            {this.getNavigateItem(
399
-              'about',
400
-              'information',
401
-              i18n.t('screens.about.title'),
402
-              i18n.t('screens.about.buttonDesc'),
403
-              this.unlockDebugMode
263
+          />
264
+          {getStartScreenPicker()}
265
+          {getNavigateItem(
266
+            'dashboard-edit',
267
+            'view-dashboard',
268
+            i18n.t('screens.settings.dashboard'),
269
+            i18n.t('screens.settings.dashboardSub')
270
+          )}
271
+        </List.Section>
272
+      </Card>
273
+      <Card style={styles.card}>
274
+        <Card.Title title="Proxiwash" />
275
+        <List.Section>
276
+          <List.Item
277
+            title={i18n.t('screens.settings.proxiwashNotifReminder')}
278
+            description={i18n.t('screens.settings.proxiwashNotifReminderSub')}
279
+            left={(props) => (
280
+              <List.Icon
281
+                color={props.color}
282
+                style={props.style}
283
+                icon="washing-machine"
284
+              />
404 285
             )}
405
-            {this.getNavigateItem(
406
-              'feedback',
407
-              'comment-quote',
408
-              i18n.t('screens.feedback.homeButtonTitle'),
409
-              i18n.t('screens.feedback.homeButtonSubtitle')
286
+          />
287
+          <View style={styles.pickerContainer}>
288
+            {getProxiwashNotifPicker()}
289
+          </View>
290
+          <List.Item
291
+            title={i18n.t('screens.settings.proxiwashChangeWash')}
292
+            description={i18n.t('screens.settings.proxiwashChangeWashSub')}
293
+            left={(props) => (
294
+              <List.Icon
295
+                color={props.color}
296
+                style={props.style}
297
+                icon="washing-machine"
298
+              />
410 299
             )}
411
-          </List.Section>
412
-        </Card>
413
-      </CollapsibleScrollView>
414
-    );
415
-  }
300
+          />
301
+          <View style={styles.pickerContainer}>
302
+            {getProxiwashChangePicker()}
303
+          </View>
304
+        </List.Section>
305
+      </Card>
306
+      <Card style={styles.card}>
307
+        <Card.Title title={i18n.t('screens.settings.information')} />
308
+        <List.Section>
309
+          {isDebugUnlocked
310
+            ? getNavigateItem(
311
+                'debug',
312
+                'bug-check',
313
+                i18n.t('screens.debug.title'),
314
+                ''
315
+              )
316
+            : null}
317
+          {getNavigateItem(
318
+            'about',
319
+            'information',
320
+            i18n.t('screens.about.title'),
321
+            i18n.t('screens.about.buttonDesc'),
322
+            unlockDebugMode
323
+          )}
324
+          {getNavigateItem(
325
+            'feedback',
326
+            'comment-quote',
327
+            i18n.t('screens.feedback.homeButtonTitle'),
328
+            i18n.t('screens.feedback.homeButtonSubtitle')
329
+          )}
330
+        </List.Section>
331
+      </Card>
332
+    </CollapsibleScrollView>
333
+  );
416 334
 }
417 335
 
418
-export default withTheme(SettingsScreen);
336
+export default SettingsScreen;

+ 27
- 26
src/screens/Planex/GroupSelectionScreen.tsx View File

@@ -17,23 +17,19 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import React, {
21
-  useCallback,
22
-  useEffect,
23
-  useLayoutEffect,
24
-  useState,
25
-} from 'react';
20
+import React, { useCallback, useLayoutEffect, useState } from 'react';
26 21
 import { Platform } from 'react-native';
27 22
 import i18n from 'i18n-js';
28 23
 import { Searchbar } from 'react-native-paper';
29 24
 import { stringMatchQuery } from '../../utils/Search';
30 25
 import WebSectionList from '../../components/Screens/WebSectionList';
31 26
 import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion';
32
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
33 27
 import Urls from '../../constants/Urls';
34 28
 import { readData } from '../../utils/WebData';
35 29
 import { useNavigation } from '@react-navigation/core';
36 30
 import { useCachedPlanexGroups } from '../../context/cacheContext';
31
+import { usePreferences } from '../../context/preferencesContext';
32
+import { getPreferenceObject, PreferenceKeys } from '../../utils/asyncStorage';
37 33
 
38 34
 export type PlanexGroupType = {
39 35
   name: string;
@@ -63,13 +59,23 @@ function sortName(
63 59
 
64 60
 function GroupSelectionScreen() {
65 61
   const navigation = useNavigation();
62
+  const { preferences, updatePreferences } = usePreferences();
66 63
   const { groups, setGroups } = useCachedPlanexGroups();
67 64
   const [currentSearchString, setCurrentSearchString] = useState('');
68
-  const [favoriteGroups, setFavoriteGroups] = useState<Array<PlanexGroupType>>(
69
-    AsyncStorageManager.getObject(
70
-      AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key
71
-    )
72
-  );
65
+
66
+  const getFavoriteGroups = (): Array<PlanexGroupType> => {
67
+    const data = getPreferenceObject(
68
+      PreferenceKeys.planexFavoriteGroups,
69
+      preferences
70
+    );
71
+    if (data) {
72
+      return data as Array<PlanexGroupType>;
73
+    } else {
74
+      return [];
75
+    }
76
+  };
77
+
78
+  const favoriteGroups = getFavoriteGroups();
73 79
 
74 80
   useLayoutEffect(() => {
75 81
     navigation.setOptions({
@@ -140,10 +146,8 @@ function GroupSelectionScreen() {
140 146
    * @param item The article pressed
141 147
    */
142 148
   const onListItemPress = (item: PlanexGroupType) => {
143
-    navigation.navigate('planex', {
144
-      screen: 'index',
145
-      params: { group: item },
146
-    });
149
+    updatePreferences(PreferenceKeys.planexCurrentGroup, item);
150
+    navigation.goBack();
147 151
   };
148 152
 
149 153
   /**
@@ -153,12 +157,16 @@ function GroupSelectionScreen() {
153 157
    */
154 158
   const onListFavoritePress = useCallback(
155 159
     (group: PlanexGroupType) => {
160
+      const updateFavorites = (newValue: Array<PlanexGroupType>) => {
161
+        updatePreferences(PreferenceKeys.planexFavoriteGroups, newValue);
162
+      };
163
+
156 164
       const removeGroupFromFavorites = (g: PlanexGroupType) => {
157
-        setFavoriteGroups(favoriteGroups.filter((f) => f.id !== g.id));
165
+        updateFavorites(favoriteGroups.filter((f) => f.id !== g.id));
158 166
       };
159 167
 
160 168
       const addGroupToFavorites = (g: PlanexGroupType) => {
161
-        setFavoriteGroups([...favoriteGroups, g].sort(sortName));
169
+        updateFavorites([...favoriteGroups, g].sort(sortName));
162 170
       };
163 171
 
164 172
       if (favoriteGroups.some((f) => f.id === group.id)) {
@@ -167,16 +175,9 @@ function GroupSelectionScreen() {
167 175
         addGroupToFavorites(group);
168 176
       }
169 177
     },
170
-    [favoriteGroups]
178
+    [favoriteGroups, updatePreferences]
171 179
   );
172 180
 
173
-  useEffect(() => {
174
-    AsyncStorageManager.set(
175
-      AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key,
176
-      favoriteGroups
177
-    );
178
-  }, [favoriteGroups]);
179
-
180 181
   /**
181 182
    * Generates the dataset to be used in the FlatList.
182 183
    * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top.

+ 27
- 52
src/screens/Planex/PlanexScreen.tsx View File

@@ -17,17 +17,12 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import React, { useCallback, useState } from 'react';
20
+import React, { useCallback, useEffect, useState } from 'react';
21 21
 import { Title, useTheme } from 'react-native-paper';
22 22
 import i18n from 'i18n-js';
23 23
 import { StyleSheet, View } from 'react-native';
24
-import {
25
-  CommonActions,
26
-  useFocusEffect,
27
-  useNavigation,
28
-} from '@react-navigation/native';
24
+import { useNavigation } from '@react-navigation/native';
29 25
 import Autolink from 'react-native-autolink';
30
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
31 26
 import AlertDialog from '../../components/Dialogs/AlertDialog';
32 27
 import { dateToString, getTimeOnlyString } from '../../utils/Planning';
33 28
 import DateManager from '../../managers/DateManager';
@@ -38,6 +33,8 @@ import { getPrettierPlanexGroupName } from '../../utils/Utils';
38 33
 import GENERAL_STYLES from '../../constants/Styles';
39 34
 import PlanexWebview from '../../components/Screens/PlanexWebview';
40 35
 import PlanexBottomBar from '../../components/Animations/PlanexBottomBar';
36
+import { usePreferences } from '../../context/preferencesContext';
37
+import { getPreferenceString, PreferenceKeys } from '../../utils/asyncStorage';
41 38
 
42 39
 const styles = StyleSheet.create({
43 40
   container: {
@@ -50,17 +47,10 @@ const styles = StyleSheet.create({
50 47
   },
51 48
 });
52 49
 
53
-type Props = {
54
-  route: {
55
-    params: {
56
-      group?: PlanexGroupType;
57
-    };
58
-  };
59
-};
60
-
61
-function PlanexScreen(props: Props) {
50
+function PlanexScreen() {
62 51
   const navigation = useNavigation();
63 52
   const theme = useTheme();
53
+  const { preferences } = usePreferences();
64 54
 
65 55
   const [dialogContent, setDialogContent] = useState<
66 56
     | undefined
@@ -72,12 +62,13 @@ function PlanexScreen(props: Props) {
72 62
   >();
73 63
   const [injectJS, setInjectJS] = useState('');
74 64
 
75
-  const getCurrentGroup = (): PlanexGroupType | undefined => {
76
-    let currentGroupString = AsyncStorageManager.getString(
77
-      AsyncStorageManager.PREFERENCES.planexCurrentGroup.key
65
+  const getCurrentGroup: () => PlanexGroupType | undefined = useCallback(() => {
66
+    let currentGroupString = getPreferenceString(
67
+      PreferenceKeys.planexCurrentGroup,
68
+      preferences
78 69
     );
79 70
     let group: PlanexGroupType;
80
-    if (currentGroupString !== '') {
71
+    if (currentGroupString) {
81 72
       group = JSON.parse(currentGroupString);
82 73
       navigation.setOptions({
83 74
         title: getPrettierPlanexGroupName(group.name),
@@ -85,22 +76,10 @@ function PlanexScreen(props: Props) {
85 76
       return group;
86 77
     }
87 78
     return undefined;
88
-  };
79
+  }, [navigation, preferences]);
89 80
 
90
-  const [currentGroup, setCurrentGroup] = useState<PlanexGroupType | undefined>(
91
-    getCurrentGroup()
92
-  );
81
+  const currentGroup = getCurrentGroup();
93 82
 
94
-  useFocusEffect(
95
-    useCallback(() => {
96
-      if (props.route.params?.group) {
97
-        // reset params to prevent infinite loop
98
-        selectNewGroup(props.route.params.group);
99
-        navigation.dispatch(CommonActions.setParams({ group: undefined }));
100
-      }
101
-      // eslint-disable-next-line react-hooks/exhaustive-deps
102
-    }, [props.route.params])
103
-  );
104 83
   /**
105 84
    * Gets the Webview, with an error view on top if no group is selected.
106 85
    *
@@ -194,21 +173,20 @@ function PlanexScreen(props: Props) {
194 173
 
195 174
   const hideDialog = () => setDialogContent(undefined);
196 175
 
197
-  /**
198
-   * Sends the webpage a message with the new group to select and save it to preferences
199
-   *
200
-   * @param group The group object selected
201
-   */
202
-  const selectNewGroup = (group: PlanexGroupType) => {
203
-    sendMessage('setGroup', group.id.toString());
204
-    setCurrentGroup(group);
205
-    AsyncStorageManager.set(
206
-      AsyncStorageManager.PREFERENCES.planexCurrentGroup.key,
207
-      group
208
-    );
176
+  useEffect(() => {
177
+    const group = getCurrentGroup();
178
+    if (group) {
179
+      sendMessage('setGroup', group.id.toString());
180
+      navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) });
181
+    }
182
+    // eslint-disable-next-line react-hooks/exhaustive-deps
183
+  }, [getCurrentGroup, navigation]);
209 184
 
210
-    navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) });
211
-  };
185
+  const showMascot =
186
+    getPreferenceString(
187
+      PreferenceKeys.defaultStartScreen,
188
+      preferences
189
+    )?.toLowerCase() !== 'planex';
212 190
 
213 191
   return (
214 192
     <View style={GENERAL_STYLES.flex}>
@@ -220,11 +198,8 @@ function PlanexScreen(props: Props) {
220 198
           <View style={GENERAL_STYLES.flex}>{getWebView()}</View>
221 199
         )}
222 200
       </View>
223
-      {AsyncStorageManager.getString(
224
-        AsyncStorageManager.PREFERENCES.defaultStartScreen.key
225
-      ).toLowerCase() !== 'planex' ? (
201
+      {showMascot ? (
226 202
         <MascotPopup
227
-          prefKey={AsyncStorageManager.PREFERENCES.planexShowMascot.key}
228 203
           title={i18n.t('screens.planex.mascotDialog.title')}
229 204
           message={i18n.t('screens.planex.mascotDialog.message')}
230 205
           icon="emoticon-kiss"

+ 0
- 2
src/screens/Planning/PlanningScreen.tsx View File

@@ -34,7 +34,6 @@ import {
34 34
 import CustomAgenda from '../../components/Overrides/CustomAgenda';
35 35
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
36 36
 import MascotPopup from '../../components/Mascot/MascotPopup';
37
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
38 37
 import GENERAL_STYLES from '../../constants/Styles';
39 38
 import Urls from '../../constants/Urls';
40 39
 
@@ -291,7 +290,6 @@ class PlanningScreen extends React.Component<PropsType, StateType> {
291 290
           }
292 291
         />
293 292
         <MascotPopup
294
-          prefKey={AsyncStorageManager.PREFERENCES.eventsShowMascot.key}
295 293
           title={i18n.t('screens.planning.mascotDialog.title')}
296 294
           message={i18n.t('screens.planning.mascotDialog.message')}
297 295
           icon="party-popper"

+ 35
- 32
src/screens/Proxiwash/ProxiwashScreen.tsx View File

@@ -17,7 +17,7 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
20
+import React, { useLayoutEffect, useRef, useState } from 'react';
21 21
 import {
22 22
   SectionListData,
23 23
   SectionListRenderItemInfo,
@@ -28,7 +28,6 @@ import i18n from 'i18n-js';
28 28
 import { Avatar, Button, Card, Text, useTheme } from 'react-native-paper';
29 29
 import { Modalize } from 'react-native-modalize';
30 30
 import WebSectionList from '../../components/Screens/WebSectionList';
31
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
32 31
 import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem';
33 32
 import ProxiwashConstants, {
34 33
   MachineStates,
@@ -50,9 +49,16 @@ import type { SectionListDataType } from '../../components/Screens/WebSectionLis
50 49
 import type { LaundromatType } from './ProxiwashAboutScreen';
51 50
 import GENERAL_STYLES from '../../constants/Styles';
52 51
 import { readData } from '../../utils/WebData';
53
-import { useFocusEffect, useNavigation } from '@react-navigation/core';
52
+import { useNavigation } from '@react-navigation/core';
54 53
 import { setupMachineNotification } from '../../utils/Notifications';
55 54
 import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader';
55
+import { usePreferences } from '../../context/preferencesContext';
56
+import {
57
+  getPreferenceNumber,
58
+  getPreferenceObject,
59
+  getPreferenceString,
60
+  PreferenceKeys,
61
+} from '../../utils/asyncStorage';
56 62
 
57 63
 const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
58 64
 const LIST_ITEM_HEIGHT = 64;
@@ -91,23 +97,35 @@ const styles = StyleSheet.create({
91 97
 function ProxiwashScreen() {
92 98
   const navigation = useNavigation();
93 99
   const theme = useTheme();
100
+  const { preferences, updatePreferences } = usePreferences();
94 101
   const [
95 102
     modalCurrentDisplayItem,
96 103
     setModalCurrentDisplayItem,
97 104
   ] = useState<React.ReactElement | null>(null);
98
-  const [machinesWatched, setMachinesWatched] = useState<
99
-    Array<ProxiwashMachineType>
100
-  >(
101
-    AsyncStorageManager.getObject(
102
-      AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
103
-    )
105
+  const reminder = getPreferenceNumber(
106
+    PreferenceKeys.proxiwashNotifications,
107
+    preferences
104 108
   );
105 109
 
106
-  const [selectedWash, setSelectedWash] = useState(
107
-    AsyncStorageManager.getString(
108
-      AsyncStorageManager.PREFERENCES.selectedWash.key
109
-    ) as 'tripodeB' | 'washinsa'
110
-  );
110
+  const getMachinesWatched = () => {
111
+    const data = getPreferenceObject(
112
+      PreferenceKeys.proxiwashWatchedMachines,
113
+      preferences
114
+    ) as Array<ProxiwashMachineType>;
115
+    return data ? (data as Array<ProxiwashMachineType>) : [];
116
+  };
117
+
118
+  const getSelectedWash = () => {
119
+    const data = getPreferenceString(PreferenceKeys.selectedWash, preferences);
120
+    if (data !== 'washinsa' && data !== 'tripodeB') {
121
+      return 'washinsa';
122
+    } else {
123
+      return data;
124
+    }
125
+  };
126
+
127
+  const machinesWatched: Array<ProxiwashMachineType> = getMachinesWatched();
128
+  const selectedWash: 'washinsa' | 'tripodeB' = getSelectedWash();
111 129
 
112 130
   const modalStateStrings: { [key in MachineStates]: string } = {
113 131
     [MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'),
@@ -137,17 +155,6 @@ function ProxiwashScreen() {
137 155
     });
138 156
   }, [navigation]);
139 157
 
140
-  useFocusEffect(
141
-    useCallback(() => {
142
-      const selected = AsyncStorageManager.getString(
143
-        AsyncStorageManager.PREFERENCES.selectedWash.key
144
-      ) as 'tripodeB' | 'washinsa';
145
-      if (selected !== selectedWash) {
146
-        setSelectedWash(selected);
147
-      }
148
-    }, [selectedWash])
149
-  );
150
-
151 158
   /**
152 159
    * Callback used when the user clicks on enable notifications for a machine
153 160
    *
@@ -293,6 +300,7 @@ function ProxiwashScreen() {
293 300
       setupMachineNotification(
294 301
         machine.number,
295 302
         true,
303
+        reminder,
296 304
         getMachineEndDate(machine)
297 305
       );
298 306
       saveNotificationToState(machine);
@@ -342,7 +350,7 @@ function ProxiwashScreen() {
342 350
         ...data.washers,
343 351
       ]);
344 352
       if (cleanedList !== machinesWatched) {
345
-        setMachinesWatched(machinesWatched);
353
+        updatePreferences(PreferenceKeys.proxiwashWatchedMachines, cleanedList);
346 354
       }
347 355
       return [
348 356
         {
@@ -407,11 +415,7 @@ function ProxiwashScreen() {
407 415
   };
408 416
 
409 417
   const saveNewWatchedList = (list: Array<ProxiwashMachineType>) => {
410
-    setMachinesWatched(list);
411
-    AsyncStorageManager.set(
412
-      AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
413
-      list
414
-    );
418
+    updatePreferences(PreferenceKeys.proxiwashWatchedMachines, list);
415 419
   };
416 420
 
417 421
   const renderListHeaderComponent = (
@@ -451,7 +455,6 @@ function ProxiwashScreen() {
451 455
         />
452 456
       </View>
453 457
       <MascotPopup
454
-        prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowMascot.key}
455 458
         title={i18n.t('screens.proxiwash.mascotDialog.title')}
456 459
         message={i18n.t('screens.proxiwash.mascotDialog.message')}
457 460
         icon="information"

+ 6
- 8
src/screens/Services/ServicesScreen.tsx View File

@@ -35,12 +35,12 @@ import MaterialHeaderButtons, {
35 35
 } from '../../components/Overrides/CustomHeaderButton';
36 36
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
37 37
 import MascotPopup from '../../components/Mascot/MascotPopup';
38
-import AsyncStorageManager from '../../managers/AsyncStorageManager';
39
-import ServicesManager, {
40
-  SERVICES_CATEGORIES_KEY,
41
-} from '../../managers/ServicesManager';
42 38
 import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
43
-import type { ServiceCategoryType } from '../../managers/ServicesManager';
39
+import {
40
+  getCategories,
41
+  ServiceCategoryType,
42
+  SERVICES_CATEGORIES_KEY,
43
+} from '../../utils/Services';
44 44
 
45 45
 type PropsType = {
46 46
   navigation: StackNavigationProp<any>;
@@ -66,8 +66,7 @@ class ServicesScreen extends React.Component<PropsType> {
66 66
 
67 67
   constructor(props: PropsType) {
68 68
     super(props);
69
-    const services = new ServicesManager(props.navigation);
70
-    this.finalDataset = services.getCategories([
69
+    this.finalDataset = getCategories(props.navigation.navigate, [
71 70
       SERVICES_CATEGORIES_KEY.SPECIAL,
72 71
     ]);
73 72
   }
@@ -159,7 +158,6 @@ class ServicesScreen extends React.Component<PropsType> {
159 158
           hasTab
160 159
         />
161 160
         <MascotPopup
162
-          prefKey={AsyncStorageManager.PREFERENCES.servicesShowMascot.key}
163 161
           title={i18n.t('screens.services.mascotDialog.title')}
164 162
           message={i18n.t('screens.services.mascotDialog.message')}
165 163
           icon="cloud-question"

+ 1
- 1
src/screens/Services/ServicesSectionScreen.tsx View File

@@ -22,7 +22,7 @@ import { Collapsible } from 'react-navigation-collapsible';
22 22
 import { CommonActions } from '@react-navigation/native';
23 23
 import { StackNavigationProp } from '@react-navigation/stack';
24 24
 import CardList from '../../components/Lists/CardList/CardList';
25
-import type { ServiceCategoryType } from '../../managers/ServicesManager';
25
+import { ServiceCategoryType } from '../../utils/Services';
26 26
 
27 27
 type PropsType = {
28 28
   navigation: StackNavigationProp<any>;

+ 4
- 7
src/utils/Notifications.ts View File

@@ -18,7 +18,6 @@
18 18
  */
19 19
 
20 20
 import i18n from 'i18n-js';
21
-import AsyncStorageManager from '../managers/AsyncStorageManager';
22 21
 import PushNotificationIOS from '@react-native-community/push-notification-ios';
23 22
 import PushNotification from 'react-native-push-notification';
24 23
 import { Platform } from 'react-native';
@@ -79,11 +78,8 @@ PushNotification.configure({
79 78
  * @param machineID The machine id to schedule notifications for. This is used as id and in the notification string.
80 79
  * @param date The date to trigger the notification at
81 80
  */
82
-function createNotifications(machineID: string, date: Date) {
83
-  const reminder = AsyncStorageManager.getNumber(
84
-    AsyncStorageManager.PREFERENCES.proxiwashNotifications.key
85
-  );
86
-  if (!Number.isNaN(reminder) && reminder > 0) {
81
+function createNotifications(machineID: string, date: Date, reminder?: number) {
82
+  if (reminder && !Number.isNaN(reminder) && reminder > 0) {
87 83
     const id = reminderIdFactor * parseInt(machineID, 10);
88 84
     const reminderDate = new Date(date);
89 85
     reminderDate.setMinutes(reminderDate.getMinutes() - reminder);
@@ -122,10 +118,11 @@ function createNotifications(machineID: string, date: Date) {
122 118
 export function setupMachineNotification(
123 119
   machineID: string,
124 120
   isEnabled: boolean,
121
+  reminder?: number,
125 122
   endDate?: Date | null
126 123
 ) {
127 124
   if (isEnabled && endDate) {
128
-    createNotifications(machineID, endDate);
125
+    createNotifications(machineID, endDate, reminder);
129 126
   } else {
130 127
     PushNotification.cancelLocalNotifications({ id: machineID });
131 128
     const reminderId = reminderIdFactor * parseInt(machineID, 10);

+ 294
- 38
src/utils/Services.ts View File

@@ -16,6 +16,11 @@
16 16
  * You should have received a copy of the GNU General Public License
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19
+import i18n from 'i18n-js';
20
+import type { FullDashboardType } from '../screens/Home/HomeScreen';
21
+import Urls from '../constants/Urls';
22
+import { MainRoutes } from '../navigation/MainNavigator';
23
+import { TabRoutes } from '../navigation/TabNavigator';
19 24
 
20 25
 /**
21 26
  * Gets the given services list without items of the given ids
@@ -25,45 +30,296 @@
25 30
  * @returns {[]}
26 31
  */
27 32
 export default function getStrippedServicesList<T extends { key: string }>(
28
-  idList: Array<string>,
29
-  sourceList: Array<T>
33
+  sourceList: Array<T>,
34
+  idList?: Array<string>
30 35
 ) {
31
-  const newArray: Array<T> = [];
32
-  sourceList.forEach((item: T) => {
33
-    if (!idList.includes(item.key)) {
34
-      newArray.push(item);
35
-    }
36
-  });
37
-  return newArray;
36
+  if (idList) {
37
+    return sourceList.filter((item) => !idList.includes(item.key));
38
+  } else {
39
+    return sourceList;
40
+  }
38 41
 }
39 42
 
40
-/**
41
- * Gets a sublist of the given list with items of the given ids only
42
- *
43
- * The given list must have a field id or key
44
- *
45
- * @param idList The ids of items to find
46
- * @param originalList The original list
47
- * @returns {[]}
48
- */
49
-export function getSublistWithIds<T extends { key: string }>(
50
-  idList: Array<string>,
51
-  originalList: Array<T>
52
-) {
53
-  const subList: Array<T | null> = [];
54
-  for (let i = 0; i < idList.length; i += 1) {
55
-    subList.push(null);
56
-  }
57
-  let itemsAdded = 0;
58
-  for (let i = 0; i < originalList.length; i += 1) {
59
-    const item = originalList[i];
60
-    if (idList.includes(item.key)) {
61
-      subList[idList.indexOf(item.key)] = item;
62
-      itemsAdded += 1;
63
-      if (itemsAdded === idList.length) {
64
-        break;
65
-      }
66
-    }
67
-  }
68
-  return subList;
43
+const AMICALE_LOGO = require('../../assets/amicale.png');
44
+
45
+export const SERVICES_KEY = {
46
+  CLUBS: 'clubs',
47
+  PROFILE: 'profile',
48
+  EQUIPMENT: 'equipment',
49
+  AMICALE_WEBSITE: 'amicale_website',
50
+  VOTE: 'vote',
51
+  PROXIMO: 'proximo',
52
+  WIKETUD: 'wiketud',
53
+  ELUS_ETUDIANTS: 'elus_etudiants',
54
+  TUTOR_INSA: 'tutor_insa',
55
+  RU: 'ru',
56
+  AVAILABLE_ROOMS: 'available_rooms',
57
+  BIB: 'bib',
58
+  EMAIL: 'email',
59
+  ENT: 'ent',
60
+  INSA_ACCOUNT: 'insa_account',
61
+  WASHERS: 'washers',
62
+  DRYERS: 'dryers',
63
+};
64
+
65
+export const SERVICES_CATEGORIES_KEY = {
66
+  AMICALE: 'amicale',
67
+  STUDENTS: 'students',
68
+  INSA: 'insa',
69
+  SPECIAL: 'special',
70
+};
71
+
72
+export type ServiceItemType = {
73
+  key: string;
74
+  title: string;
75
+  subtitle: string;
76
+  image: string | number;
77
+  onPress: () => void;
78
+  badgeFunction?: (dashboard: FullDashboardType) => number;
79
+};
80
+
81
+export type ServiceCategoryType = {
82
+  key: string;
83
+  title: string;
84
+  subtitle: string;
85
+  image: string | number;
86
+  content: Array<ServiceItemType>;
87
+};
88
+
89
+export function getAmicaleServices(
90
+  onPress: (route: string, params?: { [key: string]: any }) => void,
91
+  excludedItems?: Array<string>
92
+): Array<ServiceItemType> {
93
+  const amicaleDataset = [
94
+    {
95
+      key: SERVICES_KEY.CLUBS,
96
+      title: i18n.t('screens.clubs.title'),
97
+      subtitle: i18n.t('screens.services.descriptions.clubs'),
98
+      image: Urls.images.clubs,
99
+      onPress: () => onPress(MainRoutes.ClubList),
100
+    },
101
+    {
102
+      key: SERVICES_KEY.PROFILE,
103
+      title: i18n.t('screens.profile.title'),
104
+      subtitle: i18n.t('screens.services.descriptions.profile'),
105
+      image: Urls.images.profile,
106
+      onPress: () => onPress(MainRoutes.Profile),
107
+    },
108
+    {
109
+      key: SERVICES_KEY.EQUIPMENT,
110
+      title: i18n.t('screens.equipment.title'),
111
+      subtitle: i18n.t('screens.services.descriptions.equipment'),
112
+      image: Urls.images.equipment,
113
+      onPress: () => onPress(MainRoutes.EquipmentList),
114
+    },
115
+    {
116
+      key: SERVICES_KEY.AMICALE_WEBSITE,
117
+      title: i18n.t('screens.websites.amicale'),
118
+      subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
119
+      image: Urls.images.amicale,
120
+      onPress: () =>
121
+        onPress(MainRoutes.Website, {
122
+          host: Urls.websites.amicale,
123
+          title: i18n.t('screens.websites.amicale'),
124
+        }),
125
+    },
126
+    {
127
+      key: SERVICES_KEY.VOTE,
128
+      title: i18n.t('screens.vote.title'),
129
+      subtitle: i18n.t('screens.services.descriptions.vote'),
130
+      image: Urls.images.vote,
131
+      onPress: () => onPress(MainRoutes.Vote),
132
+    },
133
+  ];
134
+  return getStrippedServicesList(amicaleDataset, excludedItems);
135
+}
136
+
137
+export function getStudentServices(
138
+  onPress: (route: string, params?: { [key: string]: any }) => void,
139
+  excludedItems?: Array<string>
140
+): Array<ServiceItemType> {
141
+  const studentsDataset = [
142
+    {
143
+      key: SERVICES_KEY.PROXIMO,
144
+      title: i18n.t('screens.proximo.title'),
145
+      subtitle: i18n.t('screens.services.descriptions.proximo'),
146
+      image: Urls.images.proximo,
147
+      onPress: () => onPress(MainRoutes.Proximo),
148
+      badgeFunction: (dashboard: FullDashboardType): number =>
149
+        dashboard.proximo_articles,
150
+    },
151
+    {
152
+      key: SERVICES_KEY.WIKETUD,
153
+      title: 'Wiketud',
154
+      subtitle: i18n.t('screens.services.descriptions.wiketud'),
155
+      image: Urls.images.wiketud,
156
+      onPress: () =>
157
+        onPress(MainRoutes.Website, {
158
+          host: Urls.websites.wiketud,
159
+          title: 'Wiketud',
160
+        }),
161
+    },
162
+    {
163
+      key: SERVICES_KEY.ELUS_ETUDIANTS,
164
+      title: 'Élus Étudiants',
165
+      subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
166
+      image: Urls.images.elusEtudiants,
167
+      onPress: () =>
168
+        onPress(MainRoutes.Website, {
169
+          host: Urls.websites.elusEtudiants,
170
+          title: 'Élus Étudiants',
171
+        }),
172
+    },
173
+    {
174
+      key: SERVICES_KEY.TUTOR_INSA,
175
+      title: "Tutor'INSA",
176
+      subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
177
+      image: Urls.images.tutorInsa,
178
+      onPress: () =>
179
+        onPress(MainRoutes.Website, {
180
+          host: Urls.websites.tutorInsa,
181
+          title: "Tutor'INSA",
182
+        }),
183
+      badgeFunction: (dashboard: FullDashboardType): number =>
184
+        dashboard.available_tutorials,
185
+    },
186
+  ];
187
+  return getStrippedServicesList(studentsDataset, excludedItems);
188
+}
189
+
190
+export function getINSAServices(
191
+  onPress: (route: string, params?: { [key: string]: any }) => void,
192
+  excludedItems?: Array<string>
193
+): Array<ServiceItemType> {
194
+  const insaDataset = [
195
+    {
196
+      key: SERVICES_KEY.RU,
197
+      title: i18n.t('screens.menu.title'),
198
+      subtitle: i18n.t('screens.services.descriptions.self'),
199
+      image: Urls.images.menu,
200
+      onPress: () => onPress(MainRoutes.SelfMenu),
201
+      badgeFunction: (dashboard: FullDashboardType): number =>
202
+        dashboard.today_menu.length,
203
+    },
204
+    {
205
+      key: SERVICES_KEY.AVAILABLE_ROOMS,
206
+      title: i18n.t('screens.websites.rooms'),
207
+      subtitle: i18n.t('screens.services.descriptions.availableRooms'),
208
+      image: Urls.images.availableRooms,
209
+      onPress: () =>
210
+        onPress(MainRoutes.Website, {
211
+          host: Urls.websites.availableRooms,
212
+          title: i18n.t('screens.websites.rooms'),
213
+        }),
214
+    },
215
+    {
216
+      key: SERVICES_KEY.BIB,
217
+      title: i18n.t('screens.websites.bib'),
218
+      subtitle: i18n.t('screens.services.descriptions.bib'),
219
+      image: Urls.images.bib,
220
+      onPress: () =>
221
+        onPress(MainRoutes.Website, {
222
+          host: Urls.websites.bib,
223
+          title: i18n.t('screens.websites.bib'),
224
+        }),
225
+    },
226
+    {
227
+      key: SERVICES_KEY.EMAIL,
228
+      title: i18n.t('screens.websites.mails'),
229
+      subtitle: i18n.t('screens.services.descriptions.mails'),
230
+      image: Urls.images.bluemind,
231
+      onPress: () =>
232
+        onPress(MainRoutes.Website, {
233
+          host: Urls.websites.bluemind,
234
+          title: i18n.t('screens.websites.mails'),
235
+        }),
236
+    },
237
+    {
238
+      key: SERVICES_KEY.ENT,
239
+      title: i18n.t('screens.websites.ent'),
240
+      subtitle: i18n.t('screens.services.descriptions.ent'),
241
+      image: Urls.images.ent,
242
+      onPress: () =>
243
+        onPress(MainRoutes.Website, {
244
+          host: Urls.websites.ent,
245
+          title: i18n.t('screens.websites.ent'),
246
+        }),
247
+    },
248
+    {
249
+      key: SERVICES_KEY.INSA_ACCOUNT,
250
+      title: i18n.t('screens.insaAccount.title'),
251
+      subtitle: i18n.t('screens.services.descriptions.insaAccount'),
252
+      image: Urls.images.insaAccount,
253
+      onPress: () =>
254
+        onPress(MainRoutes.Website, {
255
+          host: Urls.websites.insaAccount,
256
+          title: i18n.t('screens.insaAccount.title'),
257
+        }),
258
+    },
259
+  ];
260
+  return getStrippedServicesList(insaDataset, excludedItems);
261
+}
262
+
263
+export function getSpecialServices(
264
+  onPress: (route: string, params?: { [key: string]: any }) => void,
265
+  excludedItems?: Array<string>
266
+): Array<ServiceItemType> {
267
+  const specialDataset = [
268
+    {
269
+      key: SERVICES_KEY.WASHERS,
270
+      title: i18n.t('screens.proxiwash.washers'),
271
+      subtitle: i18n.t('screens.services.descriptions.washers'),
272
+      image: Urls.images.washer,
273
+      onPress: () => onPress(TabRoutes.Proxiwash),
274
+      badgeFunction: (dashboard: FullDashboardType): number =>
275
+        dashboard.available_washers,
276
+    },
277
+    {
278
+      key: SERVICES_KEY.DRYERS,
279
+      title: i18n.t('screens.proxiwash.dryers'),
280
+      subtitle: i18n.t('screens.services.descriptions.washers'),
281
+      image: Urls.images.dryer,
282
+      onPress: () => onPress(TabRoutes.Proxiwash),
283
+      badgeFunction: (dashboard: FullDashboardType): number =>
284
+        dashboard.available_dryers,
285
+    },
286
+  ];
287
+  return getStrippedServicesList(specialDataset, excludedItems);
288
+}
289
+
290
+export function getCategories(
291
+  onPress: (route: string, params?: { [key: string]: any }) => void,
292
+  excludedItems?: Array<string>
293
+): Array<ServiceCategoryType> {
294
+  const categoriesDataset = [
295
+    {
296
+      key: SERVICES_CATEGORIES_KEY.AMICALE,
297
+      title: i18n.t('screens.services.categories.amicale'),
298
+      subtitle: i18n.t('screens.services.more'),
299
+      image: AMICALE_LOGO,
300
+      content: getAmicaleServices(onPress),
301
+    },
302
+    {
303
+      key: SERVICES_CATEGORIES_KEY.STUDENTS,
304
+      title: i18n.t('screens.services.categories.students'),
305
+      subtitle: i18n.t('screens.services.more'),
306
+      image: 'account-group',
307
+      content: getStudentServices(onPress),
308
+    },
309
+    {
310
+      key: SERVICES_CATEGORIES_KEY.INSA,
311
+      title: i18n.t('screens.services.categories.insa'),
312
+      subtitle: i18n.t('screens.services.more'),
313
+      image: 'school',
314
+      content: getINSAServices(onPress),
315
+    },
316
+    {
317
+      key: SERVICES_CATEGORIES_KEY.SPECIAL,
318
+      title: i18n.t('screens.services.categories.special'),
319
+      subtitle: i18n.t('screens.services.categories.special'),
320
+      image: 'star',
321
+      content: getSpecialServices(onPress),
322
+    },
323
+  ];
324
+  return getStrippedServicesList(categoriesDataset, excludedItems);
69 325
 }

src/managers/ThemeManager.ts → src/utils/Themes.ts View File

@@ -1,28 +1,4 @@
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 1
 import { DarkTheme, DefaultTheme } from 'react-native-paper';
21
-import { Appearance } from 'react-native-appearance';
22
-import AsyncStorageManager from './AsyncStorageManager';
23
-import AprilFoolsManager from './AprilFoolsManager';
24
-
25
-const colorScheme = Appearance.getColorScheme();
26 2
 
27 3
 declare global {
28 4
   namespace ReactNativePaper {
@@ -83,7 +59,7 @@ declare global {
83 59
   }
84 60
 }
85 61
 
86
-const CustomWhiteTheme: ReactNativePaper.Theme = {
62
+export const CustomWhiteTheme: ReactNativePaper.Theme = {
87 63
   ...DefaultTheme,
88 64
   colors: {
89 65
     ...DefaultTheme.colors,
@@ -142,7 +118,7 @@ const CustomWhiteTheme: ReactNativePaper.Theme = {
142 118
   },
143 119
 };
144 120
 
145
-const CustomDarkTheme: ReactNativePaper.Theme = {
121
+export const CustomDarkTheme: ReactNativePaper.Theme = {
146 122
   ...DarkTheme,
147 123
   colors: {
148 124
     ...DarkTheme.colors,
@@ -200,99 +176,3 @@ const CustomDarkTheme: ReactNativePaper.Theme = {
200 176
     mascotMessageArrow: '#323232',
201 177
   },
202 178
 };
203
-
204
-/**
205
- * Singleton class used to manage themes
206
- */
207
-export default class ThemeManager {
208
-  static instance: ThemeManager | null = null;
209
-
210
-  updateThemeCallback: null | (() => void);
211
-
212
-  constructor() {
213
-    this.updateThemeCallback = null;
214
-  }
215
-
216
-  /**
217
-   * Get this class instance or create one if none is found
218
-   *
219
-   * @returns {ThemeManager}
220
-   */
221
-  static getInstance(): ThemeManager {
222
-    if (ThemeManager.instance == null) {
223
-      ThemeManager.instance = new ThemeManager();
224
-    }
225
-    return ThemeManager.instance;
226
-  }
227
-
228
-  /**
229
-   * Gets night mode status.
230
-   * If Follow System Preferences is enabled, will first use system theme.
231
-   * If disabled or not available, will use value stored din preferences
232
-   *
233
-   * @returns {boolean} Night mode state
234
-   */
235
-  static getNightMode(): boolean {
236
-    return (
237
-      (AsyncStorageManager.getBool(
238
-        AsyncStorageManager.PREFERENCES.nightMode.key
239
-      ) &&
240
-        (!AsyncStorageManager.getBool(
241
-          AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key
242
-        ) ||
243
-          colorScheme === 'no-preference')) ||
244
-      (AsyncStorageManager.getBool(
245
-        AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key
246
-      ) &&
247
-        colorScheme === 'dark')
248
-    );
249
-  }
250
-
251
-  /**
252
-   * Get the current theme based on night mode and events
253
-   *
254
-   * @returns {ReactNativePaper.Theme} The current theme
255
-   */
256
-  static getCurrentTheme(): ReactNativePaper.Theme {
257
-    if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
258
-      return AprilFoolsManager.getAprilFoolsTheme(CustomWhiteTheme);
259
-    }
260
-    return ThemeManager.getBaseTheme();
261
-  }
262
-
263
-  /**
264
-   * Get the theme based on night mode
265
-   *
266
-   * @return {ReactNativePaper.Theme} The theme
267
-   */
268
-  static getBaseTheme(): ReactNativePaper.Theme {
269
-    if (ThemeManager.getNightMode()) {
270
-      return CustomDarkTheme;
271
-    }
272
-    return CustomWhiteTheme;
273
-  }
274
-
275
-  /**
276
-   * Sets the function to be called when the theme is changed (allows for general reload of the app)
277
-   *
278
-   * @param callback Function to call after theme change
279
-   */
280
-  setUpdateThemeCallback(callback: () => void) {
281
-    this.updateThemeCallback = callback;
282
-  }
283
-
284
-  /**
285
-   * Set night mode and save it to preferences
286
-   *
287
-   * @param isNightMode True to enable night mode, false to disable
288
-   */
289
-  setNightMode(isNightMode: boolean) {
290
-    AsyncStorageManager.set(
291
-      AsyncStorageManager.PREFERENCES.nightMode.key,
292
-      isNightMode
293
-    );
294
-    if (this.updateThemeCallback != null) {
295
-      this.updateThemeCallback();
296
-    }
297
-  }
298
-}

+ 10
- 12
src/utils/Utils.ts View File

@@ -18,23 +18,21 @@
18 18
  */
19 19
 
20 20
 import { Platform, StatusBar } from 'react-native';
21
-import ThemeManager from '../managers/ThemeManager';
22 21
 
23 22
 /**
24 23
  * Updates status bar content color if on iOS only,
25 24
  * as the android status bar is always set to black.
26 25
  */
27
-export function setupStatusBar() {
28
-  if (ThemeManager.getNightMode()) {
29
-    StatusBar.setBarStyle('light-content', true);
30
-  } else {
31
-    StatusBar.setBarStyle('dark-content', true);
32
-  }
33
-  if (Platform.OS === 'android') {
34
-    StatusBar.setBackgroundColor(
35
-      ThemeManager.getCurrentTheme().colors.surface,
36
-      true
37
-    );
26
+export function setupStatusBar(theme?: ReactNativePaper.Theme) {
27
+  if (theme) {
28
+    if (theme.dark) {
29
+      StatusBar.setBarStyle('light-content', true);
30
+    } else {
31
+      StatusBar.setBarStyle('dark-content', true);
32
+    }
33
+    if (Platform.OS === 'android') {
34
+      StatusBar.setBackgroundColor(theme.colors.surface, true);
35
+    }
38 36
   }
39 37
 }
40 38
 

+ 9
- 3
src/utils/asyncStorage.ts View File

@@ -1,5 +1,5 @@
1 1
 import AsyncStorage from '@react-native-async-storage/async-storage';
2
-import { SERVICES_KEY } from '../managers/ServicesManager';
2
+import { SERVICES_KEY } from './Services';
3 3
 
4 4
 export enum PreferenceKeys {
5 5
   debugUnlocked = 'debugUnlocked',
@@ -9,6 +9,7 @@ export enum PreferenceKeys {
9 9
   nightModeFollowSystem = 'nightModeFollowSystem',
10 10
   nightMode = 'nightMode',
11 11
   defaultStartScreen = 'defaultStartScreen',
12
+
12 13
   servicesShowMascot = 'servicesShowMascot',
13 14
   proxiwashShowMascot = 'proxiwashShowMascot',
14 15
   homeShowMascot = 'homeShowMascot',
@@ -17,7 +18,8 @@ export enum PreferenceKeys {
17 18
   loginShowMascot = 'loginShowMascot',
18 19
   voteShowMascot = 'voteShowMascot',
19 20
   equipmentShowMascot = 'equipmentShowMascot',
20
-  gameStartMascot = 'gameStartMascot',
21
+  gameShowMascot = 'gameShowMascot',
22
+
21 23
   proxiwashWatchedMachines = 'proxiwashWatchedMachines',
22 24
   showAprilFoolsStart = 'showAprilFoolsStart',
23 25
   planexCurrentGroup = 'planexCurrentGroup',
@@ -45,7 +47,7 @@ export const defaultPreferences: { [key in PreferenceKeys]: string } = {
45 47
   [PreferenceKeys.loginShowMascot]: '1',
46 48
   [PreferenceKeys.voteShowMascot]: '1',
47 49
   [PreferenceKeys.equipmentShowMascot]: '1',
48
-  [PreferenceKeys.gameStartMascot]: '1',
50
+  [PreferenceKeys.gameShowMascot]: '1',
49 51
   [PreferenceKeys.proxiwashWatchedMachines]: '[]',
50 52
   [PreferenceKeys.showAprilFoolsStart]: '1',
51 53
   [PreferenceKeys.planexCurrentGroup]: '',
@@ -114,6 +116,10 @@ export function setPreference(
114 116
   return prevPreferences;
115 117
 }
116 118
 
119
+export function isValidPreferenceKey(key: string): key is PreferenceKeys {
120
+  return key in Object.values(PreferenceKeys);
121
+}
122
+
117 123
 /**
118 124
  * Gets the boolean value of the given preference
119 125
  *

Loading…
Cancel
Save