Application Android et IOS pour l'amicale des élèves
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProxiwashScreen.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // @flow
  2. import * as React from 'react';
  3. import {Alert, View} from 'react-native';
  4. import i18n from 'i18n-js';
  5. import {Avatar, Button, Card, Text, withTheme} from 'react-native-paper';
  6. import {StackNavigationProp} from '@react-navigation/stack';
  7. import {Modalize} from 'react-native-modalize';
  8. import WebSectionList from '../../components/Screens/WebSectionList';
  9. import * as Notifications from '../../utils/Notifications';
  10. import AsyncStorageManager from '../../managers/AsyncStorageManager';
  11. import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem';
  12. import ProxiwashConstants from '../../constants/ProxiwashConstants';
  13. import CustomModal from '../../components/Overrides/CustomModal';
  14. import AprilFoolsManager from '../../managers/AprilFoolsManager';
  15. import MaterialHeaderButtons, {
  16. Item,
  17. } from '../../components/Overrides/CustomHeaderButton';
  18. import ProxiwashSectionHeader from '../../components/Lists/Proxiwash/ProxiwashSectionHeader';
  19. import type {CustomThemeType} from '../../managers/ThemeManager';
  20. import {
  21. getCleanedMachineWatched,
  22. getMachineEndDate,
  23. isMachineWatched,
  24. } from '../../utils/Proxiwash';
  25. import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
  26. import MascotPopup from '../../components/Mascot/MascotPopup';
  27. import type {SectionListDataType} from '../../components/Screens/WebSectionList';
  28. const DATA_URL =
  29. 'https://etud.insa-toulouse.fr/~amicale_app/v2/washinsa/washinsa_data.json';
  30. const modalStateStrings = {};
  31. const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
  32. const LIST_ITEM_HEIGHT = 64;
  33. export type ProxiwashMachineType = {
  34. number: string,
  35. state: string,
  36. startTime: string,
  37. endTime: string,
  38. donePercent: string,
  39. remainingTime: string,
  40. program: string,
  41. };
  42. type PropsType = {
  43. navigation: StackNavigationProp,
  44. theme: CustomThemeType,
  45. };
  46. type StateType = {
  47. modalCurrentDisplayItem: React.Node,
  48. machinesWatched: Array<ProxiwashMachineType>,
  49. };
  50. /**
  51. * Class defining the app's proxiwash screen. This screen shows information about washing machines and
  52. * dryers, taken from a scrapper reading proxiwash website
  53. */
  54. class ProxiwashScreen extends React.Component<PropsType, StateType> {
  55. /**
  56. * Shows a warning telling the user notifications are disabled for the app
  57. */
  58. static showNotificationsDisabledWarning() {
  59. Alert.alert(
  60. i18n.t('screens.proxiwash.modal.notificationErrorTitle'),
  61. i18n.t('screens.proxiwash.modal.notificationErrorDescription'),
  62. );
  63. }
  64. modalRef: null | Modalize;
  65. fetchedData: {
  66. dryers: Array<ProxiwashMachineType>,
  67. washers: Array<ProxiwashMachineType>,
  68. };
  69. /**
  70. * Creates machine state parameters using current theme and translations
  71. */
  72. constructor() {
  73. super();
  74. this.state = {
  75. modalCurrentDisplayItem: null,
  76. machinesWatched: AsyncStorageManager.getObject(
  77. AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
  78. ),
  79. };
  80. modalStateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t(
  81. 'screens.proxiwash.modal.ready',
  82. );
  83. modalStateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t(
  84. 'screens.proxiwash.modal.running',
  85. );
  86. modalStateStrings[
  87. ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
  88. ] = i18n.t('screens.proxiwash.modal.runningNotStarted');
  89. modalStateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
  90. 'screens.proxiwash.modal.finished',
  91. );
  92. modalStateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
  93. 'screens.proxiwash.modal.broken',
  94. );
  95. modalStateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
  96. 'screens.proxiwash.modal.error',
  97. );
  98. modalStateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
  99. 'screens.proxiwash.modal.unknown',
  100. );
  101. }
  102. /**
  103. * Setup notification channel for android and add listeners to detect notifications fired
  104. */
  105. componentDidMount() {
  106. const {navigation} = this.props;
  107. navigation.setOptions({
  108. headerRight: (): React.Node => (
  109. <MaterialHeaderButtons>
  110. <Item
  111. title="information"
  112. iconName="information"
  113. onPress={this.onAboutPress}
  114. />
  115. </MaterialHeaderButtons>
  116. ),
  117. });
  118. }
  119. /**
  120. * Callback used when pressing the about button.
  121. * This will open the ProxiwashAboutScreen.
  122. */
  123. onAboutPress = () => {
  124. const {navigation} = this.props;
  125. navigation.navigate('proxiwash-about');
  126. };
  127. /**
  128. * Callback used when the user clicks on enable notifications for a machine
  129. *
  130. * @param machine The machine to set notifications for
  131. */
  132. onSetupNotificationsPress(machine: ProxiwashMachineType) {
  133. if (this.modalRef) {
  134. this.modalRef.close();
  135. }
  136. this.setupNotifications(machine);
  137. }
  138. /**
  139. * Callback used when receiving modal ref
  140. *
  141. * @param ref
  142. */
  143. onModalRef = (ref: Modalize) => {
  144. this.modalRef = ref;
  145. };
  146. /**
  147. * Generates the modal content.
  148. * This shows information for the given machine.
  149. *
  150. * @param title The title to use
  151. * @param item The item to display information for in the modal
  152. * @param isDryer True if the given item is a dryer
  153. * @return {*}
  154. */
  155. getModalContent(
  156. title: string,
  157. item: ProxiwashMachineType,
  158. isDryer: boolean,
  159. ): React.Node {
  160. const {props, state} = this;
  161. let button = {
  162. text: i18n.t('screens.proxiwash.modal.ok'),
  163. icon: '',
  164. onPress: undefined,
  165. };
  166. let message = modalStateStrings[item.state];
  167. const onPress = this.onSetupNotificationsPress.bind(this, item);
  168. if (item.state === ProxiwashConstants.machineStates.RUNNING) {
  169. let remainingTime = parseInt(item.remainingTime, 10);
  170. if (remainingTime < 0) remainingTime = 0;
  171. button = {
  172. text: isMachineWatched(item, state.machinesWatched)
  173. ? i18n.t('screens.proxiwash.modal.disableNotifications')
  174. : i18n.t('screens.proxiwash.modal.enableNotifications'),
  175. icon: '',
  176. onPress,
  177. };
  178. message = i18n.t('screens.proxiwash.modal.running', {
  179. start: item.startTime,
  180. end: item.endTime,
  181. remaining: remainingTime,
  182. program: item.program,
  183. });
  184. } else if (item.state === ProxiwashConstants.machineStates.AVAILABLE) {
  185. if (isDryer) message += `\n${i18n.t('screens.proxiwash.dryersTariff')}`;
  186. else message += `\n${i18n.t('screens.proxiwash.washersTariff')}`;
  187. }
  188. return (
  189. <View
  190. style={{
  191. flex: 1,
  192. padding: 20,
  193. }}>
  194. <Card.Title
  195. title={title}
  196. left={(): React.Node => (
  197. <Avatar.Icon
  198. icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
  199. color={props.theme.colors.text}
  200. style={{backgroundColor: 'transparent'}}
  201. />
  202. )}
  203. />
  204. <Card.Content>
  205. <Text>{message}</Text>
  206. </Card.Content>
  207. {button.onPress !== undefined ? (
  208. <Card.Actions>
  209. <Button
  210. icon={button.icon}
  211. mode="contained"
  212. onPress={button.onPress}
  213. style={{marginLeft: 'auto', marginRight: 'auto'}}>
  214. {button.text}
  215. </Button>
  216. </Card.Actions>
  217. ) : null}
  218. </View>
  219. );
  220. }
  221. /**
  222. * Gets the section render item
  223. *
  224. * @param section The section to render
  225. * @return {*}
  226. */
  227. getRenderSectionHeader = ({
  228. section,
  229. }: {
  230. section: {title: string},
  231. }): React.Node => {
  232. const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
  233. const nbAvailable = this.getMachineAvailableNumber(isDryer);
  234. return (
  235. <ProxiwashSectionHeader
  236. title={section.title}
  237. nbAvailable={nbAvailable}
  238. isDryer={isDryer}
  239. />
  240. );
  241. };
  242. /**
  243. * Gets the list item to be rendered
  244. *
  245. * @param item The object containing the item's FetchedData
  246. * @param section The object describing the current SectionList section
  247. * @returns {React.Node}
  248. */
  249. getRenderItem = ({
  250. item,
  251. section,
  252. }: {
  253. item: ProxiwashMachineType,
  254. section: {title: string},
  255. }): React.Node => {
  256. const {machinesWatched} = this.state;
  257. const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
  258. return (
  259. <ProxiwashListItem
  260. item={item}
  261. onPress={this.showModal}
  262. isWatched={isMachineWatched(item, machinesWatched)}
  263. isDryer={isDryer}
  264. height={LIST_ITEM_HEIGHT}
  265. />
  266. );
  267. };
  268. /**
  269. * Extracts the key for the given item
  270. *
  271. * @param item The item to extract the key from
  272. * @return {*} The extracted key
  273. */
  274. getKeyExtractor = (item: ProxiwashMachineType): string => item.number;
  275. /**
  276. * Setups notifications for the machine with the given ID.
  277. * One notification will be sent at the end of the program.
  278. * Another will be send a few minutes before the end, based on the value of reminderNotifTime
  279. *
  280. * @param machine The machine to watch
  281. */
  282. setupNotifications(machine: ProxiwashMachineType) {
  283. const {machinesWatched} = this.state;
  284. if (!isMachineWatched(machine, machinesWatched)) {
  285. Notifications.setupMachineNotification(
  286. machine.number,
  287. true,
  288. getMachineEndDate(machine),
  289. )
  290. .then(() => {
  291. this.saveNotificationToState(machine);
  292. })
  293. .catch(() => {
  294. ProxiwashScreen.showNotificationsDisabledWarning();
  295. });
  296. } else {
  297. Notifications.setupMachineNotification(machine.number, false, null).then(
  298. () => {
  299. this.removeNotificationFromState(machine);
  300. },
  301. );
  302. }
  303. }
  304. /**
  305. * Gets the number of machines available
  306. *
  307. * @param isDryer True if we are only checking for dryer, false for washers
  308. * @return {number} The number of machines available
  309. */
  310. getMachineAvailableNumber(isDryer: boolean): number {
  311. let data;
  312. if (isDryer) data = this.fetchedData.dryers;
  313. else data = this.fetchedData.washers;
  314. let count = 0;
  315. data.forEach((machine: ProxiwashMachineType) => {
  316. if (machine.state === ProxiwashConstants.machineStates.AVAILABLE)
  317. count += 1;
  318. });
  319. return count;
  320. }
  321. /**
  322. * Creates the dataset to be used by the FlatList
  323. *
  324. * @param fetchedData
  325. * @return {*}
  326. */
  327. createDataset = (fetchedData: {
  328. dryers: Array<ProxiwashMachineType>,
  329. washers: Array<ProxiwashMachineType>,
  330. }): SectionListDataType<ProxiwashMachineType> => {
  331. const {state} = this;
  332. let data = fetchedData;
  333. if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
  334. data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
  335. AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
  336. AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
  337. }
  338. this.fetchedData = data;
  339. this.state.machinesWatched = getCleanedMachineWatched(
  340. state.machinesWatched,
  341. [...data.dryers, ...data.washers],
  342. );
  343. return [
  344. {
  345. title: i18n.t('screens.proxiwash.dryers'),
  346. icon: 'tumble-dryer',
  347. data: data.dryers === undefined ? [] : data.dryers,
  348. keyExtractor: this.getKeyExtractor,
  349. },
  350. {
  351. title: i18n.t('screens.proxiwash.washers'),
  352. icon: 'washing-machine',
  353. data: data.washers === undefined ? [] : data.washers,
  354. keyExtractor: this.getKeyExtractor,
  355. },
  356. ];
  357. };
  358. /**
  359. * Shows a modal for the given item
  360. *
  361. * @param title The title to use
  362. * @param item The item to display information for in the modal
  363. * @param isDryer True if the given item is a dryer
  364. */
  365. showModal = (title: string, item: ProxiwashMachineType, isDryer: boolean) => {
  366. this.setState({
  367. modalCurrentDisplayItem: this.getModalContent(title, item, isDryer),
  368. });
  369. if (this.modalRef) {
  370. this.modalRef.open();
  371. }
  372. };
  373. /**
  374. * Adds the given notifications associated to a machine ID to the watchlist, and saves the array to the preferences
  375. *
  376. * @param machine
  377. */
  378. saveNotificationToState(machine: ProxiwashMachineType) {
  379. const {machinesWatched} = this.state;
  380. const data = machinesWatched;
  381. data.push(machine);
  382. this.saveNewWatchedList(data);
  383. }
  384. /**
  385. * Removes the given index from the watchlist array and saves it to preferences
  386. *
  387. * @param selectedMachine
  388. */
  389. removeNotificationFromState(selectedMachine: ProxiwashMachineType) {
  390. const {machinesWatched} = this.state;
  391. const newList = [...machinesWatched];
  392. machinesWatched.forEach((machine: ProxiwashMachineType, index: number) => {
  393. if (
  394. machine.number === selectedMachine.number &&
  395. machine.endTime === selectedMachine.endTime
  396. )
  397. newList.splice(index, 1);
  398. });
  399. this.saveNewWatchedList(newList);
  400. }
  401. saveNewWatchedList(list: Array<ProxiwashMachineType>) {
  402. this.setState({machinesWatched: list});
  403. AsyncStorageManager.set(
  404. AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
  405. list,
  406. );
  407. }
  408. render(): React.Node {
  409. const {state} = this;
  410. const {navigation} = this.props;
  411. return (
  412. <View style={{flex: 1}}>
  413. <View
  414. style={{
  415. position: 'absolute',
  416. width: '100%',
  417. height: '100%',
  418. }}>
  419. <WebSectionList
  420. createDataset={this.createDataset}
  421. navigation={navigation}
  422. fetchUrl={DATA_URL}
  423. renderItem={this.getRenderItem}
  424. renderSectionHeader={this.getRenderSectionHeader}
  425. autoRefreshTime={REFRESH_TIME}
  426. refreshOnFocus
  427. updateData={state.machinesWatched.length}
  428. />
  429. </View>
  430. <MascotPopup
  431. prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowBanner.key}
  432. title={i18n.t('screens.proxiwash.mascotDialog.title')}
  433. message={i18n.t('screens.proxiwash.mascotDialog.message')}
  434. icon="information"
  435. buttons={{
  436. action: null,
  437. cancel: {
  438. message: i18n.t('screens.proxiwash.mascotDialog.ok'),
  439. icon: 'check',
  440. },
  441. }}
  442. emotion={MASCOT_STYLE.NORMAL}
  443. />
  444. <CustomModal onRef={this.onModalRef}>
  445. {state.modalCurrentDisplayItem}
  446. </CustomModal>
  447. </View>
  448. );
  449. }
  450. }
  451. export default withTheme(ProxiwashScreen);