Update proxiwash screen and notifications

This commit is contained in:
Arnaud Vergnet 2021-05-12 23:01:41 +02:00
parent e08fdc7c37
commit 27199b85e5
8 changed files with 252 additions and 329 deletions

View file

@ -8,7 +8,6 @@
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@ -19,31 +18,33 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
> >
<!-- NOTIFICATIONS -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="reminders"/>
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="reminders"/>
<!-- Change the resource name to your App's accent color - or any other color you want -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/colorPrimary"/> <!-- or @android:color/{name} to use a standard color -->
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher"/> <!-- START NOTIFICATIONS -->
<!-- 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) -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="false"/>
Change the resource name to your App's accent color - or any other color you want
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/colorPrimary"/>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<service <service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService" android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false"> android:exported="false" >
<intent-filter> <intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<!-- END NOTIFICATIONS -->
<!-- END NOTIFICATIONS-->
<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false"/> <meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false"/>

View file

@ -5,22 +5,11 @@ import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView; import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import android.content.Intent;
import android.content.res.Configuration;
import org.devio.rn.splashscreen.SplashScreen; import org.devio.rn.splashscreen.SplashScreen;
public class MainActivity extends ReactActivity { public class MainActivity extends ReactActivity {
// Added automatically by Expo Config
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
sendBroadcast(intent);
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this, R.style.SplashScreenTheme); SplashScreen.show(this, R.style.SplashScreenTheme);

15
package-lock.json generated
View file

@ -2530,6 +2530,12 @@
"@types/xdate": "*" "@types/xdate": "*"
} }
}, },
"@types/react-native-push-notification": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/react-native-push-notification/-/react-native-push-notification-7.2.0.tgz",
"integrity": "sha512-4kErWFa0qit8qzPB6Nbp7kG9NiwDyKu5XxrNlrCIc1zoFxu48ABeofVvNCKv2RtlmFvCftibtykeysRZCeuT8A==",
"dev": true
},
"@types/react-native-vector-icons": { "@types/react-native-vector-icons": {
"version": "6.4.6", "version": "6.4.6",
"resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.6.tgz", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.6.tgz",
@ -10684,12 +10690,9 @@
"integrity": "sha512-8xiEnU29qHZcT05XXwhPHiLChTt82Pn5Z/nFdDOYGNFZ+IYSbYeGmIxFpratCRO6dgLptNaDFDPiyw2X7UZTeg==" "integrity": "sha512-8xiEnU29qHZcT05XXwhPHiLChTt82Pn5Z/nFdDOYGNFZ+IYSbYeGmIxFpratCRO6dgLptNaDFDPiyw2X7UZTeg=="
}, },
"react-native-push-notification": { "react-native-push-notification": {
"version": "5.1.1", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-5.1.1.tgz", "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-7.3.0.tgz",
"integrity": "sha512-CJmKqzM2P/s+a9PImoaiUN4TP1+K4YfmG1B0uUbavgFdGhTtRPTLLwDfFk2h3J6VmTXNak82rUz2iGwyptHm5w==", "integrity": "sha512-Ofy8dYAhIkFJKxQDvAn7BnXxwtun1SMnqLjZUhRTRzhPEqN0tW7TmIjfyYanNf/lnggRYZqFO+b14Ul3nhlMGw=="
"requires": {
"@react-native-community/push-notification-ios": "^1.4.0"
}
}, },
"react-native-reanimated": { "react-native-reanimated": {
"version": "1.13.2", "version": "1.13.2",

View file

@ -45,7 +45,7 @@
"react-native-modalize": "2.0.8", "react-native-modalize": "2.0.8",
"react-native-paper": "4.8.1", "react-native-paper": "4.8.1",
"react-native-permissions": "3.0.3", "react-native-permissions": "3.0.3",
"react-native-push-notification": "5.1.1", "react-native-push-notification": "7.3.0",
"react-native-reanimated": "1.13.2", "react-native-reanimated": "1.13.2",
"react-native-render-html": "5.1.0", "react-native-render-html": "5.1.0",
"react-native-safe-area-context": "3.2.0", "react-native-safe-area-context": "3.2.0",
@ -65,6 +65,7 @@
"@types/react": "17.0.3", "@types/react": "17.0.3",
"@types/react-native": "0.64.4", "@types/react-native": "0.64.4",
"@types/react-native-calendars": "1.20.10", "@types/react-native-calendars": "1.20.10",
"@types/react-native-push-notification": "^7.2.0",
"@types/react-native-vector-icons": "6.4.6", "@types/react-native-vector-icons": "6.4.6",
"@types/react-test-renderer": "17.0.1", "@types/react-test-renderer": "17.0.1",
"@typescript-eslint/eslint-plugin": "4.22.1", "@typescript-eslint/eslint-plugin": "4.22.1",
@ -94,7 +95,9 @@
"rules": { "rules": {
"no-undef": 0, "no-undef": 0,
"no-shadow": "off", "no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-shadow": [
"error"
],
"prettier/prettier": [ "prettier/prettier": [
"error", "error",
{ {

View file

@ -17,27 +17,28 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { Ref } from 'react';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { Modalize } from 'react-native-modalize'; import { Modalize } from 'react-native-modalize';
import { View } from 'react-native-animatable'; import { View } from 'react-native-animatable';
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
type Props = {
children?: React.ReactChild | null;
};
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
* *
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref. * @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*} * @return {*}
*/ */
function CustomModal(props: { function CustomModal(props: Props, ref?: Ref<Modalize>) {
onRef: (re: Modalize) => void;
children?: React.ReactNode;
}) {
const theme = useTheme(); const theme = useTheme();
const { onRef, children } = props; const { children } = props;
return ( return (
<Modalize <Modalize
ref={onRef} ref={ref}
adjustToContentHeight adjustToContentHeight
handlePosition="inside" handlePosition="inside"
modalStyle={{ backgroundColor: theme.colors.card }} modalStyle={{ backgroundColor: theme.colors.card }}
@ -54,4 +55,4 @@ function CustomModal(props: {
); );
} }
export default CustomModal; export default React.forwardRef(CustomModal);

View file

@ -17,20 +17,17 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { import {
Alert,
SectionListData, SectionListData,
SectionListRenderItemInfo, SectionListRenderItemInfo,
StyleSheet, StyleSheet,
View, View,
} from 'react-native'; } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper'; import { Avatar, Button, Card, Text, useTheme } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack';
import { Modalize } from 'react-native-modalize'; import { Modalize } from 'react-native-modalize';
import WebSectionList from '../../components/Screens/WebSectionList'; import WebSectionList from '../../components/Screens/WebSectionList';
import * as Notifications from '../../utils/Notifications';
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem'; import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem';
import ProxiwashConstants, { import ProxiwashConstants, {
@ -53,6 +50,8 @@ import type { SectionListDataType } from '../../components/Screens/WebSectionLis
import type { LaundromatType } from './ProxiwashAboutScreen'; import type { LaundromatType } from './ProxiwashAboutScreen';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import { readData } from '../../utils/WebData'; import { readData } from '../../utils/WebData';
import { useFocusEffect, useNavigation } from '@react-navigation/core';
import { setupMachineNotification } from '../../utils/Notifications';
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -68,17 +67,6 @@ export type ProxiwashMachineType = {
program: string; program: string;
}; };
type PropsType = {
navigation: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
};
type StateType = {
modalCurrentDisplayItem: React.ReactNode;
machinesWatched: Array<ProxiwashMachineType>;
selectedWash: string;
};
type FetchedDataType = { type FetchedDataType = {
dryers: Array<ProxiwashMachineType>; dryers: Array<ProxiwashMachineType>;
washers: Array<ProxiwashMachineType>; washers: Array<ProxiwashMachineType>;
@ -99,22 +87,28 @@ const styles = StyleSheet.create({
}, },
}); });
/** function ProxiwashScreen() {
* Class defining the app's proxiwash screen. This screen shows information about washing machines and const navigation = useNavigation();
* dryers, taken from a scrapper reading proxiwash website const theme = useTheme();
*/ const [
class ProxiwashScreen extends React.Component<PropsType, StateType> { modalCurrentDisplayItem,
/** setModalCurrentDisplayItem,
* Shows a warning telling the user notifications are disabled for the app ] = useState<React.ReactElement | null>(null);
*/ const [machinesWatched, setMachinesWatched] = useState<
static showNotificationsDisabledWarning() { Array<ProxiwashMachineType>
Alert.alert( >(
i18n.t('screens.proxiwash.modal.notificationErrorTitle'), AsyncStorageManager.getObject(
i18n.t('screens.proxiwash.modal.notificationErrorDescription') AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
)
); );
}
modalStateStrings: { [key in MachineStates]: string } = { const [selectedWash, setSelectedWash] = useState(
AsyncStorageManager.getString(
AsyncStorageManager.PREFERENCES.selectedWash.key
)
);
const modalStateStrings: { [key in MachineStates]: string } = {
[MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'), [MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'),
[MachineStates.RUNNING]: i18n.t('screens.proxiwash.modal.running'), [MachineStates.RUNNING]: i18n.t('screens.proxiwash.modal.running'),
[MachineStates.RUNNING_NOT_STARTED]: i18n.t( [MachineStates.RUNNING_NOT_STARTED]: i18n.t(
@ -126,95 +120,48 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
[MachineStates.UNKNOWN]: i18n.t('screens.proxiwash.modal.unknown'), [MachineStates.UNKNOWN]: i18n.t('screens.proxiwash.modal.unknown'),
}; };
modalRef: null | Modalize; const modalRef = useRef<Modalize>(null);
fetchedData: { useLayoutEffect(() => {
dryers: Array<ProxiwashMachineType>;
washers: Array<ProxiwashMachineType>;
};
/**
* Creates machine state parameters using current theme and translations
*/
constructor(props: PropsType) {
super(props);
this.modalRef = null;
this.fetchedData = { dryers: [], washers: [] };
this.state = {
modalCurrentDisplayItem: null,
machinesWatched: AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
),
selectedWash: AsyncStorageManager.getString(
AsyncStorageManager.PREFERENCES.selectedWash.key
),
};
}
/**
* Setup notification channel for android and add listeners to detect notifications fired
*/
componentDidMount() {
const { navigation } = this.props;
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
title="switch" title={'switch'}
iconName="swap-horizontal" iconName={'swap-horizontal'}
onPress={(): void => navigation.navigate('settings')} onPress={() => navigation.navigate('settings')}
/> />
<Item <Item
title="information" title={'information'}
iconName="information" iconName={'information'}
onPress={this.onAboutPress} onPress={() => navigation.navigate('proxiwash-about')}
/> />
</MaterialHeaderButtons> </MaterialHeaderButtons>
), ),
}); });
navigation.addListener('focus', this.onScreenFocus); }, [navigation]);
}
onScreenFocus = () => { useFocusEffect(
const { state } = this; useCallback(() => {
const selected = AsyncStorageManager.getString( const selected = AsyncStorageManager.getString(
AsyncStorageManager.PREFERENCES.selectedWash.key AsyncStorageManager.PREFERENCES.selectedWash.key
); );
if (selected !== state.selectedWash) { if (selected !== selectedWash) {
this.setState({ setSelectedWash(selected);
selectedWash: selected,
});
} }
}; }, [selectedWash])
);
/**
* Callback used when pressing the about button.
* This will open the ProxiwashAboutScreen.
*/
onAboutPress = () => {
const { navigation } = this.props;
navigation.navigate('proxiwash-about');
};
/** /**
* Callback used when the user clicks on enable notifications for a machine * Callback used when the user clicks on enable notifications for a machine
* *
* @param machine The machine to set notifications for * @param machine The machine to set notifications for
*/ */
onSetupNotificationsPress(machine: ProxiwashMachineType) { const onSetupNotificationsPress = (machine: ProxiwashMachineType) => {
if (this.modalRef) { if (modalRef.current) {
this.modalRef.close(); modalRef.current.close();
} }
this.setupNotifications(machine); setupNotifications(machine);
}
/**
* Callback used when receiving modal ref
*
* @param ref
*/
onModalRef = (ref: Modalize) => {
this.modalRef = ref;
}; };
/** /**
@ -226,11 +173,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param isDryer True if the given item is a dryer * @param isDryer True if the given item is a dryer
* @return {*} * @return {*}
*/ */
getModalContent(title: string, item: ProxiwashMachineType, isDryer: boolean) { const getModalContent = (
const { props, state } = this; title: string,
item: ProxiwashMachineType,
isDryer: boolean
) => {
let button: { text: string; icon: string; onPress: () => void } | undefined; let button: { text: string; icon: string; onPress: () => void } | undefined;
let message = this.modalStateStrings[item.state]; let message = modalStateStrings[item.state];
const onPress = () => this.onSetupNotificationsPress(item); const onPress = () => onSetupNotificationsPress(item);
if (item.state === MachineStates.RUNNING) { if (item.state === MachineStates.RUNNING) {
let remainingTime = parseInt(item.remainingTime, 10); let remainingTime = parseInt(item.remainingTime, 10);
if (remainingTime < 0) { if (remainingTime < 0) {
@ -238,7 +188,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
} }
button = { button = {
text: isMachineWatched(item, state.machinesWatched) text: isMachineWatched(item, machinesWatched)
? i18n.t('screens.proxiwash.modal.disableNotifications') ? i18n.t('screens.proxiwash.modal.disableNotifications')
: i18n.t('screens.proxiwash.modal.enableNotifications'), : i18n.t('screens.proxiwash.modal.enableNotifications'),
icon: '', icon: '',
@ -258,7 +208,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
left={() => ( left={() => (
<Avatar.Icon <Avatar.Icon
icon={isDryer ? 'tumble-dryer' : 'washing-machine'} icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
color={props.theme.colors.text} color={theme.colors.text}
style={styles.icon} style={styles.icon}
/> />
)} )}
@ -281,7 +231,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
) : null} ) : null}
</View> </View>
); );
} };
/** /**
* Gets the section render item * Gets the section render item
@ -289,13 +239,13 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param section The section to render * @param section The section to render
* @return {*} * @return {*}
*/ */
getRenderSectionHeader = ({ const getRenderSectionHeader = ({
section, section,
}: { }: {
section: SectionListData<ProxiwashMachineType>; section: SectionListData<ProxiwashMachineType>;
}) => { }) => {
const isDryer = section.title === i18n.t('screens.proxiwash.dryers'); const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
const nbAvailable = this.getMachineAvailableNumber(isDryer); const nbAvailable = getMachineAvailableNumber(section.data);
return ( return (
<ProxiwashSectionHeader <ProxiwashSectionHeader
title={section.title} title={section.title}
@ -312,13 +262,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param section The object describing the current SectionList section * @param section The object describing the current SectionList section
* @returns {React.Node} * @returns {React.Node}
*/ */
getRenderItem = (data: SectionListRenderItemInfo<ProxiwashMachineType>) => { const getRenderItem = (
const { machinesWatched } = this.state; data: SectionListRenderItemInfo<ProxiwashMachineType>
) => {
const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers'); const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers');
return ( return (
<ProxiwashListItem <ProxiwashListItem
item={data.item} item={data.item}
onPress={this.showModal} onPress={showModal}
isWatched={isMachineWatched(data.item, machinesWatched)} isWatched={isMachineWatched(data.item, machinesWatched)}
isDryer={isDryer} isDryer={isDryer}
height={LIST_ITEM_HEIGHT} height={LIST_ITEM_HEIGHT}
@ -332,7 +283,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param item The item to extract the key from * @param item The item to extract the key from
* @return {*} The extracted key * @return {*} The extracted key
*/ */
getKeyExtractor = (item: ProxiwashMachineType): string => item.number; const getKeyExtractor = (item: ProxiwashMachineType): string => item.number;
/** /**
* Setups notifications for the machine with the given ID. * Setups notifications for the machine with the given ID.
@ -341,28 +292,19 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* *
* @param machine The machine to watch * @param machine The machine to watch
*/ */
setupNotifications(machine: ProxiwashMachineType) { const setupNotifications = (machine: ProxiwashMachineType) => {
const { machinesWatched } = this.state;
if (!isMachineWatched(machine, machinesWatched)) { if (!isMachineWatched(machine, machinesWatched)) {
Notifications.setupMachineNotification( setupMachineNotification(
machine.number, machine.number,
true, true,
getMachineEndDate(machine) getMachineEndDate(machine)
)
.then(() => {
this.saveNotificationToState(machine);
})
.catch(() => {
ProxiwashScreen.showNotificationsDisabledWarning();
});
} else {
Notifications.setupMachineNotification(machine.number, false, null).then(
() => {
this.removeNotificationFromState(machine);
}
); );
saveNotificationToState(machine);
} else {
setupMachineNotification(machine.number, false);
removeNotificationFromState(machine);
} }
} };
/** /**
* Gets the number of machines available * Gets the number of machines available
@ -370,13 +312,9 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param isDryer True if we are only checking for dryer, false for washers * @param isDryer True if we are only checking for dryer, false for washers
* @return {number} The number of machines available * @return {number} The number of machines available
*/ */
getMachineAvailableNumber(isDryer: boolean): number { const getMachineAvailableNumber = (
let data; data: ReadonlyArray<ProxiwashMachineType>
if (isDryer) { ): number => {
data = this.fetchedData.dryers;
} else {
data = this.fetchedData.washers;
}
let count = 0; let count = 0;
data.forEach((machine: ProxiwashMachineType) => { data.forEach((machine: ProxiwashMachineType) => {
if (machine.state === MachineStates.AVAILABLE) { if (machine.state === MachineStates.AVAILABLE) {
@ -384,7 +322,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
} }
}); });
return count; return count;
} };
/** /**
* Creates the dataset to be used by the FlatList * Creates the dataset to be used by the FlatList
@ -392,10 +330,9 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param fetchedData * @param fetchedData
* @return {*} * @return {*}
*/ */
createDataset = ( const createDataset = (
fetchedData: FetchedDataType | undefined fetchedData: FetchedDataType | undefined
): SectionListDataType<ProxiwashMachineType> => { ): SectionListDataType<ProxiwashMachineType> => {
const { state } = this;
if (fetchedData) { if (fetchedData) {
let data = fetchedData; let data = fetchedData;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
@ -403,24 +340,26 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers); AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers); AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
} }
this.fetchedData = data; fetchedData = data;
// TODO dirty, should be refactored const cleanedList = getCleanedMachineWatched(machinesWatched, [
this.state.machinesWatched = getCleanedMachineWatched( ...data.dryers,
state.machinesWatched, ...data.washers,
[...data.dryers, ...data.washers] ]);
); if (cleanedList !== machinesWatched) {
setMachinesWatched(machinesWatched);
}
return [ return [
{ {
title: i18n.t('screens.proxiwash.dryers'), title: i18n.t('screens.proxiwash.dryers'),
icon: 'tumble-dryer', icon: 'tumble-dryer',
data: data.dryers === undefined ? [] : data.dryers, data: data.dryers === undefined ? [] : data.dryers,
keyExtractor: this.getKeyExtractor, keyExtractor: getKeyExtractor,
}, },
{ {
title: i18n.t('screens.proxiwash.washers'), title: i18n.t('screens.proxiwash.washers'),
icon: 'washing-machine', icon: 'washing-machine',
data: data.washers === undefined ? [] : data.washers, data: data.washers === undefined ? [] : data.washers,
keyExtractor: this.getKeyExtractor, keyExtractor: getKeyExtractor,
}, },
]; ];
} else { } else {
@ -428,15 +367,6 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
} }
}; };
/**
* Callback used when the user clicks on the navigate to settings button.
* This will hide the banner and open the SettingsScreen
*/
onGoToSettings = () => {
const { navigation } = this.props;
navigation.navigate('settings');
};
/** /**
* Shows a modal for the given item * Shows a modal for the given item
* *
@ -444,12 +374,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param item The item to display information for in the modal * @param item The item to display information for in the modal
* @param isDryer True if the given item is a dryer * @param isDryer True if the given item is a dryer
*/ */
showModal = (title: string, item: ProxiwashMachineType, isDryer: boolean) => { const showModal = (
this.setState({ title: string,
modalCurrentDisplayItem: this.getModalContent(title, item, isDryer), item: ProxiwashMachineType,
}); isDryer: boolean
if (this.modalRef) { ) => {
this.modalRef.open(); setModalCurrentDisplayItem(getModalContent(title, item, isDryer));
if (modalRef.current) {
modalRef.current.open();
} }
}; };
@ -458,44 +390,36 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* *
* @param machine * @param machine
*/ */
saveNotificationToState(machine: ProxiwashMachineType) { const saveNotificationToState = (machine: ProxiwashMachineType) => {
const { machinesWatched } = this.state; let data = [...machinesWatched];
const data = machinesWatched;
data.push(machine); data.push(machine);
this.saveNewWatchedList(data); saveNewWatchedList(data);
} };
/** /**
* Removes the given index from the watchlist array and saves it to preferences * Removes the given index from the watchlist array and saves it to preferences
* *
* @param selectedMachine * @param selectedMachine
*/ */
removeNotificationFromState(selectedMachine: ProxiwashMachineType) { const removeNotificationFromState = (
const { machinesWatched } = this.state; selectedMachine: ProxiwashMachineType
const newList = [...machinesWatched]; ) => {
machinesWatched.forEach((machine: ProxiwashMachineType, index: number) => { const newList = machinesWatched.filter(
if ( (m) => m.number !== selectedMachine.number
machine.number === selectedMachine.number && );
machine.endTime === selectedMachine.endTime saveNewWatchedList(newList);
) { };
newList.splice(index, 1);
}
});
this.saveNewWatchedList(newList);
}
saveNewWatchedList(list: Array<ProxiwashMachineType>) { const saveNewWatchedList = (list: Array<ProxiwashMachineType>) => {
this.setState({ machinesWatched: list }); setMachinesWatched(list);
AsyncStorageManager.set( AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key, AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
list list
); );
} };
render() {
const { state } = this;
let data: LaundromatType; let data: LaundromatType;
switch (state.selectedWash) { switch (selectedWash) {
case 'tripodeB': case 'tripodeB':
data = ProxiwashConstants.tripodeB; data = ProxiwashConstants.tripodeB;
break; break;
@ -507,12 +431,12 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
<View style={styles.container}> <View style={styles.container}>
<WebSectionList <WebSectionList
request={() => readData<FetchedDataType>(data.url)} request={() => readData<FetchedDataType>(data.url)}
createDataset={this.createDataset} createDataset={createDataset}
renderItem={this.getRenderItem} renderItem={getRenderItem}
renderSectionHeader={this.getRenderSectionHeader} renderSectionHeader={getRenderSectionHeader}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus={true} refreshOnFocus={true}
extraData={state.machinesWatched.length} extraData={machinesWatched.length}
/> />
</View> </View>
<MascotPopup <MascotPopup
@ -524,7 +448,7 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
action: { action: {
message: i18n.t('screens.proxiwash.mascotDialog.ok'), message: i18n.t('screens.proxiwash.mascotDialog.ok'),
icon: 'cog', icon: 'cog',
onPress: this.onGoToSettings, onPress: () => navigation.navigate('settings'),
}, },
cancel: { cancel: {
message: i18n.t('screens.proxiwash.mascotDialog.cancel'), message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
@ -533,12 +457,9 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
}} }}
emotion={MASCOT_STYLE.NORMAL} emotion={MASCOT_STYLE.NORMAL}
/> />
<CustomModal onRef={this.onModalRef}> <CustomModal ref={modalRef}>{modalCurrentDisplayItem}</CustomModal>
{state.modalCurrentDisplayItem}
</CustomModal>
</View> </View>
); );
}
} }
export default withTheme(ProxiwashScreen); export default ProxiwashScreen;

View file

@ -17,44 +17,59 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {
checkNotifications,
requestNotifications,
RESULTS,
} from 'react-native-permissions';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AsyncStorageManager from '../managers/AsyncStorageManager'; import AsyncStorageManager from '../managers/AsyncStorageManager';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
const PushNotification = require('react-native-push-notification'); import PushNotification from 'react-native-push-notification';
import { Platform } from 'react-native';
// Used to multiply the normal notification id to create the reminder one. It allows to find it back easily // Used to multiply the normal notification id to create the reminder one. It allows to find it back easily
const reminderIdFactor = 100; const reminderIdFactor = 100;
/** PushNotification.createChannel(
* Async function asking permission to send notifications to the user. {
* Used on ios. channelId: 'reminders', // (required)
* channelName: 'Reminders', // (required)
* @returns {Promise<void>} channelDescription: 'Get laundry reminders', // (optional) default: undefined.
playSound: true, // (optional) default: true
soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function
importance: 4, // (optional) default: 4. Int value of the Android notification importance
vibrate: true, // (optional) default: true. Creates the default vibration patten if true.
},
(created) => console.log(`createChannel returned '${created}'`) // (optional) callback returns whether the channel was created, false means it already existed.
);
PushNotification.configure({
// (required) Called when a remote is received or opened, or local notification is opened
onNotification: function (notification) {
console.log('NOTIFICATION:', notification);
// process the notification
// (required) Called when a remote is received or opened, or local notification is opened
notification.finish(PushNotificationIOS.FetchResult.NoData);
},
// IOS ONLY (optional): default: all - Permissions to register.
permissions: {
alert: true,
badge: true,
sound: true,
},
// Should the initial notification be popped automatically
// default: true
popInitialNotification: true,
/**
* (optional) default: true
* - Specified if permissions (ios) and token (android and ios) will requested or not,
* - if not, you must call PushNotificationsHandler.requestPermissions() later
* - if you are not using remote notification or do not have Firebase installed, use this:
* requestPermissions: Platform.OS === 'ios'
*/ */
export async function askPermissions(): Promise<void> { requestPermissions: Platform.OS === 'ios',
return new Promise((resolve: () => void, reject: () => void) => { });
checkNotifications().then(({ status }: { status: string }) => {
if (status === RESULTS.GRANTED) {
resolve();
} else if (status === RESULTS.BLOCKED) {
reject();
} else {
requestNotifications([]).then((result: { status: string }) => {
if (result.status === RESULTS.GRANTED) {
resolve();
} else {
reject();
}
});
}
});
});
}
/** /**
* Creates a notification for the given machine id at the given date. * Creates a notification for the given machine id at the given date.
@ -79,7 +94,7 @@ function createNotifications(machineID: string, date: Date) {
message: i18n.t('screens.proxiwash.notifications.machineRunningBody', { message: i18n.t('screens.proxiwash.notifications.machineRunningBody', {
number: machineID, number: machineID,
}), }),
id: id.toString(), id: id,
date: reminderDate, date: reminderDate,
}); });
} }
@ -89,7 +104,7 @@ function createNotifications(machineID: string, date: Date) {
message: i18n.t('screens.proxiwash.notifications.machineFinishedBody', { message: i18n.t('screens.proxiwash.notifications.machineFinishedBody', {
number: machineID, number: machineID,
}), }),
id: machineID, id: parseInt(machineID, 10),
date, date,
}); });
} }
@ -104,26 +119,16 @@ function createNotifications(machineID: string, date: Date) {
* @param isEnabled True to enable notifications, false to disable * @param isEnabled True to enable notifications, false to disable
* @param endDate The trigger date, or null if disabling notifications * @param endDate The trigger date, or null if disabling notifications
*/ */
export async function setupMachineNotification( export function setupMachineNotification(
machineID: string, machineID: string,
isEnabled: boolean, isEnabled: boolean,
endDate: Date | null endDate?: Date | null
): Promise<void> { ) {
return new Promise((resolve: () => void, reject: () => void) => { if (isEnabled && endDate) {
if (isEnabled && endDate != null) {
askPermissions()
.then(() => {
createNotifications(machineID, endDate); createNotifications(machineID, endDate);
resolve();
})
.catch(() => {
reject();
});
} else { } else {
PushNotification.cancelLocalNotifications({ id: machineID }); PushNotification.cancelLocalNotifications({ id: machineID });
const reminderId = reminderIdFactor * parseInt(machineID, 10); const reminderId = reminderIdFactor * parseInt(machineID, 10);
PushNotification.cancelLocalNotifications({ id: reminderId.toString() }); PushNotification.cancelLocalNotifications({ id: reminderId.toString() });
resolve();
} }
});
} }