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 15KB

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