forked from vergnet/application-amicale
		
	Replaced local notifications with push notifications
This commit is contained in:
		
							parent
							
								
									728e72b503
								
							
						
					
					
						commit
						8d4223333f
					
				
					 9 changed files with 103 additions and 93 deletions
				
			
		
							
								
								
									
										4
									
								
								App.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								App.js
									
									
									
									
									
								
							|  | @ -13,6 +13,8 @@ import * as Font from 'expo-font'; | |||
| import {clearThemeCache} from 'native-base-shoutem-theme'; | ||||
| import AsyncStorageManager from "./utils/AsyncStorageManager"; | ||||
| import CustomIntroSlider from "./components/CustomIntroSlider"; | ||||
| import {Notifications} from 'expo'; | ||||
| import NotificationsManager from "./utils/NotificationsManager"; | ||||
| 
 | ||||
| type Props = {}; | ||||
| 
 | ||||
|  | @ -47,6 +49,8 @@ export default class App extends React.Component<Props, State> { | |||
|         }); | ||||
|         await AsyncStorageManager.getInstance().loadPreferences(); | ||||
|         ThemeManager.getInstance().setUpdateThemeCallback(() => this.updateTheme()); | ||||
|         await NotificationsManager.initExpoToken(); | ||||
|         console.log(AsyncStorageManager.getInstance().preferences.expoToken.current); | ||||
|         // Only show intro if this is the first time starting the app
 | ||||
|         this.setState({ | ||||
|             isLoading: false, | ||||
|  |  | |||
|  | @ -17,11 +17,9 @@ type State = { | |||
|     refreshing: boolean, | ||||
|     firstLoading: boolean, | ||||
|     fetchedData: Object, | ||||
|     machinesWatched: Array<Object>, | ||||
|     machinesWatched: Array<string>, | ||||
| }; | ||||
| 
 | ||||
| const minTimeBetweenRefresh = 60; | ||||
| 
 | ||||
| /** | ||||
|  * Class used to create a basic list view using online json data. | ||||
|  * Used by inheriting from it and redefining getters. | ||||
|  | @ -36,6 +34,8 @@ export default class FetchedDataSectionList extends React.Component<Props, State | |||
|     refreshTime: number; | ||||
|     lastRefresh: Date; | ||||
| 
 | ||||
|     minTimeBetweenRefresh = 60; | ||||
| 
 | ||||
|     constructor(fetchUrl: string, refreshTime: number) { | ||||
|         super(); | ||||
|         this.webDataManager = new WebDataManager(fetchUrl); | ||||
|  | @ -65,6 +65,10 @@ export default class FetchedDataSectionList extends React.Component<Props, State | |||
|         return ["whoa", "nah"]; | ||||
|     } | ||||
| 
 | ||||
|     setMinTimeRefresh(value: number) { | ||||
|         this.minTimeBetweenRefresh = value; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Register react navigation events on first screen load. | ||||
|      * Allows to detect when the screen is focused | ||||
|  | @ -117,7 +121,7 @@ export default class FetchedDataSectionList extends React.Component<Props, State | |||
|     _onRefresh = () => { | ||||
|         let canRefresh; | ||||
|         if (this.lastRefresh !== undefined) | ||||
|             canRefresh = (new Date().getTime() - this.lastRefresh.getTime())/1000 > minTimeBetweenRefresh; | ||||
|             canRefresh = (new Date().getTime() - this.lastRefresh.getTime())/1000 > this.minTimeBetweenRefresh; | ||||
|         else | ||||
|             canRefresh = true; | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ import ThemeManager from "../utils/ThemeManager"; | |||
| 
 | ||||
| const ICON_AMICALE = require('../assets/amicale.png'); | ||||
| const NAME_AMICALE = 'Amicale INSA Toulouse'; | ||||
| const DATA_URL = "https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/facebook_data.json"; | ||||
| const DATA_URL = "https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/facebook/facebook_data.json"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -16,8 +16,8 @@ type Props = { | |||
| 
 | ||||
| const PLANEX_URL = 'http://planex.insa-toulouse.fr/'; | ||||
| 
 | ||||
| const CUSTOM_CSS_LINK = 'https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/planex/generalCustom.css'; | ||||
| 
 | ||||
| const CUSTOM_CSS_GENERAL = 'https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/custom_css/planex/customMobile.css'; | ||||
| const CUSTOM_CSS_NIGHTMODE = 'https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/custom_css/planex/customDark.css'; | ||||
| /** | ||||
|  * Class defining the app's planex screen. | ||||
|  * This screen uses a webview to render the planex page | ||||
|  | @ -31,9 +31,9 @@ export default class PlanningScreen extends React.Component<Props> { | |||
|         super(); | ||||
|         this.customInjectedJS = | ||||
|             'document.querySelector(\'head\').innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=1.0">\';' + | ||||
|             'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/planex/customMobile.css" type="text/css"/>\';'; | ||||
|             'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="' + CUSTOM_CSS_GENERAL + '" type="text/css"/>\';'; | ||||
|         if (ThemeManager.getNightMode()) | ||||
|             this.customInjectedJS += 'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/planex/customDark.css" type="text/css"/>\';'; | ||||
|             this.customInjectedJS += 'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="' + CUSTOM_CSS_NIGHTMODE + '" type="text/css"/>\';'; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import i18n from "i18n-js"; | |||
| import {Platform, View} from "react-native"; | ||||
| import CustomMaterialIcon from "../components/CustomMaterialIcon"; | ||||
| import ThemeManager from "../utils/ThemeManager"; | ||||
| import {Linking} from "expo"; | ||||
| import {Linking, Notifications} from "expo"; | ||||
| import BaseContainer from "../components/BaseContainer"; | ||||
| 
 | ||||
| type Props = { | ||||
|  | @ -25,6 +25,7 @@ function openWebLink(link) { | |||
|  * Class defining the app's planning screen | ||||
|  */ | ||||
| export default class PlanningScreen extends React.Component<Props> { | ||||
| 
 | ||||
|     render() { | ||||
|         const nav = this.props.navigation; | ||||
|         return ( | ||||
|  |  | |||
|  | @ -13,9 +13,7 @@ import Touchable from "react-native-platform-touchable"; | |||
| import AsyncStorageManager from "../utils/AsyncStorageManager"; | ||||
| import * as Expo from "expo"; | ||||
| 
 | ||||
| const DATA_URL = "https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/washinsa/washinsa.json"; | ||||
| 
 | ||||
| let reminderNotifTime = 5; | ||||
| const DATA_URL = "https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json"; | ||||
| 
 | ||||
| const MACHINE_STATES = { | ||||
|     "TERMINE": "0", | ||||
|  | @ -43,7 +41,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|      * Creates machine state parameters using current theme and translations | ||||
|      */ | ||||
|     constructor() { | ||||
|         super(DATA_URL, 1000 * 60); // Refresh every minute
 | ||||
|         super(DATA_URL, 1000 * 30); // Refresh every half minute
 | ||||
|         let colors = ThemeManager.getCurrentThemeVariables(); | ||||
|         stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor; | ||||
|         stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor; | ||||
|  | @ -69,13 +67,15 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|         stateIcons[MACHINE_STATES.HS] = 'alert-octagram-outline'; | ||||
|         stateIcons[MACHINE_STATES.ERREUR] = 'alert'; | ||||
| 
 | ||||
|         let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current; | ||||
|         // let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current;
 | ||||
|         this.state = { | ||||
|             refreshing: false, | ||||
|             firstLoading: true, | ||||
|             fetchedData: {}, | ||||
|             machinesWatched: JSON.parse(dataString), | ||||
|             // machinesWatched: JSON.parse(dataString),
 | ||||
|             machinesWatched: [], | ||||
|         }; | ||||
|         this.setMinTimeRefresh(30); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -90,14 +90,6 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|                 vibrate: [0, 250, 250, 250], | ||||
|             }); | ||||
|         } | ||||
|         // Remove machine from watch list when receiving last notification
 | ||||
|         Expo.Notifications.addListener((notification) => { | ||||
|             if (notification.data !== undefined) { | ||||
|                 if (this.isMachineWatched(notification.data.id) && notification.data.isMachineFinished === true) { | ||||
|                     this.removeNotificationFromPrefs(this.getMachineIndexInWatchList(notification.data.id)); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     getHeaderTranslation() { | ||||
|  | @ -140,49 +132,16 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|      * Another will be send a few minutes before the end, based on the value of reminderNotifTime | ||||
|      * | ||||
|      * @param machineId The machine's ID | ||||
|      * @param remainingTime The time remaining for this machine | ||||
|      * @returns {Promise<void>} | ||||
|      */ | ||||
|     async setupNotifications(machineId: string, remainingTime: number) { | ||||
|     setupNotifications(machineId: string) { | ||||
|         if (!this.isMachineWatched(machineId)) { | ||||
|             let endNotificationID = await NotificationsManager.scheduleNotification( | ||||
|                 i18n.t('proxiwashScreen.notifications.machineFinishedTitle'), | ||||
|                 i18n.t('proxiwashScreen.notifications.machineFinishedBody', {number: machineId}), | ||||
|                 new Date().getTime() + remainingTime * (60 * 1000), // Convert back to milliseconds
 | ||||
|                 {id: machineId, isMachineFinished: true}, | ||||
|                 'reminders' | ||||
|             ); | ||||
|             let reminderNotificationID = await ProxiwashScreen.setupReminderNotification(machineId, remainingTime); | ||||
|             this.saveNotificationToPrefs(machineId, endNotificationID, reminderNotificationID); | ||||
|             NotificationsManager.setupMachineNotification(machineId, true); | ||||
|             this.saveNotificationToPrefs(machineId); | ||||
|         } else | ||||
|             this.disableNotification(machineId); | ||||
|     } | ||||
| 
 | ||||
|     static async setupReminderNotification(machineId: string, remainingTime: number): Promise<string | null> { | ||||
|         let reminderNotificationID: string | null = null; | ||||
|         let reminderNotificationTime = ProxiwashScreen.getReminderNotificationTime(); | ||||
|         if (remainingTime > reminderNotificationTime && reminderNotificationTime > 0) { | ||||
|             reminderNotificationID = await NotificationsManager.scheduleNotification( | ||||
|                 i18n.t('proxiwashScreen.notifications.machineRunningTitle', {time: reminderNotificationTime}), | ||||
|                 i18n.t('proxiwashScreen.notifications.machineRunningBody', {number: machineId}), | ||||
|                 new Date().getTime() + (remainingTime - reminderNotificationTime) * (60 * 1000), // Convert back to milliseconds
 | ||||
|                 {id: machineId, isMachineFinished: false}, | ||||
|                 'reminders' | ||||
|             ); | ||||
|         } | ||||
|         return reminderNotificationID; | ||||
|     } | ||||
| 
 | ||||
|     static getReminderNotificationTime(): number { | ||||
|         let val = AsyncStorageManager.getInstance().preferences.proxiwashNotifications.current; | ||||
|         if (val !== "never") | ||||
|             reminderNotifTime = parseInt(val); | ||||
|         else | ||||
|             reminderNotifTime = -1; | ||||
|         return reminderNotifTime; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Stop scheduled notifications for the machine of the given ID. | ||||
|      * This will also remove the notification if it was already shown. | ||||
|  | @ -192,43 +151,22 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|     disableNotification(machineId: string) { | ||||
|         let data = this.state.machinesWatched; | ||||
|         if (data.length > 0) { | ||||
|             let arrayIndex = this.getMachineIndexInWatchList(machineId); | ||||
|             let arrayIndex = data.indexOf(machineId); | ||||
|             if (arrayIndex !== -1) { | ||||
|                 NotificationsManager.cancelScheduledNotification(data[arrayIndex].endNotificationID); | ||||
|                 if (data[arrayIndex].reminderNotificationID !== null) | ||||
|                     NotificationsManager.cancelScheduledNotification(data[arrayIndex].reminderNotificationID); | ||||
|                 NotificationsManager.setupMachineNotification(machineId, false); | ||||
|                 this.removeNotificationFromPrefs(arrayIndex); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the index of the given machine ID in the watchlist array | ||||
|      * | ||||
|      * @param machineId | ||||
|      * @return | ||||
|      */ | ||||
|     getMachineIndexInWatchList(machineId: string): number { | ||||
|         let elem = this.state.machinesWatched.find(function (elem) { | ||||
|             return elem.machineNumber === machineId | ||||
|         }); | ||||
|         return this.state.machinesWatched.indexOf(elem); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add the given notifications associated to a machine ID to the watchlist, and save the array to the preferences | ||||
|      * | ||||
|      * @param machineId | ||||
|      * @param endNotificationID | ||||
|      * @param reminderNotificationID | ||||
|      */ | ||||
|     saveNotificationToPrefs(machineId: string, endNotificationID: string, reminderNotificationID: string | null) { | ||||
|     saveNotificationToPrefs(machineId: string) { | ||||
|         let data = this.state.machinesWatched; | ||||
|         data.push({ | ||||
|             machineNumber: machineId, | ||||
|             endNotificationID: endNotificationID, | ||||
|             reminderNotificationID: reminderNotificationID | ||||
|         }); | ||||
|         data.push(machineId); | ||||
|         this.updateNotificationPrefs(data); | ||||
|     } | ||||
| 
 | ||||
|  | @ -250,8 +188,8 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|      */ | ||||
|     updateNotificationPrefs(data: Array<Object>) { | ||||
|         this.setState({machinesWatched: data}); | ||||
|         let prefKey = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.key; | ||||
|         AsyncStorageManager.getInstance().savePref(prefKey, JSON.stringify(data)); | ||||
|         // let prefKey = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.key;
 | ||||
|         // AsyncStorageManager.getInstance().savePref(prefKey, JSON.stringify(data));
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -261,9 +199,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|      * @returns {boolean} | ||||
|      */ | ||||
|     isMachineWatched(machineID: string) { | ||||
|         return this.state.machinesWatched.find(function (elem) { | ||||
|             return elem.machineNumber === machineID | ||||
|         }) !== undefined; | ||||
|         return this.state.machinesWatched.indexOf(machineID) !== -1; | ||||
|     } | ||||
| 
 | ||||
|     createDataset(fetchedData: Object) { | ||||
|  | @ -307,7 +243,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList { | |||
|                     text: this.isMachineWatched(item.number) ? | ||||
|                         i18n.t("proxiwashScreen.modal.disableNotifications") : | ||||
|                         i18n.t("proxiwashScreen.modal.enableNotifications"), | ||||
|                     onPress: () => this.setupNotifications(item.number, remainingTime) | ||||
|                     onPress: () => this.setupNotifications(item.number) | ||||
|                 }, | ||||
|                 { | ||||
|                     text: i18n.t("proxiwashScreen.modal.cancel") | ||||
|  |  | |||
|  | @ -16,6 +16,8 @@ type Props = { | |||
| 
 | ||||
| 
 | ||||
| const RU_URL = 'http://m.insa-toulouse.fr/ru.html'; | ||||
| const CUSTOM_CSS_GENERAL = 'https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/custom_css/RU/customGeneral.css'; | ||||
| const CUSTOM_CSS_LIGHT = 'https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/custom_css/RU/customLight.css'; | ||||
| 
 | ||||
| /** | ||||
|  * Class defining the app's planex screen. | ||||
|  | @ -30,9 +32,9 @@ export default class SelfMenuScreen extends React.Component<Props> { | |||
|         super(); | ||||
|         this.customInjectedJS = | ||||
|             'document.querySelector(\'head\').innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=1.0">\';' + | ||||
|             'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/RU/customGeneral.css" type="text/css"/>\';'; | ||||
|             'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="' + CUSTOM_CSS_GENERAL + '" type="text/css"/>\';'; | ||||
|         if (!ThemeManager.getNightMode()) | ||||
|             this.customInjectedJS += 'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/RU/customLight.css" type="text/css"/>\';'; | ||||
|             this.customInjectedJS += 'document.querySelector(\'head\').innerHTML += \'<link rel="stylesheet" href="' + CUSTOM_CSS_LIGHT + '" type="text/css"/>\';'; | ||||
|     } | ||||
| 
 | ||||
|     getRefreshButton() { | ||||
|  |  | |||
|  | @ -43,6 +43,11 @@ export default class AsyncStorageManager { | |||
|             key: 'nightMode', | ||||
|             default: '0', | ||||
|             current: '', | ||||
|         }, | ||||
|         expoToken: { | ||||
|             key: 'expoToken', | ||||
|             default: '', | ||||
|             current: '', | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,9 @@ | |||
| 
 | ||||
| import * as Permissions from 'expo-permissions'; | ||||
| import {Notifications} from 'expo'; | ||||
| import AsyncStorageManager from "./AsyncStorageManager"; | ||||
| 
 | ||||
| const EXPO_TOKEN_SERVER = 'https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/expo_notifications/save_token.php'; | ||||
| 
 | ||||
| /** | ||||
|  * Static class used to manage notifications sent to the user | ||||
|  | @ -45,10 +48,15 @@ export default class NotificationsManager { | |||
|      * @param body Notification body text | ||||
|      * @param time Time at which we should send the notification | ||||
|      * @param data Data to send with the notification, used for listeners | ||||
|      * @param androidChannelID | ||||
|      * @returns {Promise<import("react").ReactText>} Notification Id | ||||
|      */ | ||||
|     static async scheduleNotification(title: string, body: string, time: number, data: Object, androidChannelID: string): Promise<string> { | ||||
|         await NotificationsManager.askPermissions(); | ||||
|         console.log(time); | ||||
|         let date = new Date(); | ||||
|         date.setTime(time); | ||||
|         console.log(date); | ||||
|         return Notifications.scheduleLocalNotificationAsync( | ||||
|             { | ||||
|                 title: title, | ||||
|  | @ -75,4 +83,54 @@ export default class NotificationsManager { | |||
|     static async cancelScheduledNotification(notificationID: number) { | ||||
|         await Notifications.cancelScheduledNotificationAsync(notificationID); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save expo token to allow sending notifications to this device. | ||||
|      * This token is unique for each device and won't change. | ||||
|      * It only needs to be fetched once, then it will be saved in storage. | ||||
|      * | ||||
|      * @return {Promise<void>} | ||||
|      */ | ||||
|     static async initExpoToken() { | ||||
|         let token = AsyncStorageManager.getInstance().preferences.expoToken.current; | ||||
|         if (AsyncStorageManager.getInstance().preferences.expoToken.current === '') { | ||||
|             let expoToken = await Notifications.getExpoPushTokenAsync(); | ||||
|             // Save token for instant use later on
 | ||||
|             AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.expoToken.key, expoToken); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Ask the server to enable/disable notifications for the specified machine | ||||
|      * | ||||
|      * @param machineID | ||||
|      * @param isEnabled | ||||
|      */ | ||||
|     static setupMachineNotification(machineID: string, isEnabled: boolean) { | ||||
|         let token = AsyncStorageManager.getInstance().preferences.expoToken.current; | ||||
|         if (token === '') { | ||||
|             throw Error('Expo token not available'); | ||||
|         } | ||||
|         let data = { | ||||
|             function: 'setup_machine_notification', | ||||
|             token: token, | ||||
|             machine_id: machineID, | ||||
|             enabled: isEnabled | ||||
|         }; | ||||
|         fetch(EXPO_TOKEN_SERVER, { | ||||
|             method: 'POST', | ||||
|             headers: new Headers({ | ||||
|                 Accept: 'application/json', | ||||
|                 'Content-Type': 'application/json', | ||||
|             }), | ||||
|             body: JSON.stringify(data) // <-- Post parameters
 | ||||
|         }) | ||||
|             .then((response) => response.text()) | ||||
|             .then((responseText) => { | ||||
|                 console.log(responseText); | ||||
|             }) | ||||
|             .catch((error) => { | ||||
|                 console.log(error); | ||||
|             }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue