Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
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

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