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

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