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

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