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

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