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

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