Replaced local notifications with push notifications

This commit is contained in:
keplyx 2019-08-13 12:20:03 +02:00
parent 728e72b503
commit 8d4223333f
9 changed files with 103 additions and 93 deletions

4
App.js
View file

@ -13,6 +13,8 @@ import * as Font from 'expo-font';
import {clearThemeCache} from 'native-base-shoutem-theme'; import {clearThemeCache} from 'native-base-shoutem-theme';
import AsyncStorageManager from "./utils/AsyncStorageManager"; import AsyncStorageManager from "./utils/AsyncStorageManager";
import CustomIntroSlider from "./components/CustomIntroSlider"; import CustomIntroSlider from "./components/CustomIntroSlider";
import {Notifications} from 'expo';
import NotificationsManager from "./utils/NotificationsManager";
type Props = {}; type Props = {};
@ -47,6 +49,8 @@ export default class App extends React.Component<Props, State> {
}); });
await AsyncStorageManager.getInstance().loadPreferences(); await AsyncStorageManager.getInstance().loadPreferences();
ThemeManager.getInstance().setUpdateThemeCallback(() => this.updateTheme()); 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 // Only show intro if this is the first time starting the app
this.setState({ this.setState({
isLoading: false, isLoading: false,

View file

@ -17,11 +17,9 @@ type State = {
refreshing: boolean, refreshing: boolean,
firstLoading: boolean, firstLoading: boolean,
fetchedData: Object, fetchedData: Object,
machinesWatched: Array<Object>, machinesWatched: Array<string>,
}; };
const minTimeBetweenRefresh = 60;
/** /**
* Class used to create a basic list view using online json data. * Class used to create a basic list view using online json data.
* Used by inheriting from it and redefining getters. * Used by inheriting from it and redefining getters.
@ -36,6 +34,8 @@ export default class FetchedDataSectionList extends React.Component<Props, State
refreshTime: number; refreshTime: number;
lastRefresh: Date; lastRefresh: Date;
minTimeBetweenRefresh = 60;
constructor(fetchUrl: string, refreshTime: number) { constructor(fetchUrl: string, refreshTime: number) {
super(); super();
this.webDataManager = new WebDataManager(fetchUrl); this.webDataManager = new WebDataManager(fetchUrl);
@ -65,6 +65,10 @@ export default class FetchedDataSectionList extends React.Component<Props, State
return ["whoa", "nah"]; return ["whoa", "nah"];
} }
setMinTimeRefresh(value: number) {
this.minTimeBetweenRefresh = value;
}
/** /**
* Register react navigation events on first screen load. * Register react navigation events on first screen load.
* Allows to detect when the screen is focused * Allows to detect when the screen is focused
@ -117,7 +121,7 @@ export default class FetchedDataSectionList extends React.Component<Props, State
_onRefresh = () => { _onRefresh = () => {
let canRefresh; let canRefresh;
if (this.lastRefresh !== undefined) if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime())/1000 > minTimeBetweenRefresh; canRefresh = (new Date().getTime() - this.lastRefresh.getTime())/1000 > this.minTimeBetweenRefresh;
else else
canRefresh = true; canRefresh = true;

View file

@ -11,7 +11,7 @@ import ThemeManager from "../utils/ThemeManager";
const ICON_AMICALE = require('../assets/amicale.png'); const ICON_AMICALE = require('../assets/amicale.png');
const NAME_AMICALE = 'Amicale INSA Toulouse'; 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";
/** /**

View file

@ -16,8 +16,8 @@ type Props = {
const PLANEX_URL = 'http://planex.insa-toulouse.fr/'; 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. * Class defining the app's planex screen.
* This screen uses a webview to render the planex page * This screen uses a webview to render the planex page
@ -31,9 +31,9 @@ export default class PlanningScreen extends React.Component<Props> {
super(); super();
this.customInjectedJS = this.customInjectedJS =
'document.querySelector(\'head\').innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=1.0">\';' + '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()) 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"/>\';';
} }

View file

@ -6,7 +6,7 @@ import i18n from "i18n-js";
import {Platform, View} from "react-native"; import {Platform, View} from "react-native";
import CustomMaterialIcon from "../components/CustomMaterialIcon"; import CustomMaterialIcon from "../components/CustomMaterialIcon";
import ThemeManager from "../utils/ThemeManager"; import ThemeManager from "../utils/ThemeManager";
import {Linking} from "expo"; import {Linking, Notifications} from "expo";
import BaseContainer from "../components/BaseContainer"; import BaseContainer from "../components/BaseContainer";
type Props = { type Props = {
@ -25,6 +25,7 @@ function openWebLink(link) {
* Class defining the app's planning screen * Class defining the app's planning screen
*/ */
export default class PlanningScreen extends React.Component<Props> { export default class PlanningScreen extends React.Component<Props> {
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;
return ( return (

View file

@ -13,9 +13,7 @@ import Touchable from "react-native-platform-touchable";
import AsyncStorageManager from "../utils/AsyncStorageManager"; import AsyncStorageManager from "../utils/AsyncStorageManager";
import * as Expo from "expo"; import * as Expo from "expo";
const DATA_URL = "https://srv-falcon.etud.insa-toulouse.fr/~vergnet/appli-amicale/washinsa/washinsa.json"; const DATA_URL = "https://srv-falcon.etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
let reminderNotifTime = 5;
const MACHINE_STATES = { const MACHINE_STATES = {
"TERMINE": "0", "TERMINE": "0",
@ -43,7 +41,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
* Creates machine state parameters using current theme and translations * Creates machine state parameters using current theme and translations
*/ */
constructor() { constructor() {
super(DATA_URL, 1000 * 60); // Refresh every minute super(DATA_URL, 1000 * 30); // Refresh every half minute
let colors = ThemeManager.getCurrentThemeVariables(); let colors = ThemeManager.getCurrentThemeVariables();
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor; stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor; 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.HS] = 'alert-octagram-outline';
stateIcons[MACHINE_STATES.ERREUR] = 'alert'; stateIcons[MACHINE_STATES.ERREUR] = 'alert';
let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current; // let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current;
this.state = { this.state = {
refreshing: false, refreshing: false,
firstLoading: true, firstLoading: true,
fetchedData: {}, 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], 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() { 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 * Another will be send a few minutes before the end, based on the value of reminderNotifTime
* *
* @param machineId The machine's ID * @param machineId The machine's ID
* @param remainingTime The time remaining for this machine
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async setupNotifications(machineId: string, remainingTime: number) { setupNotifications(machineId: string) {
if (!this.isMachineWatched(machineId)) { if (!this.isMachineWatched(machineId)) {
let endNotificationID = await NotificationsManager.scheduleNotification( NotificationsManager.setupMachineNotification(machineId, true);
i18n.t('proxiwashScreen.notifications.machineFinishedTitle'), this.saveNotificationToPrefs(machineId);
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);
} else } else
this.disableNotification(machineId); 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. * Stop scheduled notifications for the machine of the given ID.
* This will also remove the notification if it was already shown. * This will also remove the notification if it was already shown.
@ -192,43 +151,22 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
disableNotification(machineId: string) { disableNotification(machineId: string) {
let data = this.state.machinesWatched; let data = this.state.machinesWatched;
if (data.length > 0) { if (data.length > 0) {
let arrayIndex = this.getMachineIndexInWatchList(machineId); let arrayIndex = data.indexOf(machineId);
if (arrayIndex !== -1) { if (arrayIndex !== -1) {
NotificationsManager.cancelScheduledNotification(data[arrayIndex].endNotificationID); NotificationsManager.setupMachineNotification(machineId, false);
if (data[arrayIndex].reminderNotificationID !== null)
NotificationsManager.cancelScheduledNotification(data[arrayIndex].reminderNotificationID);
this.removeNotificationFromPrefs(arrayIndex); 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 * Add the given notifications associated to a machine ID to the watchlist, and save the array to the preferences
* *
* @param machineId * @param machineId
* @param endNotificationID
* @param reminderNotificationID
*/ */
saveNotificationToPrefs(machineId: string, endNotificationID: string, reminderNotificationID: string | null) { saveNotificationToPrefs(machineId: string) {
let data = this.state.machinesWatched; let data = this.state.machinesWatched;
data.push({ data.push(machineId);
machineNumber: machineId,
endNotificationID: endNotificationID,
reminderNotificationID: reminderNotificationID
});
this.updateNotificationPrefs(data); this.updateNotificationPrefs(data);
} }
@ -250,8 +188,8 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
*/ */
updateNotificationPrefs(data: Array<Object>) { updateNotificationPrefs(data: Array<Object>) {
this.setState({machinesWatched: data}); this.setState({machinesWatched: data});
let prefKey = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.key; // let prefKey = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.key;
AsyncStorageManager.getInstance().savePref(prefKey, JSON.stringify(data)); // AsyncStorageManager.getInstance().savePref(prefKey, JSON.stringify(data));
} }
/** /**
@ -261,9 +199,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
* @returns {boolean} * @returns {boolean}
*/ */
isMachineWatched(machineID: string) { isMachineWatched(machineID: string) {
return this.state.machinesWatched.find(function (elem) { return this.state.machinesWatched.indexOf(machineID) !== -1;
return elem.machineNumber === machineID
}) !== undefined;
} }
createDataset(fetchedData: Object) { createDataset(fetchedData: Object) {
@ -307,7 +243,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
text: this.isMachineWatched(item.number) ? text: this.isMachineWatched(item.number) ?
i18n.t("proxiwashScreen.modal.disableNotifications") : i18n.t("proxiwashScreen.modal.disableNotifications") :
i18n.t("proxiwashScreen.modal.enableNotifications"), i18n.t("proxiwashScreen.modal.enableNotifications"),
onPress: () => this.setupNotifications(item.number, remainingTime) onPress: () => this.setupNotifications(item.number)
}, },
{ {
text: i18n.t("proxiwashScreen.modal.cancel") text: i18n.t("proxiwashScreen.modal.cancel")

View file

@ -16,6 +16,8 @@ type Props = {
const RU_URL = 'http://m.insa-toulouse.fr/ru.html'; 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. * Class defining the app's planex screen.
@ -30,9 +32,9 @@ export default class SelfMenuScreen extends React.Component<Props> {
super(); super();
this.customInjectedJS = this.customInjectedJS =
'document.querySelector(\'head\').innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=1.0">\';' + '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()) 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() { getRefreshButton() {

View file

@ -43,6 +43,11 @@ export default class AsyncStorageManager {
key: 'nightMode', key: 'nightMode',
default: '0', default: '0',
current: '', current: '',
},
expoToken: {
key: 'expoToken',
default: '',
current: '',
} }
}; };

View file

@ -2,6 +2,9 @@
import * as Permissions from 'expo-permissions'; import * as Permissions from 'expo-permissions';
import {Notifications} from 'expo'; 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 * Static class used to manage notifications sent to the user
@ -45,10 +48,15 @@ export default class NotificationsManager {
* @param body Notification body text * @param body Notification body text
* @param time Time at which we should send the notification * @param time Time at which we should send the notification
* @param data Data to send with the notification, used for listeners * @param data Data to send with the notification, used for listeners
* @param androidChannelID
* @returns {Promise<import("react").ReactText>} Notification Id * @returns {Promise<import("react").ReactText>} Notification Id
*/ */
static async scheduleNotification(title: string, body: string, time: number, data: Object, androidChannelID: string): Promise<string> { static async scheduleNotification(title: string, body: string, time: number, data: Object, androidChannelID: string): Promise<string> {
await NotificationsManager.askPermissions(); await NotificationsManager.askPermissions();
console.log(time);
let date = new Date();
date.setTime(time);
console.log(date);
return Notifications.scheduleLocalNotificationAsync( return Notifications.scheduleLocalNotificationAsync(
{ {
title: title, title: title,
@ -75,4 +83,54 @@ export default class NotificationsManager {
static async cancelScheduledNotification(notificationID: number) { static async cancelScheduledNotification(notificationID: number) {
await Notifications.cancelScheduledNotificationAsync(notificationID); 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);
});
}
} }