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.

HomeScreen.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. // @flow
  2. import * as React from 'react';
  3. import {FlatList} from 'react-native';
  4. import i18n from "i18n-js";
  5. import DashboardItem from "../../components/Home/EventDashboardItem";
  6. import WebSectionList from "../../components/Screens/WebSectionList";
  7. import {withTheme} from 'react-native-paper';
  8. import FeedItem from "../../components/Home/FeedItem";
  9. import SquareDashboardItem from "../../components/Home/SmallDashboardItem";
  10. import PreviewEventDashboardItem from "../../components/Home/PreviewEventDashboardItem";
  11. import {stringToDate} from "../../utils/Planning";
  12. import ActionsDashBoardItem from "../../components/Home/ActionsDashboardItem";
  13. import {CommonActions} from '@react-navigation/native';
  14. import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
  15. import AnimatedFAB from "../../components/Animations/AnimatedFAB";
  16. import {StackNavigationProp} from "@react-navigation/stack";
  17. import type {CustomTheme} from "../../managers/ThemeManager";
  18. import {View} from "react-native-animatable";
  19. import ConnectionManager from "../../managers/ConnectionManager";
  20. import LogoutDialog from "../../components/Amicale/LogoutDialog";
  21. import AsyncStorageManager from "../../managers/AsyncStorageManager";
  22. import AvailableWebsites from "../../constants/AvailableWebsites";
  23. import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
  24. import MascotPopup from "../../components/Mascot/MascotPopup";
  25. // import DATA from "../dashboard_data.json";
  26. const NAME_AMICALE = 'Amicale INSA Toulouse';
  27. const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json";
  28. const FEED_ITEM_HEIGHT = 500;
  29. const SECTIONS_ID = [
  30. 'dashboard',
  31. 'news_feed'
  32. ];
  33. const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
  34. type rawDashboard = {
  35. news_feed: {
  36. data: Array<feedItem>,
  37. },
  38. dashboard: fullDashboard,
  39. }
  40. export type feedItem = {
  41. full_picture: string,
  42. message: string,
  43. permalink_url: string,
  44. created_time: number,
  45. id: string,
  46. };
  47. type fullDashboard = {
  48. today_menu: Array<{ [key: string]: any }>,
  49. proximo_articles: number,
  50. available_dryers: number,
  51. available_washers: number,
  52. today_events: Array<{ [key: string]: any }>,
  53. available_tutorials: number,
  54. }
  55. type dashboardItem = {
  56. id: string,
  57. content: Array<{ [key: string]: any }>
  58. };
  59. type dashboardSmallItem = {
  60. id: string,
  61. data: number,
  62. icon: string,
  63. color: string,
  64. onPress: () => void,
  65. isAvailable: boolean
  66. };
  67. export type event = {
  68. id: number,
  69. title: string,
  70. logo: string | null,
  71. date_begin: string,
  72. date_end: string,
  73. description: string,
  74. club: string,
  75. category_id: number,
  76. url: string,
  77. }
  78. type listSection = {
  79. title: string,
  80. data: Array<dashboardItem> | Array<feedItem>,
  81. id: string
  82. };
  83. type Props = {
  84. navigation: StackNavigationProp,
  85. route: { params: any, ... },
  86. theme: CustomTheme,
  87. }
  88. type State = {
  89. dialogVisible: boolean,
  90. mascotDialogVisible: boolean,
  91. }
  92. /**
  93. * Class defining the app's home screen
  94. */
  95. class HomeScreen extends React.Component<Props, State> {
  96. isLoggedIn: boolean | null;
  97. fabRef: { current: null | AnimatedFAB };
  98. currentNewFeed: Array<feedItem>;
  99. constructor(props) {
  100. super(props);
  101. this.fabRef = React.createRef();
  102. this.currentNewFeed = [];
  103. this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
  104. this.props.navigation.setOptions({
  105. headerRight: this.getHeaderButton,
  106. });
  107. this.state = {
  108. dialogVisible: false,
  109. mascotDialogVisible: AsyncStorageManager.getInstance().preferences.homeShowBanner.current === "1"
  110. && !this.isLoggedIn,
  111. }
  112. }
  113. /**
  114. * Converts a dateString using Unix Timestamp to a formatted date
  115. *
  116. * @param dateString {string} The Unix Timestamp representation of a date
  117. * @return {string} The formatted output date
  118. */
  119. static getFormattedDate(dateString: number) {
  120. let date = new Date(dateString * 1000);
  121. return date.toLocaleString();
  122. }
  123. componentDidMount() {
  124. this.props.navigation.addListener('focus', this.onScreenFocus);
  125. // Handle link open when home is focused
  126. this.props.navigation.addListener('state', this.handleNavigationParams);
  127. }
  128. /**
  129. * Updates login state and navigation parameters on screen focus
  130. */
  131. onScreenFocus = () => {
  132. if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
  133. this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
  134. this.props.navigation.setOptions({
  135. headerRight: this.getHeaderButton,
  136. });
  137. }
  138. // handle link open when home is not focused or created
  139. this.handleNavigationParams();
  140. };
  141. /**
  142. * Navigates to the a new screen if navigation parameters specify one
  143. */
  144. handleNavigationParams = () => {
  145. if (this.props.route.params != null) {
  146. if (this.props.route.params.nextScreen != null) {
  147. this.props.navigation.navigate(this.props.route.params.nextScreen, this.props.route.params.data);
  148. // reset params to prevent infinite loop
  149. this.props.navigation.dispatch(CommonActions.setParams({nextScreen: null}));
  150. }
  151. }
  152. };
  153. /**
  154. * Gets header buttons based on login state
  155. *
  156. * @returns {*}
  157. */
  158. getHeaderButton = () => {
  159. let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"});
  160. let logIcon = "login";
  161. let logColor = this.props.theme.colors.primary;
  162. if (this.isLoggedIn) {
  163. onPressLog = () => this.showDisconnectDialog();
  164. logIcon = "logout";
  165. logColor = this.props.theme.colors.text;
  166. }
  167. const onPressSettings = () => this.props.navigation.navigate("settings");
  168. return <MaterialHeaderButtons>
  169. <Item title="log" iconName={logIcon} color={logColor} onPress={onPressLog}/>
  170. <Item title={i18n.t("screens.settings")} iconName={"settings"} onPress={onPressSettings}/>
  171. </MaterialHeaderButtons>;
  172. };
  173. hideMascotDialog = () => {
  174. AsyncStorageManager.getInstance().savePref(
  175. AsyncStorageManager.getInstance().preferences.homeShowBanner.key,
  176. '0'
  177. );
  178. this.setState({mascotDialogVisible: false})
  179. };
  180. showDisconnectDialog = () => this.setState({dialogVisible: true});
  181. hideDisconnectDialog = () => this.setState({dialogVisible: false});
  182. onProxiwashClick = () => {
  183. this.props.navigation.navigate("proxiwash");
  184. };
  185. onProximoClick = () => {
  186. this.props.navigation.navigate("proximo");
  187. };
  188. onTutorInsaClick = () => {
  189. this.props.navigation.navigate("website", {host: AvailableWebsites.websites.TUTOR_INSA, title: "Tutor'INSA"});
  190. };
  191. onMenuClick = () => {
  192. this.props.navigation.navigate('self-menu');
  193. };
  194. /**
  195. * Creates the dataset to be used in the FlatList
  196. *
  197. * @param fetchedData
  198. * @return {*}
  199. */
  200. createDataset = (fetchedData: rawDashboard) => {
  201. // fetchedData = DATA;
  202. let dashboardData;
  203. if (fetchedData.news_feed != null) {
  204. this.currentNewFeed = fetchedData.news_feed.data;
  205. }
  206. if (fetchedData.dashboard != null)
  207. dashboardData = this.generateDashboardDataset(fetchedData.dashboard);
  208. else
  209. dashboardData = this.generateDashboardDataset(null);
  210. return [
  211. {
  212. title: '',
  213. data: dashboardData,
  214. id: SECTIONS_ID[0]
  215. },
  216. {
  217. title: i18n.t('homeScreen.newsFeed'),
  218. data: this.currentNewFeed,
  219. id: SECTIONS_ID[1]
  220. }
  221. ];
  222. };
  223. /**
  224. * Generates the dataset associated to the dashboard to be displayed in the FlatList as a section
  225. *
  226. * @param dashboardData
  227. * @return {Array<dashboardItem>}
  228. */
  229. generateDashboardDataset(dashboardData: fullDashboard | null): Array<dashboardItem> {
  230. return [
  231. {id: 'actions', content: []},
  232. {
  233. id: 'top',
  234. content: [
  235. {
  236. id: 'washers',
  237. data: dashboardData == null ? 0 : dashboardData.available_washers,
  238. icon: 'washing-machine',
  239. color: this.props.theme.colors.proxiwashColor,
  240. onPress: this.onProxiwashClick,
  241. isAvailable: dashboardData == null ? false : dashboardData.available_washers > 0
  242. },
  243. {
  244. id: 'dryers',
  245. data: dashboardData == null ? 0 : dashboardData.available_dryers,
  246. icon: 'tumble-dryer',
  247. color: this.props.theme.colors.proxiwashColor,
  248. onPress: this.onProxiwashClick,
  249. isAvailable: dashboardData == null ? false : dashboardData.available_dryers > 0
  250. },
  251. {
  252. id: 'available_tutorials',
  253. data: dashboardData == null ? 0 : dashboardData.available_tutorials,
  254. icon: 'school',
  255. color: this.props.theme.colors.tutorinsaColor,
  256. onPress: this.onTutorInsaClick,
  257. isAvailable: dashboardData == null ? false : dashboardData.available_tutorials > 0
  258. },
  259. {
  260. id: 'proximo_articles',
  261. data: dashboardData == null ? 0 : dashboardData.proximo_articles,
  262. icon: 'shopping',
  263. color: this.props.theme.colors.proximoColor,
  264. onPress: this.onProximoClick,
  265. isAvailable: dashboardData == null ? false : dashboardData.proximo_articles > 0
  266. },
  267. {
  268. id: 'today_menu',
  269. data: dashboardData == null ? [] : dashboardData.today_menu,
  270. icon: 'silverware-fork-knife',
  271. color: this.props.theme.colors.menuColor,
  272. onPress: this.onMenuClick,
  273. isAvailable: dashboardData == null ? false : dashboardData.today_menu.length > 0
  274. },
  275. ]
  276. },
  277. {
  278. id: 'event',
  279. content: dashboardData == null ? [] : dashboardData.today_events
  280. },
  281. ];
  282. }
  283. /**
  284. * Gets a dashboard item
  285. *
  286. * @param item The item to display
  287. * @return {*}
  288. */
  289. getDashboardItem(item: dashboardItem) {
  290. let content = item.content;
  291. if (item.id === 'event')
  292. return this.getDashboardEvent(content);
  293. else if (item.id === 'top')
  294. return this.getDashboardRow(content);
  295. else
  296. return this.getDashboardActions();
  297. }
  298. /**
  299. * Gets a dashboard item with action buttons
  300. *
  301. * @returns {*}
  302. */
  303. getDashboardActions() {
  304. return <ActionsDashBoardItem {...this.props} isLoggedIn={this.isLoggedIn}/>;
  305. }
  306. /**
  307. * Gets the time limit depending on the current day:
  308. * 17:30 for every day of the week except for thursday 11:30
  309. * 00:00 on weekends
  310. */
  311. getTodayEventTimeLimit() {
  312. let now = new Date();
  313. if (now.getDay() === 4) // Thursday
  314. now.setHours(11, 30, 0);
  315. else if (now.getDay() === 6 || now.getDay() === 0) // Weekend
  316. now.setHours(0, 0, 0);
  317. else
  318. now.setHours(17, 30, 0);
  319. return now;
  320. }
  321. /**
  322. * Gets the duration (in milliseconds) of an event
  323. *
  324. * @param event {event}
  325. * @return {number} The number of milliseconds
  326. */
  327. getEventDuration(event: event): number {
  328. let start = stringToDate(event.date_begin);
  329. let end = stringToDate(event.date_end);
  330. let duration = 0;
  331. if (start != null && end != null)
  332. duration = end - start;
  333. return duration;
  334. }
  335. /**
  336. * Gets events starting after the limit
  337. *
  338. * @param events
  339. * @param limit
  340. * @return {Array<Object>}
  341. */
  342. getEventsAfterLimit(events: Array<event>, limit: Date): Array<event> {
  343. let validEvents = [];
  344. for (let event of events) {
  345. let startDate = stringToDate(event.date_begin);
  346. if (startDate != null && startDate >= limit) {
  347. validEvents.push(event);
  348. }
  349. }
  350. return validEvents;
  351. }
  352. /**
  353. * Gets the event with the longest duration in the given array.
  354. * If all events have the same duration, return the first in the array.
  355. *
  356. * @param events
  357. */
  358. getLongestEvent(events: Array<event>): event {
  359. let longestEvent = events[0];
  360. let longestTime = 0;
  361. for (let event of events) {
  362. let time = this.getEventDuration(event);
  363. if (time > longestTime) {
  364. longestTime = time;
  365. longestEvent = event;
  366. }
  367. }
  368. return longestEvent;
  369. }
  370. /**
  371. * Gets events that have not yet ended/started
  372. *
  373. * @param events
  374. */
  375. getFutureEvents(events: Array<event>): Array<event> {
  376. let validEvents = [];
  377. let now = new Date();
  378. for (let event of events) {
  379. let startDate = stringToDate(event.date_begin);
  380. let endDate = stringToDate(event.date_end);
  381. if (startDate != null) {
  382. if (startDate > now)
  383. validEvents.push(event);
  384. else if (endDate != null) {
  385. if (endDate > now || endDate < startDate) // Display event if it ends the following day
  386. validEvents.push(event);
  387. }
  388. }
  389. }
  390. return validEvents;
  391. }
  392. /**
  393. * Gets the event to display in the preview
  394. *
  395. * @param events
  396. * @return {Object}
  397. */
  398. getDisplayEvent(events: Array<event>): event | null {
  399. let displayEvent = null;
  400. if (events.length > 1) {
  401. let eventsAfterLimit = this.getEventsAfterLimit(events, this.getTodayEventTimeLimit());
  402. if (eventsAfterLimit.length > 0) {
  403. if (eventsAfterLimit.length === 1)
  404. displayEvent = eventsAfterLimit[0];
  405. else
  406. displayEvent = this.getLongestEvent(events);
  407. } else {
  408. displayEvent = this.getLongestEvent(events);
  409. }
  410. } else if (events.length === 1) {
  411. displayEvent = events[0];
  412. }
  413. return displayEvent;
  414. }
  415. onEventContainerClick = () => this.props.navigation.navigate('planning');
  416. /**
  417. * Gets the event dashboard render item.
  418. * If a preview is available, it will be rendered inside
  419. *
  420. * @param content
  421. * @return {*}
  422. */
  423. getDashboardEvent(content: Array<event>) {
  424. let futureEvents = this.getFutureEvents(content);
  425. let displayEvent = this.getDisplayEvent(futureEvents);
  426. // const clickPreviewAction = () =>
  427. // this.props.navigation.navigate('students', {
  428. // screen: 'planning-information',
  429. // params: {data: displayEvent}
  430. // });
  431. return (
  432. <DashboardItem
  433. eventNumber={futureEvents.length}
  434. clickAction={this.onEventContainerClick}
  435. >
  436. <PreviewEventDashboardItem
  437. event={displayEvent != null ? displayEvent : undefined}
  438. clickAction={this.onEventContainerClick}
  439. />
  440. </DashboardItem>
  441. );
  442. }
  443. /**
  444. * Gets a dashboard shortcut item
  445. *
  446. * @param item
  447. * @returns {*}
  448. */
  449. dashboardRowRenderItem = ({item}: { item: dashboardSmallItem }) => {
  450. return (
  451. <SquareDashboardItem
  452. color={item.color}
  453. icon={item.icon}
  454. clickAction={item.onPress}
  455. isAvailable={item.isAvailable}
  456. badgeNumber={item.data}
  457. />
  458. );
  459. };
  460. /**
  461. * Gets a dashboard item with a row of shortcut buttons.
  462. *
  463. * @param content
  464. * @return {*}
  465. */
  466. getDashboardRow(content: Array<dashboardSmallItem>) {
  467. return (
  468. //$FlowFixMe
  469. <FlatList
  470. data={content}
  471. renderItem={this.dashboardRowRenderItem}
  472. horizontal={true}
  473. contentContainerStyle={{
  474. marginLeft: 'auto',
  475. marginRight: 'auto',
  476. marginTop: 5,
  477. }}
  478. />);
  479. }
  480. /**
  481. * Gets a render item for the given feed object
  482. *
  483. * @param item The feed item to display
  484. * @return {*}
  485. */
  486. getFeedItem(item: feedItem) {
  487. return (
  488. <FeedItem
  489. {...this.props}
  490. item={item}
  491. title={NAME_AMICALE}
  492. subtitle={HomeScreen.getFormattedDate(item.created_time)}
  493. height={FEED_ITEM_HEIGHT}
  494. />
  495. );
  496. }
  497. /**
  498. * Gets a FlatList render item
  499. *
  500. * @param item The item to display
  501. * @param section The current section
  502. * @return {*}
  503. */
  504. getRenderItem = ({item, section}: {
  505. item: { [key: string]: any },
  506. section: listSection
  507. }) => {
  508. if (section.id === SECTIONS_ID[0]) {
  509. const data: dashboardItem = item;
  510. return this.getDashboardItem(data);
  511. } else {
  512. const data: feedItem = item;
  513. return this.getFeedItem(data);
  514. }
  515. };
  516. openScanner = () => this.props.navigation.navigate("scanner");
  517. onScroll = (event: SyntheticEvent<EventTarget>) => {
  518. if (this.fabRef.current != null)
  519. this.fabRef.current.onScroll(event);
  520. };
  521. /**
  522. * Callback when pressing the login button on the banner.
  523. * This hides the banner and takes the user to the login page.
  524. */
  525. onLogin = () => {
  526. this.hideMascotDialog();
  527. this.props.navigation.navigate("login", {nextScreen: "profile"});
  528. }
  529. render() {
  530. return (
  531. <View
  532. style={{flex: 1}}
  533. >
  534. <View style={{
  535. position: "absolute",
  536. width: "100%",
  537. height: "100%",
  538. }}>
  539. <WebSectionList
  540. {...this.props}
  541. createDataset={this.createDataset}
  542. autoRefreshTime={REFRESH_TIME}
  543. refreshOnFocus={true}
  544. fetchUrl={DATA_URL}
  545. renderItem={this.getRenderItem}
  546. itemHeight={FEED_ITEM_HEIGHT}
  547. onScroll={this.onScroll}
  548. showError={false}
  549. />
  550. </View>
  551. <MascotPopup
  552. visible={this.state.mascotDialogVisible}
  553. title={i18n.t("homeScreen.loginBanner.title")}
  554. message={i18n.t("homeScreen.loginBanner.message")}
  555. icon={"check"}
  556. buttons={{
  557. action: {
  558. message: i18n.t("homeScreen.loginBanner.login"),
  559. icon: "login",
  560. onPress: this.onLogin,
  561. },
  562. cancel: {
  563. message: i18n.t("homeScreen.loginBanner.later"),
  564. icon: "close",
  565. color: this.props.theme.colors.warning,
  566. onPress: this.hideMascotDialog,
  567. }
  568. }}
  569. emotion={MASCOT_STYLE.CUTE}
  570. />
  571. <AnimatedFAB
  572. {...this.props}
  573. ref={this.fabRef}
  574. icon="qrcode-scan"
  575. onPress={this.openScanner}
  576. />
  577. <LogoutDialog
  578. {...this.props}
  579. visible={this.state.dialogVisible}
  580. onDismiss={this.hideDisconnectDialog}
  581. />
  582. </View>
  583. );
  584. }
  585. }
  586. export default withTheme(HomeScreen);