Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
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

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