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

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