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.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /*
  2. * Copyright (c) 2019 - 2020 Arnaud Vergnet.
  3. *
  4. * This file is part of Campus INSAT.
  5. *
  6. * Campus INSAT is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Campus INSAT is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import React, { useLayoutEffect, useRef, useState } from 'react';
  20. import {
  21. FlatList,
  22. NativeScrollEvent,
  23. NativeSyntheticEvent,
  24. SectionListData,
  25. StyleSheet,
  26. } from 'react-native';
  27. import i18n from 'i18n-js';
  28. import { Headline, useTheme } from 'react-native-paper';
  29. import {
  30. CommonActions,
  31. useFocusEffect,
  32. useNavigation,
  33. } from '@react-navigation/native';
  34. import { StackScreenProps } from '@react-navigation/stack';
  35. import * as Animatable from 'react-native-animatable';
  36. import { View } from 'react-native-animatable';
  37. import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
  38. import DashboardItem from '../../components/Home/EventDashboardItem';
  39. import WebSectionList from '../../components/Screens/WebSectionList';
  40. import FeedItem from '../../components/Home/FeedItem';
  41. import SmallDashboardItem from '../../components/Home/SmallDashboardItem';
  42. import PreviewEventDashboardItem from '../../components/Home/PreviewEventDashboardItem';
  43. import ActionsDashBoardItem from '../../components/Home/ActionsDashboardItem';
  44. import MaterialHeaderButtons, {
  45. Item,
  46. } from '../../components/Overrides/CustomHeaderButton';
  47. import AnimatedFAB from '../../components/Animations/AnimatedFAB';
  48. import LogoutDialog from '../../components/Amicale/LogoutDialog';
  49. import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
  50. import MascotPopup from '../../components/Mascot/MascotPopup';
  51. import { getDisplayEvent, getFutureEvents } from '../../utils/Home';
  52. import type { PlanningEventType } from '../../utils/Planning';
  53. import GENERAL_STYLES from '../../constants/Styles';
  54. import Urls from '../../constants/Urls';
  55. import { readData } from '../../utils/WebData';
  56. import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
  57. import { ServiceItemType } from '../../utils/Services';
  58. import { useCurrentDashboard } from '../../context/preferencesContext';
  59. import { MainRoutes } from '../../navigation/MainNavigator';
  60. import { useLoginState } from '../../context/loginContext';
  61. const FEED_ITEM_HEIGHT = 500;
  62. const SECTIONS_ID = ['dashboard', 'news_feed'];
  63. const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
  64. export type FeedItemType = {
  65. id: string;
  66. message: string;
  67. url: string;
  68. image: string | null;
  69. video: string | null;
  70. link: string | null;
  71. time: number;
  72. page_id: string;
  73. };
  74. export type FullDashboardType = {
  75. today_menu: Array<{ [key: string]: object }>;
  76. proximo_articles: number;
  77. available_dryers: number;
  78. available_washers: number;
  79. today_events: Array<PlanningEventType>;
  80. available_tutorials: number;
  81. };
  82. type RawNewsFeedType = { [key: string]: Array<FeedItemType> };
  83. type RawDashboardType = {
  84. news_feed: RawNewsFeedType;
  85. dashboard: FullDashboardType;
  86. };
  87. type Props = StackScreenProps<TabStackParamsList, TabRoutes.Home>;
  88. const styles = StyleSheet.create({
  89. dashboardRow: {
  90. marginLeft: 'auto',
  91. marginRight: 'auto',
  92. marginTop: 10,
  93. marginBottom: 10,
  94. },
  95. sectionHeader: {
  96. textAlign: 'center',
  97. marginTop: 50,
  98. marginBottom: 10,
  99. },
  100. sectionHeaderEmpty: {
  101. textAlign: 'center',
  102. marginTop: 50,
  103. marginBottom: 10,
  104. marginLeft: 20,
  105. marginRight: 20,
  106. },
  107. activityIndicator: {
  108. marginTop: 10,
  109. },
  110. content: {
  111. position: 'absolute',
  112. width: '100%',
  113. height: '100%',
  114. },
  115. });
  116. const sortFeedTime = (a: FeedItemType, b: FeedItemType): number =>
  117. b.time - a.time;
  118. const generateNewsFeed = (rawFeed: RawNewsFeedType): Array<FeedItemType> => {
  119. const finalFeed: Array<FeedItemType> = [];
  120. Object.keys(rawFeed).forEach((key: string) => {
  121. const category: Array<FeedItemType> | null = rawFeed[key];
  122. if (category != null && category.length > 0) {
  123. finalFeed.push(...category);
  124. }
  125. });
  126. finalFeed.sort(sortFeedTime);
  127. return finalFeed;
  128. };
  129. function HomeScreen(props: Props) {
  130. const theme = useTheme();
  131. const navigation = useNavigation();
  132. const [dialogVisible, setDialogVisible] = useState(false);
  133. const fabRef = useRef<AnimatedFAB>(null);
  134. const isLoggedIn = useLoginState();
  135. const { currentDashboard } = useCurrentDashboard();
  136. let homeDashboard: FullDashboardType | null = null;
  137. useLayoutEffect(() => {
  138. const getHeaderButton = () => {
  139. let onPressLog = () =>
  140. navigation.navigate('login', { nextScreen: 'profile' });
  141. let logIcon = 'login';
  142. let logColor = theme.colors.primary;
  143. if (isLoggedIn) {
  144. onPressLog = () => showDisconnectDialog();
  145. logIcon = 'logout';
  146. logColor = theme.colors.text;
  147. }
  148. return (
  149. <MaterialHeaderButtons>
  150. <Item
  151. title={'log'}
  152. iconName={logIcon}
  153. color={logColor}
  154. onPress={onPressLog}
  155. />
  156. <Item
  157. title={i18n.t('screens.settings.title')}
  158. iconName={'cog'}
  159. onPress={() => navigation.navigate(MainRoutes.Settings)}
  160. />
  161. </MaterialHeaderButtons>
  162. );
  163. };
  164. navigation.setOptions({
  165. headerRight: getHeaderButton,
  166. });
  167. // eslint-disable-next-line react-hooks/exhaustive-deps
  168. }, [navigation, isLoggedIn]);
  169. useFocusEffect(
  170. React.useCallback(() => {
  171. const handleNavigationParams = () => {
  172. const { route } = props;
  173. if (route.params != null) {
  174. if (route.params.nextScreen != null) {
  175. navigation.navigate(route.params.nextScreen, route.params.data);
  176. // reset params to prevent infinite loop
  177. navigation.dispatch(CommonActions.setParams({ nextScreen: null }));
  178. }
  179. }
  180. };
  181. // handle link open when home is not focused or created
  182. handleNavigationParams();
  183. // eslint-disable-next-line react-hooks/exhaustive-deps
  184. }, [isLoggedIn])
  185. );
  186. /**
  187. * Gets the event dashboard render item.
  188. * If a preview is available, it will be rendered inside
  189. *
  190. * @param content
  191. * @return {*}
  192. */
  193. const getDashboardEvent = (content: Array<PlanningEventType>) => {
  194. const futureEvents = getFutureEvents(content);
  195. const displayEvent = getDisplayEvent(futureEvents);
  196. // const clickPreviewAction = () =>
  197. // this.props.navigation.navigate('students', {
  198. // screen: 'planning-information',
  199. // params: {data: displayEvent}
  200. // });
  201. return (
  202. <DashboardItem
  203. eventNumber={futureEvents.length}
  204. clickAction={onEventContainerClick}
  205. >
  206. <PreviewEventDashboardItem
  207. event={displayEvent}
  208. clickAction={onEventContainerClick}
  209. />
  210. </DashboardItem>
  211. );
  212. };
  213. /**
  214. * Gets a dashboard item with a row of shortcut buttons.
  215. *
  216. * @param content
  217. * @return {*}
  218. */
  219. const getDashboardRow = (content: Array<ServiceItemType | null>) => {
  220. return (
  221. <FlatList
  222. data={content}
  223. renderItem={getDashboardRowRenderItem}
  224. horizontal
  225. contentContainerStyle={styles.dashboardRow}
  226. />
  227. );
  228. };
  229. /**
  230. * Gets a dashboard shortcut item
  231. *
  232. * @param item
  233. * @returns {*}
  234. */
  235. const getDashboardRowRenderItem = ({
  236. item,
  237. }: {
  238. item: ServiceItemType | null;
  239. }) => {
  240. if (item != null) {
  241. return (
  242. <SmallDashboardItem
  243. image={item.image}
  244. onPress={item.onPress}
  245. badgeCount={
  246. homeDashboard != null && item.badgeFunction != null
  247. ? item.badgeFunction(homeDashboard)
  248. : undefined
  249. }
  250. />
  251. );
  252. }
  253. return <SmallDashboardItem />;
  254. };
  255. const getRenderItem = ({ item }: { item: FeedItemType }) => (
  256. <FeedItem item={item} height={FEED_ITEM_HEIGHT} />
  257. );
  258. const getRenderSectionHeader = (data: {
  259. section: SectionListData<FeedItemType>;
  260. }) => {
  261. const icon = data.section.icon;
  262. if (data.section.data.length > 0) {
  263. return (
  264. <Headline style={styles.sectionHeader}>{data.section.title}</Headline>
  265. );
  266. }
  267. return (
  268. <View>
  269. <Headline
  270. style={{
  271. ...styles.sectionHeaderEmpty,
  272. color: theme.colors.textDisabled,
  273. }}
  274. >
  275. {data.section.title}
  276. </Headline>
  277. {icon ? (
  278. <MaterialCommunityIcons
  279. name={icon}
  280. size={100}
  281. color={theme.colors.textDisabled}
  282. style={GENERAL_STYLES.center}
  283. />
  284. ) : null}
  285. </View>
  286. );
  287. };
  288. const getListHeader = (fetchedData: RawDashboardType | undefined) => {
  289. let dashboard = null;
  290. if (fetchedData != null) {
  291. dashboard = fetchedData.dashboard;
  292. }
  293. return (
  294. <Animatable.View animation="fadeInDown" duration={500} useNativeDriver>
  295. <ActionsDashBoardItem />
  296. {getDashboardRow(currentDashboard)}
  297. {getDashboardEvent(dashboard == null ? [] : dashboard.today_events)}
  298. </Animatable.View>
  299. );
  300. };
  301. const showDisconnectDialog = () => setDialogVisible(true);
  302. const hideDisconnectDialog = () => setDialogVisible(false);
  303. const openScanner = () => navigation.navigate('scanner');
  304. /**
  305. * Creates the dataset to be used in the FlatList
  306. *
  307. * @param fetchedData
  308. * @param isLoading
  309. * @return {*}
  310. */
  311. const createDataset = (
  312. fetchedData: RawDashboardType | undefined,
  313. isLoading: boolean
  314. ): Array<{
  315. title: string;
  316. data: [] | Array<FeedItemType>;
  317. icon?: string;
  318. id: string;
  319. }> => {
  320. let currentNewFeed: Array<FeedItemType> = [];
  321. if (fetchedData) {
  322. if (fetchedData.news_feed) {
  323. currentNewFeed = generateNewsFeed(fetchedData.news_feed);
  324. }
  325. if (fetchedData.dashboard) {
  326. homeDashboard = fetchedData.dashboard;
  327. }
  328. }
  329. if (currentNewFeed.length > 0) {
  330. return [
  331. {
  332. title: i18n.t('screens.home.feedTitle'),
  333. data: currentNewFeed,
  334. id: SECTIONS_ID[1],
  335. },
  336. ];
  337. }
  338. return [
  339. {
  340. title: isLoading
  341. ? i18n.t('screens.home.feedLoading')
  342. : i18n.t('screens.home.feedError'),
  343. data: [],
  344. icon: isLoading ? undefined : 'access-point-network-off',
  345. id: SECTIONS_ID[1],
  346. },
  347. ];
  348. };
  349. const onEventContainerClick = () => navigation.navigate(TabRoutes.Planning);
  350. const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
  351. if (fabRef.current) {
  352. fabRef.current.onScroll(event);
  353. }
  354. };
  355. /**
  356. * Callback when pressing the login button on the banner.
  357. * This hides the banner and takes the user to the login page.
  358. */
  359. const onLogin = () =>
  360. navigation.navigate(MainRoutes.Login, {
  361. nextScreen: 'profile',
  362. });
  363. return (
  364. <View style={GENERAL_STYLES.flex}>
  365. <View style={styles.content}>
  366. <WebSectionList
  367. request={() => readData<RawDashboardType>(Urls.app.dashboard)}
  368. createDataset={createDataset}
  369. autoRefreshTime={REFRESH_TIME}
  370. refreshOnFocus={true}
  371. renderItem={getRenderItem}
  372. itemHeight={FEED_ITEM_HEIGHT}
  373. onScroll={onScroll}
  374. renderSectionHeader={getRenderSectionHeader}
  375. renderListHeaderComponent={getListHeader}
  376. />
  377. </View>
  378. {!isLoggedIn ? (
  379. <MascotPopup
  380. title={i18n.t('screens.home.mascotDialog.title')}
  381. message={i18n.t('screens.home.mascotDialog.message')}
  382. icon="human-greeting"
  383. buttons={{
  384. action: {
  385. message: i18n.t('screens.home.mascotDialog.login'),
  386. icon: 'login',
  387. onPress: onLogin,
  388. },
  389. cancel: {
  390. message: i18n.t('screens.home.mascotDialog.later'),
  391. icon: 'close',
  392. color: theme.colors.warning,
  393. },
  394. }}
  395. emotion={MASCOT_STYLE.CUTE}
  396. />
  397. ) : null}
  398. <AnimatedFAB ref={fabRef} icon="qrcode-scan" onPress={openScanner} />
  399. <LogoutDialog visible={dialogVisible} onDismiss={hideDisconnectDialog} />
  400. </View>
  401. );
  402. }
  403. export default HomeScreen;