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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. // @flow
  2. import * as React from 'react';
  3. import {Alert, Platform, View} from 'react-native';
  4. import i18n from "i18n-js";
  5. import WebSectionList from "../../components/Lists/WebSectionList";
  6. import * as Notifications from "../../utils/Notifications";
  7. import AsyncStorageManager from "../../managers/AsyncStorageManager";
  8. import * as Expo from "expo";
  9. import {Avatar, Banner, Button, Card, Text, withTheme} from 'react-native-paper';
  10. import ProxiwashListItem from "../../components/Lists/ProxiwashListItem";
  11. import ProxiwashConstants from "../../constants/ProxiwashConstants";
  12. import CustomModal from "../../components/Custom/CustomModal";
  13. import AprilFoolsManager from "../../managers/AprilFoolsManager";
  14. import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton";
  15. const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
  16. let stateStrings = {};
  17. let modalStateStrings = {};
  18. let stateIcons = {};
  19. const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
  20. const LIST_ITEM_HEIGHT = 64;
  21. type Props = {
  22. navigation: Object,
  23. theme: Object,
  24. }
  25. type State = {
  26. refreshing: boolean,
  27. firstLoading: boolean,
  28. modalCurrentDisplayItem: React.Node,
  29. machinesWatched: Array<string>,
  30. bannerVisible: boolean,
  31. };
  32. /**
  33. * Class defining the app's proxiwash screen. This screen shows information about washing machines and
  34. * dryers, taken from a scrapper reading proxiwash website
  35. */
  36. class ProxiwashScreen extends React.Component<Props, State> {
  37. modalRef: Object;
  38. onAboutPress: Function;
  39. getRenderItem: Function;
  40. getRenderSectionHeader: Function;
  41. createDataset: Function;
  42. onHideBanner: Function;
  43. onModalRef: Function;
  44. fetchedData: Object;
  45. colors: Object;
  46. state = {
  47. refreshing: false,
  48. firstLoading: true,
  49. fetchedData: {},
  50. machinesWatched: [],
  51. modalCurrentDisplayItem: null,
  52. bannerVisible: AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.current === '1',
  53. };
  54. /**
  55. * Creates machine state parameters using current theme and translations
  56. */
  57. constructor(props) {
  58. super(props);
  59. stateStrings[ProxiwashConstants.machineStates.TERMINE] = i18n.t('proxiwashScreen.states.finished');
  60. stateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.states.ready');
  61. stateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.states.running');
  62. stateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.states.broken');
  63. stateStrings[ProxiwashConstants.machineStates.ERREUR] = i18n.t('proxiwashScreen.states.error');
  64. modalStateStrings[ProxiwashConstants.machineStates.TERMINE] = i18n.t('proxiwashScreen.modal.finished');
  65. modalStateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.modal.ready');
  66. modalStateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.modal.running');
  67. modalStateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.modal.broken');
  68. modalStateStrings[ProxiwashConstants.machineStates.ERREUR] = i18n.t('proxiwashScreen.modal.error');
  69. stateIcons[ProxiwashConstants.machineStates.TERMINE] = 'check-circle';
  70. stateIcons[ProxiwashConstants.machineStates.DISPONIBLE] = 'radiobox-blank';
  71. stateIcons[ProxiwashConstants.machineStates["EN COURS"]] = 'progress-check';
  72. stateIcons[ProxiwashConstants.machineStates.HS] = 'alert-octagram-outline';
  73. stateIcons[ProxiwashConstants.machineStates.ERREUR] = 'alert';
  74. // let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current;
  75. this.onAboutPress = this.onAboutPress.bind(this);
  76. this.getRenderItem = this.getRenderItem.bind(this);
  77. this.getRenderSectionHeader = this.getRenderSectionHeader.bind(this);
  78. this.createDataset = this.createDataset.bind(this);
  79. this.onHideBanner = this.onHideBanner.bind(this);
  80. this.onModalRef = this.onModalRef.bind(this);
  81. this.colors = props.theme.colors;
  82. }
  83. /**
  84. * Callback used when closing the banner.
  85. * This hides the banner and saves to preferences to prevent it from reopening
  86. */
  87. onHideBanner() {
  88. this.setState({bannerVisible: false});
  89. AsyncStorageManager.getInstance().savePref(
  90. AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.key,
  91. '0'
  92. );
  93. }
  94. /**
  95. * Setup notification channel for android and add listeners to detect notifications fired
  96. */
  97. componentDidMount() {
  98. const rightButton = this.getAboutButton.bind(this);
  99. this.props.navigation.setOptions({
  100. headerRight: rightButton,
  101. });
  102. if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
  103. // Get latest watchlist from server
  104. Notifications.getMachineNotificationWatchlist((fetchedList) => {
  105. this.setState({machinesWatched: fetchedList})
  106. });
  107. // Get updated watchlist after received notification
  108. Expo.Notifications.addListener(() => {
  109. Notifications.getMachineNotificationWatchlist((fetchedList) => {
  110. this.setState({machinesWatched: fetchedList})
  111. });
  112. });
  113. if (Platform.OS === 'android') {
  114. Expo.Notifications.createChannelAndroidAsync('reminders', {
  115. name: 'Reminders',
  116. priority: 'max',
  117. vibrate: [0, 250, 250, 250],
  118. });
  119. }
  120. }
  121. }
  122. /**
  123. * Callback used when pressing the about button.
  124. * This will open the ProxiwashAboutScreen.
  125. */
  126. onAboutPress() {
  127. this.props.navigation.navigate('proxiwash-about');
  128. }
  129. /**
  130. * Gets the about header button
  131. *
  132. * @return {*}
  133. */
  134. getAboutButton() {
  135. return <MaterialHeaderButtons>
  136. <Item title="information" iconName="information" onPress={this.onAboutPress}/>
  137. </MaterialHeaderButtons>;
  138. }
  139. /**
  140. * Extracts the key for the given item
  141. *
  142. * @param item The item to extract the key from
  143. * @return {*} The extracted key
  144. */
  145. getKeyExtractor(item: Object) {
  146. return item !== undefined ? item.number : undefined;
  147. }
  148. /**
  149. * Setups notifications for the machine with the given ID.
  150. * One notification will be sent at the end of the program.
  151. * Another will be send a few minutes before the end, based on the value of reminderNotifTime
  152. *
  153. * @param machineId The machine's ID
  154. * @returns {Promise<void>}
  155. */
  156. setupNotifications(machineId: string) {
  157. if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
  158. if (!this.isMachineWatched(machineId)) {
  159. Notifications.setupMachineNotification(machineId, true);
  160. this.saveNotificationToState(machineId);
  161. } else
  162. this.disableNotification(machineId);
  163. } else {
  164. this.showNotificationsDisabledWarning();
  165. }
  166. }
  167. /**
  168. * Shows a warning telling the user notifications are disabled for the app
  169. */
  170. showNotificationsDisabledWarning() {
  171. Alert.alert(
  172. i18n.t("proxiwashScreen.modal.notificationErrorTitle"),
  173. i18n.t("proxiwashScreen.modal.notificationErrorDescription"),
  174. );
  175. }
  176. /**
  177. * Stops scheduled notifications for the machine of the given ID.
  178. * This will also remove the notification if it was already shown.
  179. *
  180. * @param machineId The machine's ID
  181. */
  182. disableNotification(machineId: string) {
  183. let data = this.state.machinesWatched;
  184. if (data.length > 0) {
  185. let arrayIndex = data.indexOf(machineId);
  186. if (arrayIndex !== -1) {
  187. Notifications.setupMachineNotification(machineId, false);
  188. this.removeNotificationFroState(arrayIndex);
  189. }
  190. }
  191. }
  192. /**
  193. * Adds the given notifications associated to a machine ID to the watchlist, and saves the array to the preferences
  194. *
  195. * @param machineId
  196. */
  197. saveNotificationToState(machineId: string) {
  198. let data = this.state.machinesWatched;
  199. data.push(machineId);
  200. this.updateNotificationState(data);
  201. }
  202. /**
  203. * Removes the given index from the watchlist array and saves it to preferences
  204. *
  205. * @param index
  206. */
  207. removeNotificationFroState(index: number) {
  208. let data = this.state.machinesWatched;
  209. data.splice(index, 1);
  210. this.updateNotificationState(data);
  211. }
  212. /**
  213. * Sets the given fetchedData as the watchlist
  214. *
  215. * @param data
  216. */
  217. updateNotificationState(data: Array<Object>) {
  218. this.setState({machinesWatched: data});
  219. }
  220. /**
  221. * Checks whether the machine of the given ID has scheduled notifications
  222. *
  223. * @param machineID The machine's ID
  224. * @returns {boolean}
  225. */
  226. isMachineWatched(machineID: string) {
  227. return this.state.machinesWatched.indexOf(machineID) !== -1;
  228. }
  229. /**
  230. * Creates the dataset to be used by the flatlist
  231. *
  232. * @param fetchedData
  233. * @return {*}
  234. */
  235. createDataset(fetchedData: Object) {
  236. let data = fetchedData;
  237. if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
  238. data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
  239. AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
  240. AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
  241. }
  242. this.fetchedData = fetchedData;
  243. return [
  244. {
  245. title: i18n.t('proxiwashScreen.dryers'),
  246. icon: 'tumble-dryer',
  247. data: data.dryers === undefined ? [] : data.dryers,
  248. keyExtractor: this.getKeyExtractor
  249. },
  250. {
  251. title: i18n.t('proxiwashScreen.washers'),
  252. icon: 'washing-machine',
  253. data: data.washers === undefined ? [] : data.washers,
  254. keyExtractor: this.getKeyExtractor
  255. },
  256. ];
  257. }
  258. /**
  259. * Shows a modal for the given item
  260. *
  261. * @param title The title to use
  262. * @param item The item to display information for in the modal
  263. * @param isDryer True if the given item is a dryer
  264. */
  265. showModal(title: string, item: Object, isDryer: boolean) {
  266. this.setState({
  267. modalCurrentDisplayItem: this.getModalContent(title, item, isDryer)
  268. });
  269. if (this.modalRef) {
  270. this.modalRef.open();
  271. }
  272. }
  273. /**
  274. * Callback used when the user clicks on enable notifications for a machine
  275. *
  276. * @param machineId The machine's id to set notifications for
  277. */
  278. onSetupNotificationsPress(machineId: string) {
  279. if (this.modalRef) {
  280. this.modalRef.close();
  281. }
  282. this.setupNotifications(machineId)
  283. }
  284. /**
  285. * Generates the modal content.
  286. * This shows information for the given machine.
  287. *
  288. * @param title The title to use
  289. * @param item The item to display information for in the modal
  290. * @param isDryer True if the given item is a dryer
  291. * @return {*}
  292. */
  293. getModalContent(title: string, item: Object, isDryer: boolean) {
  294. let button = {
  295. text: i18n.t("proxiwashScreen.modal.ok"),
  296. icon: '',
  297. onPress: undefined
  298. };
  299. let message = modalStateStrings[ProxiwashConstants.machineStates[item.state]];
  300. const onPress = this.onSetupNotificationsPress.bind(this, item.number);
  301. if (ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates["EN COURS"]) {
  302. button =
  303. {
  304. text: this.isMachineWatched(item.number) ?
  305. i18n.t("proxiwashScreen.modal.disableNotifications") :
  306. i18n.t("proxiwashScreen.modal.enableNotifications"),
  307. icon: '',
  308. onPress: onPress
  309. }
  310. ;
  311. message = i18n.t('proxiwashScreen.modal.running',
  312. {
  313. start: item.startTime,
  314. end: item.endTime,
  315. remaining: item.remainingTime
  316. });
  317. } else if (ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates.DISPONIBLE) {
  318. if (isDryer)
  319. message += '\n' + i18n.t('proxiwashScreen.dryersTariff');
  320. else
  321. message += '\n' + i18n.t('proxiwashScreen.washersTariff');
  322. }
  323. return (
  324. <View style={{
  325. flex: 1,
  326. padding: 20
  327. }}>
  328. <Card.Title
  329. title={title}
  330. left={() => <Avatar.Icon
  331. icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
  332. color={this.colors.text}
  333. style={{backgroundColor: 'transparent'}}/>}
  334. />
  335. <Card.Content>
  336. <Text>{message}</Text>
  337. </Card.Content>
  338. {button.onPress !== undefined ?
  339. <Card.Actions>
  340. <Button
  341. icon={button.icon}
  342. mode="contained"
  343. onPress={button.onPress}
  344. style={{marginLeft: 'auto', marginRight: 'auto'}}
  345. >
  346. {button.text}
  347. </Button>
  348. </Card.Actions> : null}
  349. </View>
  350. );
  351. }
  352. /**
  353. * Callback used when receiving modal ref
  354. *
  355. * @param ref
  356. */
  357. onModalRef(ref: Object) {
  358. this.modalRef = ref;
  359. }
  360. /**
  361. * Gets the number of machines available
  362. *
  363. * @param isDryer True if we are only checking for dryer, false for washers
  364. * @return {number} The number of machines available
  365. */
  366. getMachineAvailableNumber(isDryer: boolean) {
  367. let data;
  368. if (isDryer)
  369. data = this.fetchedData.dryers;
  370. else
  371. data = this.fetchedData.washers;
  372. let count = 0;
  373. for (let i = 0; i < data.length; i++) {
  374. if (ProxiwashConstants.machineStates[data[i].state] === ProxiwashConstants.machineStates["DISPONIBLE"])
  375. count += 1;
  376. }
  377. return count;
  378. }
  379. /**
  380. * Gets the section render item
  381. *
  382. * @param section The section to render
  383. * @return {*}
  384. */
  385. getRenderSectionHeader({section}: Object) {
  386. const isDryer = section.title === i18n.t('proxiwashScreen.dryers');
  387. const nbAvailable = this.getMachineAvailableNumber(isDryer);
  388. const subtitle = nbAvailable + ' ' + ((nbAvailable <= 1) ? i18n.t('proxiwashScreen.numAvailable')
  389. : i18n.t('proxiwashScreen.numAvailablePlural'));
  390. return (
  391. <View style={{
  392. flexDirection: 'row',
  393. marginLeft: 5,
  394. marginRight: 5,
  395. marginBottom: 10,
  396. marginTop: 20,
  397. }}>
  398. <Avatar.Icon
  399. icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
  400. color={this.colors.primary}
  401. style={{backgroundColor: 'transparent'}}
  402. />
  403. <View style={{
  404. justifyContent: 'center',
  405. }}>
  406. <Text style={{
  407. fontSize: 20,
  408. fontWeight: 'bold',
  409. }}>
  410. {section.title}
  411. </Text>
  412. <Text style={{
  413. color: this.colors.subtitle,
  414. }}>
  415. {subtitle}
  416. </Text>
  417. </View>
  418. </View>
  419. );
  420. }
  421. /**
  422. * Gets the list item to be rendered
  423. *
  424. * @param item The object containing the item's FetchedData
  425. * @param section The object describing the current SectionList section
  426. * @returns {React.Node}
  427. */
  428. getRenderItem({item, section}: Object) {
  429. const isMachineRunning = ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates["EN COURS"];
  430. let displayNumber = item.number;
  431. if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
  432. displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(item.number));
  433. const machineName = (section.title === i18n.t('proxiwashScreen.dryers') ?
  434. i18n.t('proxiwashScreen.dryer') :
  435. i18n.t('proxiwashScreen.washer')) + ' n°' + displayNumber;
  436. const isDryer = section.title === i18n.t('proxiwashScreen.dryers');
  437. const onPress = this.showModal.bind(this, machineName, item, isDryer);
  438. let width = item.donePercent !== '' ? (parseInt(item.donePercent)).toString() + '%' : 0;
  439. if (ProxiwashConstants.machineStates[item.state] === '0')
  440. width = '100%';
  441. return (
  442. <ProxiwashListItem
  443. title={machineName}
  444. description={isMachineRunning ? item.startTime + '/' + item.endTime : ''}
  445. onPress={onPress}
  446. progress={width}
  447. state={item.state}
  448. isWatched={this.isMachineWatched(item.number)}
  449. isDryer={isDryer}
  450. statusText={stateStrings[ProxiwashConstants.machineStates[item.state]]}
  451. statusIcon={stateIcons[ProxiwashConstants.machineStates[item.state]]}
  452. height={LIST_ITEM_HEIGHT}
  453. />
  454. );
  455. }
  456. render() {
  457. const nav = this.props.navigation;
  458. return (
  459. <View>
  460. <Banner
  461. visible={this.state.bannerVisible}
  462. actions={[
  463. {
  464. label: 'OK',
  465. onPress: this.onHideBanner,
  466. },
  467. ]}
  468. icon={() => <Avatar.Icon
  469. icon={'information'}
  470. size={40}
  471. />}
  472. >
  473. {i18n.t('proxiwashScreen.enableNotificationsTip')}
  474. </Banner>
  475. <CustomModal onRef={this.onModalRef}>
  476. {this.state.modalCurrentDisplayItem}
  477. </CustomModal>
  478. <WebSectionList
  479. createDataset={this.createDataset}
  480. navigation={nav}
  481. fetchUrl={DATA_URL}
  482. renderItem={this.getRenderItem}
  483. renderSectionHeader={this.getRenderSectionHeader}
  484. autoRefreshTime={REFRESH_TIME}
  485. refreshOnFocus={true}
  486. updateData={this.state.machinesWatched.length}/>
  487. </View>
  488. );
  489. }
  490. }
  491. export default withTheme(ProxiwashScreen);