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 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,

View file

@ -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;

View file

@ -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";
/**

View file

@ -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"/>\';';
}

View file

@ -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 (

View file

@ -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")

View file

@ -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() {

View file

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

View file

@ -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);
});
}
}