Browse Source

Update proxiwash screen and notifications

Arnaud Vergnet 6 months ago
parent
commit
27199b85e5

+ 17
- 16
android/app/src/main/AndroidManifest.xml View File

@@ -8,7 +8,6 @@
8 8
     <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
9 9
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
10 10
     <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
11
-    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
12 11
 
13 12
     <application
14 13
             android:name=".MainApplication"
@@ -19,31 +18,33 @@
19 18
             android:theme="@style/AppTheme"
20 19
             android:usesCleartextTraffic="true"
21 20
     >
22
-        <!--        NOTIFICATIONS -->
23
-        <meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
24
-                   android:value="reminders"/>
25
-        <meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
26
-                   android:value="reminders"/>
27
-        <!-- Change the resource name to your App's accent color - or any other color you want -->
28
-        <meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
29
-                   android:resource="@color/colorPrimary"/> <!-- or @android:color/{name} to use a standard color -->
30 21
 
31
-        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher"/>
22
+        <!-- START NOTIFICATIONS -->
23
+
24
+        <!-- Change the value to true to enable pop-up for in foreground on receiving remote notifications (for prevent duplicating while showing local notifications set this to false) -->
25
+        <meta-data  android:name="com.dieam.reactnativepushnotification.notification_foreground"
26
+                    android:value="false"/>
27
+        Change the resource name to your App's accent color - or any other color you want
28
+        <meta-data  android:name="com.dieam.reactnativepushnotification.notification_color"
29
+                    android:resource="@color/colorPrimary"/>
30
+        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
31
+        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
32 32
         <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
33 33
             <intent-filter>
34
-                <action android:name="android.intent.action.BOOT_COMPLETED"/>
34
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
35
+                <action android:name="android.intent.action.QUICKBOOT_POWERON" />
36
+                <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
35 37
             </intent-filter>
36 38
         </receiver>
37 39
 
38 40
         <service
39
-                android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
40
-                android:exported="false">
41
+            android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
42
+            android:exported="false" >
41 43
             <intent-filter>
42
-                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
44
+                <action android:name="com.google.firebase.MESSAGING_EVENT" />
43 45
             </intent-filter>
44 46
         </service>
45
-
46
-        <!--        END NOTIFICATIONS-->
47
+        <!-- END NOTIFICATIONS -->
47 48
 
48 49
 
49 50
         <meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false"/>

+ 0
- 11
android/app/src/main/java/fr/amicaleinsat/application/MainActivity.java View File

@@ -5,22 +5,11 @@ import com.facebook.react.ReactActivity;
5 5
 import com.facebook.react.ReactActivityDelegate;
6 6
 import com.facebook.react.ReactRootView;
7 7
 import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
8
-import android.content.Intent;
9
-import android.content.res.Configuration;
10 8
 
11 9
 import org.devio.rn.splashscreen.SplashScreen;
12 10
 
13 11
 public class MainActivity extends ReactActivity {
14 12
 
15
-    // Added automatically by Expo Config
16
-    @Override
17
-    public void onConfigurationChanged(Configuration newConfig) {
18
-        super.onConfigurationChanged(newConfig);
19
-        Intent intent = new Intent("onConfigurationChanged");
20
-        intent.putExtra("newConfig", newConfig);
21
-        sendBroadcast(intent);
22
-    }
23
-
24 13
    @Override
25 14
     protected void onCreate(Bundle savedInstanceState) {
26 15
         SplashScreen.show(this, R.style.SplashScreenTheme);

+ 1
- 1
android/app/src/release/AndroidManifest.xml View File

@@ -21,7 +21,7 @@
21 21
     <uses-permission tools:node="remove" android:name="android.permission.WRITE_CALENDAR"/>
22 22
     <uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE"/>
23 23
     <uses-permission tools:node="remove" android:name="android.permission.RECORD_AUDIO"/>
24
-        <uses-permission tools:node="remove" android:name="android.permission.WRITE_SETTINGS"/>
24
+    <uses-permission tools:node="remove" android:name="android.permission.WRITE_SETTINGS"/>
25 25
     <uses-permission tools:node="remove" android:name="android.permission.ACCESS_NETWORK_STATE"/>
26 26
 
27 27
 </manifest>

+ 9
- 6
package-lock.json View File

@@ -2530,6 +2530,12 @@
2530 2530
         "@types/xdate": "*"
2531 2531
       }
2532 2532
     },
2533
+    "@types/react-native-push-notification": {
2534
+      "version": "7.2.0",
2535
+      "resolved": "https://registry.npmjs.org/@types/react-native-push-notification/-/react-native-push-notification-7.2.0.tgz",
2536
+      "integrity": "sha512-4kErWFa0qit8qzPB6Nbp7kG9NiwDyKu5XxrNlrCIc1zoFxu48ABeofVvNCKv2RtlmFvCftibtykeysRZCeuT8A==",
2537
+      "dev": true
2538
+    },
2533 2539
     "@types/react-native-vector-icons": {
2534 2540
       "version": "6.4.6",
2535 2541
       "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.6.tgz",
@@ -10684,12 +10690,9 @@
10684 10690
       "integrity": "sha512-8xiEnU29qHZcT05XXwhPHiLChTt82Pn5Z/nFdDOYGNFZ+IYSbYeGmIxFpratCRO6dgLptNaDFDPiyw2X7UZTeg=="
10685 10691
     },
10686 10692
     "react-native-push-notification": {
10687
-      "version": "5.1.1",
10688
-      "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-5.1.1.tgz",
10689
-      "integrity": "sha512-CJmKqzM2P/s+a9PImoaiUN4TP1+K4YfmG1B0uUbavgFdGhTtRPTLLwDfFk2h3J6VmTXNak82rUz2iGwyptHm5w==",
10690
-      "requires": {
10691
-        "@react-native-community/push-notification-ios": "^1.4.0"
10692
-      }
10693
+      "version": "7.3.0",
10694
+      "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-7.3.0.tgz",
10695
+      "integrity": "sha512-Ofy8dYAhIkFJKxQDvAn7BnXxwtun1SMnqLjZUhRTRzhPEqN0tW7TmIjfyYanNf/lnggRYZqFO+b14Ul3nhlMGw=="
10693 10696
     },
10694 10697
     "react-native-reanimated": {
10695 10698
       "version": "1.13.2",

+ 5
- 2
package.json View File

@@ -45,7 +45,7 @@
45 45
     "react-native-modalize": "2.0.8",
46 46
     "react-native-paper": "4.8.1",
47 47
     "react-native-permissions": "3.0.3",
48
-    "react-native-push-notification": "5.1.1",
48
+    "react-native-push-notification": "7.3.0",
49 49
     "react-native-reanimated": "1.13.2",
50 50
     "react-native-render-html": "5.1.0",
51 51
     "react-native-safe-area-context": "3.2.0",
@@ -65,6 +65,7 @@
65 65
     "@types/react": "17.0.3",
66 66
     "@types/react-native": "0.64.4",
67 67
     "@types/react-native-calendars": "1.20.10",
68
+    "@types/react-native-push-notification": "^7.2.0",
68 69
     "@types/react-native-vector-icons": "6.4.6",
69 70
     "@types/react-test-renderer": "17.0.1",
70 71
     "@typescript-eslint/eslint-plugin": "4.22.1",
@@ -94,7 +95,9 @@
94 95
     "rules": {
95 96
       "no-undef": 0,
96 97
       "no-shadow": "off",
97
-      "@typescript-eslint/no-shadow": ["error"],
98
+      "@typescript-eslint/no-shadow": [
99
+        "error"
100
+      ],
98 101
       "prettier/prettier": [
99 102
         "error",
100 103
         {

+ 9
- 8
src/components/Overrides/CustomModal.tsx View File

@@ -17,27 +17,28 @@
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, { Ref } from 'react';
21 21
 import { useTheme } from 'react-native-paper';
22 22
 import { Modalize } from 'react-native-modalize';
23 23
 import { View } from 'react-native-animatable';
24 24
 import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
25 25
 
26
+type Props = {
27
+  children?: React.ReactChild | null;
28
+};
29
+
26 30
 /**
27 31
  * Abstraction layer for Modalize component, using custom configuration
28 32
  *
29 33
  * @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
30 34
  * @return {*}
31 35
  */
32
-function CustomModal(props: {
33
-  onRef: (re: Modalize) => void;
34
-  children?: React.ReactNode;
35
-}) {
36
+function CustomModal(props: Props, ref?: Ref<Modalize>) {
36 37
   const theme = useTheme();
37
-  const { onRef, children } = props;
38
+  const { children } = props;
38 39
   return (
39 40
     <Modalize
40
-      ref={onRef}
41
+      ref={ref}
41 42
       adjustToContentHeight
42 43
       handlePosition="inside"
43 44
       modalStyle={{ backgroundColor: theme.colors.card }}
@@ -54,4 +55,4 @@ function CustomModal(props: {
54 55
   );
55 56
 }
56 57
 
57
-export default CustomModal;
58
+export default React.forwardRef(CustomModal);

+ 151
- 230
src/screens/Proxiwash/ProxiwashScreen.tsx View File

@@ -17,20 +17,17 @@
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, { useCallback, useLayoutEffect, useRef, useState } from 'react';
21 21
 import {
22
-  Alert,
23 22
   SectionListData,
24 23
   SectionListRenderItemInfo,
25 24
   StyleSheet,
26 25
   View,
27 26
 } from 'react-native';
28 27
 import i18n from 'i18n-js';
29
-import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper';
30
-import { StackNavigationProp } from '@react-navigation/stack';
28
+import { Avatar, Button, Card, Text, useTheme } from 'react-native-paper';
31 29
 import { Modalize } from 'react-native-modalize';
32 30
 import WebSectionList from '../../components/Screens/WebSectionList';
33
-import * as Notifications from '../../utils/Notifications';
34 31
 import AsyncStorageManager from '../../managers/AsyncStorageManager';
35 32
 import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem';
36 33
 import ProxiwashConstants, {
@@ -53,6 +50,8 @@ import type { SectionListDataType } from '../../components/Screens/WebSectionLis
53 50
 import type { LaundromatType } from './ProxiwashAboutScreen';
54 51
 import GENERAL_STYLES from '../../constants/Styles';
55 52
 import { readData } from '../../utils/WebData';
53
+import { useFocusEffect, useNavigation } from '@react-navigation/core';
54
+import { setupMachineNotification } from '../../utils/Notifications';
56 55
 
57 56
 const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
58 57
 const LIST_ITEM_HEIGHT = 64;
@@ -68,17 +67,6 @@ export type ProxiwashMachineType = {
68 67
   program: string;
69 68
 };
70 69
 
71
-type PropsType = {
72
-  navigation: StackNavigationProp<any>;
73
-  theme: ReactNativePaper.Theme;
74
-};
75
-
76
-type StateType = {
77
-  modalCurrentDisplayItem: React.ReactNode;
78
-  machinesWatched: Array<ProxiwashMachineType>;
79
-  selectedWash: string;
80
-};
81
-
82 70
 type FetchedDataType = {
83 71
   dryers: Array<ProxiwashMachineType>;
84 72
   washers: Array<ProxiwashMachineType>;
@@ -99,22 +87,28 @@ const styles = StyleSheet.create({
99 87
   },
100 88
 });
101 89
 
102
-/**
103
- * Class defining the app's proxiwash screen. This screen shows information about washing machines and
104
- * dryers, taken from a scrapper reading proxiwash website
105
- */
106
-class ProxiwashScreen extends React.Component<PropsType, StateType> {
107
-  /**
108
-   * Shows a warning telling the user notifications are disabled for the app
109
-   */
110
-  static showNotificationsDisabledWarning() {
111
-    Alert.alert(
112
-      i18n.t('screens.proxiwash.modal.notificationErrorTitle'),
113
-      i18n.t('screens.proxiwash.modal.notificationErrorDescription')
114
-    );
115
-  }
90
+function ProxiwashScreen() {
91
+  const navigation = useNavigation();
92
+  const theme = useTheme();
93
+  const [
94
+    modalCurrentDisplayItem,
95
+    setModalCurrentDisplayItem,
96
+  ] = useState<React.ReactElement | null>(null);
97
+  const [machinesWatched, setMachinesWatched] = useState<
98
+    Array<ProxiwashMachineType>
99
+  >(
100
+    AsyncStorageManager.getObject(
101
+      AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
102
+    )
103
+  );
104
+
105
+  const [selectedWash, setSelectedWash] = useState(
106
+    AsyncStorageManager.getString(
107
+      AsyncStorageManager.PREFERENCES.selectedWash.key
108
+    )
109
+  );
116 110
 
117
-  modalStateStrings: { [key in MachineStates]: string } = {
111
+  const modalStateStrings: { [key in MachineStates]: string } = {
118 112
     [MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'),
119 113
     [MachineStates.RUNNING]: i18n.t('screens.proxiwash.modal.running'),
120 114
     [MachineStates.RUNNING_NOT_STARTED]: i18n.t(
@@ -126,95 +120,48 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
126 120
     [MachineStates.UNKNOWN]: i18n.t('screens.proxiwash.modal.unknown'),
127 121
   };
128 122
 
129
-  modalRef: null | Modalize;
130
-
131
-  fetchedData: {
132
-    dryers: Array<ProxiwashMachineType>;
133
-    washers: Array<ProxiwashMachineType>;
134
-  };
135
-
136
-  /**
137
-   * Creates machine state parameters using current theme and translations
138
-   */
139
-  constructor(props: PropsType) {
140
-    super(props);
141
-    this.modalRef = null;
142
-    this.fetchedData = { dryers: [], washers: [] };
143
-    this.state = {
144
-      modalCurrentDisplayItem: null,
145
-      machinesWatched: AsyncStorageManager.getObject(
146
-        AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
147
-      ),
148
-      selectedWash: AsyncStorageManager.getString(
149
-        AsyncStorageManager.PREFERENCES.selectedWash.key
150
-      ),
151
-    };
152
-  }
123
+  const modalRef = useRef<Modalize>(null);
153 124
 
154
-  /**
155
-   * Setup notification channel for android and add listeners to detect notifications fired
156
-   */
157
-  componentDidMount() {
158
-    const { navigation } = this.props;
125
+  useLayoutEffect(() => {
159 126
     navigation.setOptions({
160 127
       headerRight: () => (
161 128
         <MaterialHeaderButtons>
162 129
           <Item
163
-            title="switch"
164
-            iconName="swap-horizontal"
165
-            onPress={(): void => navigation.navigate('settings')}
130
+            title={'switch'}
131
+            iconName={'swap-horizontal'}
132
+            onPress={() => navigation.navigate('settings')}
166 133
           />
167 134
           <Item
168
-            title="information"
169
-            iconName="information"
170
-            onPress={this.onAboutPress}
135
+            title={'information'}
136
+            iconName={'information'}
137
+            onPress={() => navigation.navigate('proxiwash-about')}
171 138
           />
172 139
         </MaterialHeaderButtons>
173 140
       ),
174 141
     });
175
-    navigation.addListener('focus', this.onScreenFocus);
176
-  }
177
-
178
-  onScreenFocus = () => {
179
-    const { state } = this;
180
-    const selected = AsyncStorageManager.getString(
181
-      AsyncStorageManager.PREFERENCES.selectedWash.key
182
-    );
183
-    if (selected !== state.selectedWash) {
184
-      this.setState({
185
-        selectedWash: selected,
186
-      });
187
-    }
188
-  };
142
+  }, [navigation]);
189 143
 
190
-  /**
191
-   * Callback used when pressing the about button.
192
-   * This will open the ProxiwashAboutScreen.
193
-   */
194
-  onAboutPress = () => {
195
-    const { navigation } = this.props;
196
-    navigation.navigate('proxiwash-about');
197
-  };
144
+  useFocusEffect(
145
+    useCallback(() => {
146
+      const selected = AsyncStorageManager.getString(
147
+        AsyncStorageManager.PREFERENCES.selectedWash.key
148
+      );
149
+      if (selected !== selectedWash) {
150
+        setSelectedWash(selected);
151
+      }
152
+    }, [selectedWash])
153
+  );
198 154
 
199 155
   /**
200 156
    * Callback used when the user clicks on enable notifications for a machine
201 157
    *
202 158
    * @param machine The machine to set notifications for
203 159
    */
204
-  onSetupNotificationsPress(machine: ProxiwashMachineType) {
205
-    if (this.modalRef) {
206
-      this.modalRef.close();
160
+  const onSetupNotificationsPress = (machine: ProxiwashMachineType) => {
161
+    if (modalRef.current) {
162
+      modalRef.current.close();
207 163
     }
208
-    this.setupNotifications(machine);
209
-  }
210
-
211
-  /**
212
-   * Callback used when receiving modal ref
213
-   *
214
-   * @param ref
215
-   */
216
-  onModalRef = (ref: Modalize) => {
217
-    this.modalRef = ref;
164
+    setupNotifications(machine);
218 165
   };
219 166
 
220 167
   /**
@@ -226,11 +173,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
226 173
    * @param isDryer True if the given item is a dryer
227 174
    * @return {*}
228 175
    */
229
-  getModalContent(title: string, item: ProxiwashMachineType, isDryer: boolean) {
230
-    const { props, state } = this;
176
+  const getModalContent = (
177
+    title: string,
178
+    item: ProxiwashMachineType,
179
+    isDryer: boolean
180
+  ) => {
231 181
     let button: { text: string; icon: string; onPress: () => void } | undefined;
232
-    let message = this.modalStateStrings[item.state];
233
-    const onPress = () => this.onSetupNotificationsPress(item);
182
+    let message = modalStateStrings[item.state];
183
+    const onPress = () => onSetupNotificationsPress(item);
234 184
     if (item.state === MachineStates.RUNNING) {
235 185
       let remainingTime = parseInt(item.remainingTime, 10);
236 186
       if (remainingTime < 0) {
@@ -238,7 +188,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
238 188
       }
239 189
 
240 190
       button = {
241
-        text: isMachineWatched(item, state.machinesWatched)
191
+        text: isMachineWatched(item, machinesWatched)
242 192
           ? i18n.t('screens.proxiwash.modal.disableNotifications')
243 193
           : i18n.t('screens.proxiwash.modal.enableNotifications'),
244 194
         icon: '',
@@ -258,7 +208,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
258 208
           left={() => (
259 209
             <Avatar.Icon
260 210
               icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
261
-              color={props.theme.colors.text}
211
+              color={theme.colors.text}
262 212
               style={styles.icon}
263 213
             />
264 214
           )}
@@ -281,7 +231,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
281 231
         ) : null}
282 232
       </View>
283 233
     );
284
-  }
234
+  };
285 235
 
286 236
   /**
287 237
    * Gets the section render item
@@ -289,13 +239,13 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
289 239
    * @param section The section to render
290 240
    * @return {*}
291 241
    */
292
-  getRenderSectionHeader = ({
242
+  const getRenderSectionHeader = ({
293 243
     section,
294 244
   }: {
295 245
     section: SectionListData<ProxiwashMachineType>;
296 246
   }) => {
297 247
     const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
298
-    const nbAvailable = this.getMachineAvailableNumber(isDryer);
248
+    const nbAvailable = getMachineAvailableNumber(section.data);
299 249
     return (
300 250
       <ProxiwashSectionHeader
301 251
         title={section.title}
@@ -312,13 +262,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
312 262
    * @param section The object describing the current SectionList section
313 263
    * @returns {React.Node}
314 264
    */
315
-  getRenderItem = (data: SectionListRenderItemInfo<ProxiwashMachineType>) => {
316
-    const { machinesWatched } = this.state;
265
+  const getRenderItem = (
266
+    data: SectionListRenderItemInfo<ProxiwashMachineType>
267
+  ) => {
317 268
     const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers');
318 269
     return (
319 270
       <ProxiwashListItem
320 271
         item={data.item}
321
-        onPress={this.showModal}
272
+        onPress={showModal}
322 273
         isWatched={isMachineWatched(data.item, machinesWatched)}
323 274
         isDryer={isDryer}
324 275
         height={LIST_ITEM_HEIGHT}
@@ -332,7 +283,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
332 283
    * @param item The item to extract the key from
333 284
    * @return {*} The extracted key
334 285
    */
335
-  getKeyExtractor = (item: ProxiwashMachineType): string => item.number;
286
+  const getKeyExtractor = (item: ProxiwashMachineType): string => item.number;
336 287
 
337 288
   /**
338 289
    * Setups notifications for the machine with the given ID.
@@ -341,28 +292,19 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
341 292
    *
342 293
    * @param machine The machine to watch
343 294
    */
344
-  setupNotifications(machine: ProxiwashMachineType) {
345
-    const { machinesWatched } = this.state;
295
+  const setupNotifications = (machine: ProxiwashMachineType) => {
346 296
     if (!isMachineWatched(machine, machinesWatched)) {
347
-      Notifications.setupMachineNotification(
297
+      setupMachineNotification(
348 298
         machine.number,
349 299
         true,
350 300
         getMachineEndDate(machine)
351
-      )
352
-        .then(() => {
353
-          this.saveNotificationToState(machine);
354
-        })
355
-        .catch(() => {
356
-          ProxiwashScreen.showNotificationsDisabledWarning();
357
-        });
358
-    } else {
359
-      Notifications.setupMachineNotification(machine.number, false, null).then(
360
-        () => {
361
-          this.removeNotificationFromState(machine);
362
-        }
363 301
       );
302
+      saveNotificationToState(machine);
303
+    } else {
304
+      setupMachineNotification(machine.number, false);
305
+      removeNotificationFromState(machine);
364 306
     }
365
-  }
307
+  };
366 308
 
367 309
   /**
368 310
    * Gets the number of machines available
@@ -370,13 +312,9 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
370 312
    * @param isDryer True if we are only checking for dryer, false for washers
371 313
    * @return {number} The number of machines available
372 314
    */
373
-  getMachineAvailableNumber(isDryer: boolean): number {
374
-    let data;
375
-    if (isDryer) {
376
-      data = this.fetchedData.dryers;
377
-    } else {
378
-      data = this.fetchedData.washers;
379
-    }
315
+  const getMachineAvailableNumber = (
316
+    data: ReadonlyArray<ProxiwashMachineType>
317
+  ): number => {
380 318
     let count = 0;
381 319
     data.forEach((machine: ProxiwashMachineType) => {
382 320
       if (machine.state === MachineStates.AVAILABLE) {
@@ -384,7 +322,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
384 322
       }
385 323
     });
386 324
     return count;
387
-  }
325
+  };
388 326
 
389 327
   /**
390 328
    * Creates the dataset to be used by the FlatList
@@ -392,10 +330,9 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
392 330
    * @param fetchedData
393 331
    * @return {*}
394 332
    */
395
-  createDataset = (
333
+  const createDataset = (
396 334
     fetchedData: FetchedDataType | undefined
397 335
   ): SectionListDataType<ProxiwashMachineType> => {
398
-    const { state } = this;
399 336
     if (fetchedData) {
400 337
       let data = fetchedData;
401 338
       if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
@@ -403,24 +340,26 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
403 340
         AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
404 341
         AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
405 342
       }
406
-      this.fetchedData = data;
407
-      // TODO dirty, should be refactored
408
-      this.state.machinesWatched = getCleanedMachineWatched(
409
-        state.machinesWatched,
410
-        [...data.dryers, ...data.washers]
411
-      );
343
+      fetchedData = data;
344
+      const cleanedList = getCleanedMachineWatched(machinesWatched, [
345
+        ...data.dryers,
346
+        ...data.washers,
347
+      ]);
348
+      if (cleanedList !== machinesWatched) {
349
+        setMachinesWatched(machinesWatched);
350
+      }
412 351
       return [
413 352
         {
414 353
           title: i18n.t('screens.proxiwash.dryers'),
415 354
           icon: 'tumble-dryer',
416 355
           data: data.dryers === undefined ? [] : data.dryers,
417
-          keyExtractor: this.getKeyExtractor,
356
+          keyExtractor: getKeyExtractor,
418 357
         },
419 358
         {
420 359
           title: i18n.t('screens.proxiwash.washers'),
421 360
           icon: 'washing-machine',
422 361
           data: data.washers === undefined ? [] : data.washers,
423
-          keyExtractor: this.getKeyExtractor,
362
+          keyExtractor: getKeyExtractor,
424 363
         },
425 364
       ];
426 365
     } else {
@@ -429,27 +368,20 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
429 368
   };
430 369
 
431 370
   /**
432
-   * Callback used when the user clicks on the navigate to settings button.
433
-   * This will hide the banner and open the SettingsScreen
434
-   */
435
-  onGoToSettings = () => {
436
-    const { navigation } = this.props;
437
-    navigation.navigate('settings');
438
-  };
439
-
440
-  /**
441 371
    * Shows a modal for the given item
442 372
    *
443 373
    * @param title The title to use
444 374
    * @param item The item to display information for in the modal
445 375
    * @param isDryer True if the given item is a dryer
446 376
    */
447
-  showModal = (title: string, item: ProxiwashMachineType, isDryer: boolean) => {
448
-    this.setState({
449
-      modalCurrentDisplayItem: this.getModalContent(title, item, isDryer),
450
-    });
451
-    if (this.modalRef) {
452
-      this.modalRef.open();
377
+  const showModal = (
378
+    title: string,
379
+    item: ProxiwashMachineType,
380
+    isDryer: boolean
381
+  ) => {
382
+    setModalCurrentDisplayItem(getModalContent(title, item, isDryer));
383
+    if (modalRef.current) {
384
+      modalRef.current.open();
453 385
     }
454 386
   };
455 387
 
@@ -458,87 +390,76 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
458 390
    *
459 391
    * @param machine
460 392
    */
461
-  saveNotificationToState(machine: ProxiwashMachineType) {
462
-    const { machinesWatched } = this.state;
463
-    const data = machinesWatched;
393
+  const saveNotificationToState = (machine: ProxiwashMachineType) => {
394
+    let data = [...machinesWatched];
464 395
     data.push(machine);
465
-    this.saveNewWatchedList(data);
466
-  }
396
+    saveNewWatchedList(data);
397
+  };
467 398
 
468 399
   /**
469 400
    * Removes the given index from the watchlist array and saves it to preferences
470 401
    *
471 402
    * @param selectedMachine
472 403
    */
473
-  removeNotificationFromState(selectedMachine: ProxiwashMachineType) {
474
-    const { machinesWatched } = this.state;
475
-    const newList = [...machinesWatched];
476
-    machinesWatched.forEach((machine: ProxiwashMachineType, index: number) => {
477
-      if (
478
-        machine.number === selectedMachine.number &&
479
-        machine.endTime === selectedMachine.endTime
480
-      ) {
481
-        newList.splice(index, 1);
482
-      }
483
-    });
484
-    this.saveNewWatchedList(newList);
485
-  }
404
+  const removeNotificationFromState = (
405
+    selectedMachine: ProxiwashMachineType
406
+  ) => {
407
+    const newList = machinesWatched.filter(
408
+      (m) => m.number !== selectedMachine.number
409
+    );
410
+    saveNewWatchedList(newList);
411
+  };
486 412
 
487
-  saveNewWatchedList(list: Array<ProxiwashMachineType>) {
488
-    this.setState({ machinesWatched: list });
413
+  const saveNewWatchedList = (list: Array<ProxiwashMachineType>) => {
414
+    setMachinesWatched(list);
489 415
     AsyncStorageManager.set(
490 416
       AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
491 417
       list
492 418
     );
493
-  }
419
+  };
494 420
 
495
-  render() {
496
-    const { state } = this;
497
-    let data: LaundromatType;
498
-    switch (state.selectedWash) {
499
-      case 'tripodeB':
500
-        data = ProxiwashConstants.tripodeB;
501
-        break;
502
-      default:
503
-        data = ProxiwashConstants.washinsa;
504
-    }
505
-    return (
506
-      <View style={GENERAL_STYLES.flex}>
507
-        <View style={styles.container}>
508
-          <WebSectionList
509
-            request={() => readData<FetchedDataType>(data.url)}
510
-            createDataset={this.createDataset}
511
-            renderItem={this.getRenderItem}
512
-            renderSectionHeader={this.getRenderSectionHeader}
513
-            autoRefreshTime={REFRESH_TIME}
514
-            refreshOnFocus={true}
515
-            extraData={state.machinesWatched.length}
516
-          />
517
-        </View>
518
-        <MascotPopup
519
-          prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowMascot.key}
520
-          title={i18n.t('screens.proxiwash.mascotDialog.title')}
521
-          message={i18n.t('screens.proxiwash.mascotDialog.message')}
522
-          icon="information"
523
-          buttons={{
524
-            action: {
525
-              message: i18n.t('screens.proxiwash.mascotDialog.ok'),
526
-              icon: 'cog',
527
-              onPress: this.onGoToSettings,
528
-            },
529
-            cancel: {
530
-              message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
531
-              icon: 'close',
532
-            },
533
-          }}
534
-          emotion={MASCOT_STYLE.NORMAL}
421
+  let data: LaundromatType;
422
+  switch (selectedWash) {
423
+    case 'tripodeB':
424
+      data = ProxiwashConstants.tripodeB;
425
+      break;
426
+    default:
427
+      data = ProxiwashConstants.washinsa;
428
+  }
429
+  return (
430
+    <View style={GENERAL_STYLES.flex}>
431
+      <View style={styles.container}>
432
+        <WebSectionList
433
+          request={() => readData<FetchedDataType>(data.url)}
434
+          createDataset={createDataset}
435
+          renderItem={getRenderItem}
436
+          renderSectionHeader={getRenderSectionHeader}
437
+          autoRefreshTime={REFRESH_TIME}
438
+          refreshOnFocus={true}
439
+          extraData={machinesWatched.length}
535 440
         />
536
-        <CustomModal onRef={this.onModalRef}>
537
-          {state.modalCurrentDisplayItem}
538
-        </CustomModal>
539 441
       </View>
540
-    );
541
-  }
442
+      <MascotPopup
443
+        prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowMascot.key}
444
+        title={i18n.t('screens.proxiwash.mascotDialog.title')}
445
+        message={i18n.t('screens.proxiwash.mascotDialog.message')}
446
+        icon="information"
447
+        buttons={{
448
+          action: {
449
+            message: i18n.t('screens.proxiwash.mascotDialog.ok'),
450
+            icon: 'cog',
451
+            onPress: () => navigation.navigate('settings'),
452
+          },
453
+          cancel: {
454
+            message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
455
+            icon: 'close',
456
+          },
457
+        }}
458
+        emotion={MASCOT_STYLE.NORMAL}
459
+      />
460
+      <CustomModal ref={modalRef}>{modalCurrentDisplayItem}</CustomModal>
461
+    </View>
462
+  );
542 463
 }
543 464
 
544
-export default withTheme(ProxiwashScreen);
465
+export default ProxiwashScreen;

+ 59
- 54
src/utils/Notifications.ts View File

@@ -17,44 +17,59 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import {
21
-  checkNotifications,
22
-  requestNotifications,
23
-  RESULTS,
24
-} from 'react-native-permissions';
25 20
 import i18n from 'i18n-js';
26 21
 import AsyncStorageManager from '../managers/AsyncStorageManager';
27
-
28
-const PushNotification = require('react-native-push-notification');
22
+import PushNotificationIOS from '@react-native-community/push-notification-ios';
23
+import PushNotification from 'react-native-push-notification';
24
+import { Platform } from 'react-native';
29 25
 
30 26
 // Used to multiply the normal notification id to create the reminder one. It allows to find it back easily
31 27
 const reminderIdFactor = 100;
32 28
 
33
-/**
34
- * Async function asking permission to send notifications to the user.
35
- * Used on ios.
36
- *
37
- * @returns {Promise<void>}
38
- */
39
-export async function askPermissions(): Promise<void> {
40
-  return new Promise((resolve: () => void, reject: () => void) => {
41
-    checkNotifications().then(({ status }: { status: string }) => {
42
-      if (status === RESULTS.GRANTED) {
43
-        resolve();
44
-      } else if (status === RESULTS.BLOCKED) {
45
-        reject();
46
-      } else {
47
-        requestNotifications([]).then((result: { status: string }) => {
48
-          if (result.status === RESULTS.GRANTED) {
49
-            resolve();
50
-          } else {
51
-            reject();
52
-          }
53
-        });
54
-      }
55
-    });
56
-  });
57
-}
29
+PushNotification.createChannel(
30
+  {
31
+    channelId: 'reminders', // (required)
32
+    channelName: 'Reminders', // (required)
33
+    channelDescription: 'Get laundry reminders', // (optional) default: undefined.
34
+    playSound: true, // (optional) default: true
35
+    soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function
36
+    importance: 4, // (optional) default: 4. Int value of the Android notification importance
37
+    vibrate: true, // (optional) default: true. Creates the default vibration patten if true.
38
+  },
39
+  (created) => console.log(`createChannel returned '${created}'`) // (optional) callback returns whether the channel was created, false means it already existed.
40
+);
41
+
42
+PushNotification.configure({
43
+  // (required) Called when a remote is received or opened, or local notification is opened
44
+  onNotification: function (notification) {
45
+    console.log('NOTIFICATION:', notification);
46
+
47
+    // process the notification
48
+
49
+    // (required) Called when a remote is received or opened, or local notification is opened
50
+    notification.finish(PushNotificationIOS.FetchResult.NoData);
51
+  },
52
+
53
+  // IOS ONLY (optional): default: all - Permissions to register.
54
+  permissions: {
55
+    alert: true,
56
+    badge: true,
57
+    sound: true,
58
+  },
59
+
60
+  // Should the initial notification be popped automatically
61
+  // default: true
62
+  popInitialNotification: true,
63
+
64
+  /**
65
+   * (optional) default: true
66
+   * - Specified if permissions (ios) and token (android and ios) will requested or not,
67
+   * - if not, you must call PushNotificationsHandler.requestPermissions() later
68
+   * - if you are not using remote notification or do not have Firebase installed, use this:
69
+   *     requestPermissions: Platform.OS === 'ios'
70
+   */
71
+  requestPermissions: Platform.OS === 'ios',
72
+});
58 73
 
59 74
 /**
60 75
  * Creates a notification for the given machine id at the given date.
@@ -79,7 +94,7 @@ function createNotifications(machineID: string, date: Date) {
79 94
       message: i18n.t('screens.proxiwash.notifications.machineRunningBody', {
80 95
         number: machineID,
81 96
       }),
82
-      id: id.toString(),
97
+      id: id,
83 98
       date: reminderDate,
84 99
     });
85 100
   }
@@ -89,7 +104,7 @@ function createNotifications(machineID: string, date: Date) {
89 104
     message: i18n.t('screens.proxiwash.notifications.machineFinishedBody', {
90 105
       number: machineID,
91 106
     }),
92
-    id: machineID,
107
+    id: parseInt(machineID, 10),
93 108
     date,
94 109
   });
95 110
 }
@@ -104,26 +119,16 @@ function createNotifications(machineID: string, date: Date) {
104 119
  * @param isEnabled True to enable notifications, false to disable
105 120
  * @param endDate The trigger date, or null if disabling notifications
106 121
  */
107
-export async function setupMachineNotification(
122
+export function setupMachineNotification(
108 123
   machineID: string,
109 124
   isEnabled: boolean,
110
-  endDate: Date | null
111
-): Promise<void> {
112
-  return new Promise((resolve: () => void, reject: () => void) => {
113
-    if (isEnabled && endDate != null) {
114
-      askPermissions()
115
-        .then(() => {
116
-          createNotifications(machineID, endDate);
117
-          resolve();
118
-        })
119
-        .catch(() => {
120
-          reject();
121
-        });
122
-    } else {
123
-      PushNotification.cancelLocalNotifications({ id: machineID });
124
-      const reminderId = reminderIdFactor * parseInt(machineID, 10);
125
-      PushNotification.cancelLocalNotifications({ id: reminderId.toString() });
126
-      resolve();
127
-    }
128
-  });
125
+  endDate?: Date | null
126
+) {
127
+  if (isEnabled && endDate) {
128
+    createNotifications(machineID, endDate);
129
+  } else {
130
+    PushNotification.cancelLocalNotifications({ id: machineID });
131
+    const reminderId = reminderIdFactor * parseInt(machineID, 10);
132
+    PushNotification.cancelLocalNotifications({ id: reminderId.toString() });
133
+  }
129 134
 }

Loading…
Cancel
Save