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.

FetchedDataSectionList.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. // @flow
  2. import * as React from 'react';
  3. import WebDataManager from "../utils/WebDataManager";
  4. import {H3, Spinner, Tab, TabHeading, Tabs, Text} from "native-base";
  5. import {RefreshControl, SectionList, View} from "react-native";
  6. import CustomMaterialIcon from "./CustomMaterialIcon";
  7. import i18n from 'i18n-js';
  8. import ThemeManager from "../utils/ThemeManager";
  9. import BaseContainer from "./BaseContainer";
  10. type Props = {
  11. navigation: Object,
  12. }
  13. type State = {
  14. refreshing: boolean,
  15. firstLoading: boolean,
  16. fetchedData: Object,
  17. machinesWatched: Array<string>,
  18. };
  19. /**
  20. * Class used to create a basic list view using online json data.
  21. * Used by inheriting from it and redefining getters.
  22. */
  23. export default class FetchedDataSectionList extends React.Component<Props, State> {
  24. webDataManager: WebDataManager;
  25. willFocusSubscription: function;
  26. willBlurSubscription: function;
  27. refreshInterval: IntervalID;
  28. refreshTime: number;
  29. lastRefresh: Date;
  30. minTimeBetweenRefresh = 60;
  31. state = {
  32. refreshing: false,
  33. firstLoading: true,
  34. fetchedData: {},
  35. machinesWatched: [],
  36. };
  37. onRefresh: Function;
  38. renderSectionHeaderEmpty: Function;
  39. renderSectionHeaderNotEmpty: Function;
  40. renderItemEmpty: Function;
  41. renderItemNotEmpty: Function;
  42. constructor(fetchUrl: string, refreshTime: number) {
  43. super();
  44. this.webDataManager = new WebDataManager(fetchUrl);
  45. this.refreshTime = refreshTime;
  46. // creating references to functions used in render()
  47. this.onRefresh = this.onRefresh.bind(this);
  48. this.renderSectionHeaderEmpty = this.renderSectionHeader.bind(this, true);
  49. this.renderSectionHeaderNotEmpty = this.renderSectionHeader.bind(this, false);
  50. this.renderItemEmpty = this.renderItem.bind(this, true);
  51. this.renderItemNotEmpty = this.renderItem.bind(this, false);
  52. }
  53. shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
  54. return this.state.refreshing !== nextState.refreshing ||
  55. nextState.firstLoading !== this.state.firstLoading ||
  56. nextState.machinesWatched.length !== this.state.machinesWatched.length ||
  57. nextState.fetchedData.len !== this.state.fetchedData.len;
  58. }
  59. /**
  60. * Get the translation for the header in the current language
  61. * @return {string}
  62. */
  63. getHeaderTranslation(): string {
  64. return "Header";
  65. }
  66. /**
  67. * Get the translation for the toasts in the current language
  68. * @return {string}
  69. */
  70. getUpdateToastTranslations(): Array<string> {
  71. return ["whoa", "nah"];
  72. }
  73. setMinTimeRefresh(value: number) {
  74. this.minTimeBetweenRefresh = value;
  75. }
  76. /**
  77. * Register react navigation events on first screen load.
  78. * Allows to detect when the screen is focused
  79. */
  80. componentDidMount() {
  81. this.willFocusSubscription = this.props.navigation.addListener(
  82. 'willFocus', this.onScreenFocus.bind(this));
  83. this.willBlurSubscription = this.props.navigation.addListener(
  84. 'willBlur', this.onScreenBlur.bind(this));
  85. }
  86. /**
  87. * Refresh data when focusing the screen and setup a refresh interval if asked to
  88. */
  89. onScreenFocus() {
  90. this.onRefresh();
  91. if (this.refreshTime > 0)
  92. this.refreshInterval = setInterval(this.onRefresh.bind(this), this.refreshTime)
  93. }
  94. /**
  95. * Remove any interval on un-focus
  96. */
  97. onScreenBlur() {
  98. clearInterval(this.refreshInterval);
  99. }
  100. /**
  101. * Unregister from event when un-mounting components
  102. */
  103. componentWillUnmount() {
  104. if (this.willBlurSubscription !== undefined)
  105. this.willBlurSubscription.remove();
  106. if (this.willFocusSubscription !== undefined)
  107. this.willFocusSubscription.remove();
  108. }
  109. /**
  110. * Refresh data and show a toast if any error occurred
  111. * @private
  112. */
  113. onRefresh() {
  114. let canRefresh;
  115. if (this.lastRefresh !== undefined)
  116. canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh;
  117. else
  118. canRefresh = true;
  119. if (canRefresh) {
  120. this.setState({refreshing: true});
  121. this.webDataManager.readData()
  122. .then((fetchedData) => {
  123. this.setState({
  124. fetchedData: fetchedData,
  125. refreshing: false,
  126. firstLoading: false
  127. });
  128. this.lastRefresh = new Date();
  129. })
  130. .catch(() => {
  131. this.setState({
  132. fetchedData: {},
  133. refreshing: false,
  134. firstLoading: false
  135. });
  136. this.webDataManager.showUpdateToast(this.getUpdateToastTranslations()[0], this.getUpdateToastTranslations()[1]);
  137. });
  138. }
  139. }
  140. /**
  141. * Get the render item to be used for display in the list.
  142. * Must be overridden by inheriting class.
  143. *
  144. * @param item
  145. * @param section
  146. * @return {*}
  147. */
  148. getRenderItem(item: Object, section: Object) {
  149. return <View/>;
  150. }
  151. /**
  152. * Get the render item to be used for the section title in the list.
  153. * Must be overridden by inheriting class.
  154. *
  155. * @param title
  156. * @return {*}
  157. */
  158. getRenderSectionHeader(title: string) {
  159. return <View/>;
  160. }
  161. /**
  162. * Get the render item to be used when the list is empty.
  163. * No need to be overridden, has good defaults.
  164. *
  165. * @param text
  166. * @param isSpinner
  167. * @param icon
  168. * @return {*}
  169. */
  170. getEmptyRenderItem(text: string, isSpinner: boolean, icon: string) {
  171. return (
  172. <View>
  173. <View style={{
  174. justifyContent: 'center',
  175. alignItems: 'center',
  176. width: '100%',
  177. height: 100,
  178. marginBottom: 20
  179. }}>
  180. {isSpinner ?
  181. <Spinner/>
  182. :
  183. <CustomMaterialIcon
  184. icon={icon}
  185. fontSize={100}
  186. width={100}
  187. color={ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText}/>}
  188. </View>
  189. <H3 style={{
  190. textAlign: 'center',
  191. marginRight: 20,
  192. marginLeft: 20,
  193. color: ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText
  194. }}>
  195. {text}
  196. </H3>
  197. </View>);
  198. }
  199. /**
  200. * Create the dataset to be used in the list from the data fetched.
  201. * Must be overridden.
  202. *
  203. * @param fetchedData {Object}
  204. * @return {Array}
  205. */
  206. createDataset(fetchedData: Object): Array<Object> {
  207. return [];
  208. }
  209. datasetKeyExtractor(item: Object) {
  210. return item.text
  211. }
  212. /**
  213. * Create the dataset when no fetched data is available.
  214. * No need to be overridden, has good defaults.
  215. *
  216. * @return
  217. */
  218. createEmptyDataset() {
  219. return [
  220. {
  221. title: '',
  222. data: [
  223. {
  224. text: this.state.refreshing ?
  225. i18n.t('general.loading') :
  226. i18n.t('general.networkError'),
  227. isSpinner: this.state.refreshing,
  228. icon: this.state.refreshing ?
  229. 'refresh' :
  230. 'access-point-network-off'
  231. }
  232. ],
  233. keyExtractor: this.datasetKeyExtractor,
  234. }
  235. ];
  236. }
  237. /**
  238. * Should the app use a tab layout instead of a section list ?
  239. * If yes, each section will be rendered in a new tab.
  240. * Can be overridden.
  241. *
  242. * @return {boolean}
  243. */
  244. hasTabs() {
  245. return false;
  246. }
  247. hasBackButton() {
  248. return false;
  249. }
  250. getRightButton() {
  251. return <View/>
  252. }
  253. hasStickyHeader() {
  254. return false;
  255. }
  256. hasSideMenu() {
  257. return true;
  258. }
  259. renderSectionHeader(isEmpty: boolean, {section: {title}} : Object) {
  260. return isEmpty ?
  261. <View/> :
  262. this.getRenderSectionHeader(title)
  263. }
  264. renderItem(isEmpty: boolean, {item, section}: Object) {
  265. return isEmpty ?
  266. this.getEmptyRenderItem(item.text, item.isSpinner, item.icon) :
  267. this.getRenderItem(item, section)
  268. }
  269. /**
  270. * Get the section list render using the generated dataset
  271. *
  272. * @param dataset
  273. * @return
  274. */
  275. getSectionList(dataset: Array<Object>) {
  276. let isEmpty = dataset[0].data.length === 0;
  277. if (isEmpty)
  278. dataset = this.createEmptyDataset();
  279. return (
  280. <SectionList
  281. sections={dataset}
  282. stickySectionHeadersEnabled={this.hasStickyHeader()}
  283. refreshControl={
  284. <RefreshControl
  285. refreshing={this.state.refreshing}
  286. onRefresh={this.onRefresh}
  287. />
  288. }
  289. renderSectionHeader={isEmpty ? this.renderSectionHeaderEmpty : this.renderSectionHeaderNotEmpty}
  290. renderItem={isEmpty ? this.renderItemEmpty : this.renderItemNotEmpty}
  291. style={{minHeight: 300, width: '100%'}}
  292. contentContainerStyle={
  293. isEmpty ?
  294. {flexGrow: 1, justifyContent: 'center', alignItems: 'center'} : {}
  295. }
  296. />
  297. );
  298. }
  299. /**
  300. * Generate the tabs containing the lists
  301. *
  302. * @param dataset
  303. * @return
  304. */
  305. getTabbedView(dataset: Array<Object>) {
  306. let tabbedView = [];
  307. for (let i = 0; i < dataset.length; i++) {
  308. tabbedView.push(
  309. <Tab heading={
  310. <TabHeading>
  311. <CustomMaterialIcon
  312. icon={dataset[i].icon}
  313. color={ThemeManager.getCurrentThemeVariables().tabIconColor}
  314. fontSize={20}
  315. />
  316. <Text>{dataset[i].title}</Text>
  317. </TabHeading>}
  318. key={dataset[i].title}
  319. style={{backgroundColor: ThemeManager.getCurrentThemeVariables().containerBgColor}}>
  320. {this.getSectionList(
  321. [
  322. {
  323. title: dataset[i].title,
  324. data: dataset[i].data,
  325. extraData: dataset[i].extraData,
  326. keyExtractor: dataset[i].keyExtractor
  327. }
  328. ]
  329. )}
  330. </Tab>);
  331. }
  332. return tabbedView;
  333. }
  334. render() {
  335. // console.log("rendering FetchedDataSectionList");
  336. const dataset = this.createDataset(this.state.fetchedData);
  337. return (
  338. <BaseContainer
  339. navigation={this.props.navigation}
  340. headerTitle={this.getHeaderTranslation()}
  341. headerRightButton={this.getRightButton()}
  342. hasTabs={this.hasTabs()}
  343. hasBackButton={this.hasBackButton()}
  344. hasSideMenu={this.hasSideMenu()}
  345. >
  346. {this.hasTabs() ?
  347. <Tabs
  348. tabContainerStyle={{
  349. elevation: 0, // Fix for android shadow
  350. }}
  351. >
  352. {this.getTabbedView(dataset)}
  353. </Tabs>
  354. :
  355. this.getSectionList(dataset)
  356. }
  357. </BaseContainer>
  358. );
  359. }
  360. }