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.

WebSectionList.js 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. // @flow
  2. import * as React from 'react';
  3. import i18n from 'i18n-js';
  4. import {Snackbar} from 'react-native-paper';
  5. import {RefreshControl, View} from 'react-native';
  6. import * as Animatable from 'react-native-animatable';
  7. import {Collapsible} from 'react-navigation-collapsible';
  8. import {StackNavigationProp} from '@react-navigation/stack';
  9. import ErrorView from './ErrorView';
  10. import BasicLoadingScreen from './BasicLoadingScreen';
  11. import withCollapsible from '../../utils/withCollapsible';
  12. import CustomTabBar from '../Tabbar/CustomTabBar';
  13. import {ERROR_TYPE, readData} from '../../utils/WebData';
  14. import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
  15. import type {ApiGenericDataType} from '../../utils/WebData';
  16. export type SectionListDataType<T> = Array<{
  17. title: string,
  18. data: Array<T>,
  19. keyExtractor?: (T) => string,
  20. }>;
  21. type PropsType<T> = {
  22. navigation: StackNavigationProp,
  23. fetchUrl: string,
  24. autoRefreshTime: number,
  25. refreshOnFocus: boolean,
  26. renderItem: (data: {item: T}) => React.Node,
  27. createDataset: (
  28. data: ApiGenericDataType | null,
  29. isLoading?: boolean,
  30. ) => SectionListDataType<T>,
  31. onScroll: (event: SyntheticEvent<EventTarget>) => void,
  32. collapsibleStack: Collapsible,
  33. showError?: boolean,
  34. itemHeight?: number | null,
  35. updateData?: number,
  36. renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
  37. renderSectionHeader?: (
  38. data: {section: {title: string}},
  39. isLoading?: boolean,
  40. ) => React.Node,
  41. stickyHeader?: boolean,
  42. };
  43. type StateType = {
  44. refreshing: boolean,
  45. fetchedData: ApiGenericDataType | null,
  46. snackbarVisible: boolean,
  47. };
  48. const MIN_REFRESH_TIME = 5 * 1000;
  49. /**
  50. * Component used to render a SectionList with data fetched from the web
  51. *
  52. * This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
  53. * To force the component to update, change the value of updateData.
  54. */
  55. class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
  56. static defaultProps = {
  57. showError: true,
  58. itemHeight: null,
  59. updateData: 0,
  60. renderListHeaderComponent: (): React.Node => null,
  61. renderSectionHeader: (): React.Node => null,
  62. stickyHeader: false,
  63. };
  64. refreshInterval: IntervalID;
  65. lastRefresh: Date | null;
  66. constructor() {
  67. super();
  68. this.state = {
  69. refreshing: false,
  70. fetchedData: null,
  71. snackbarVisible: false,
  72. };
  73. }
  74. /**
  75. * Registers react navigation events on first screen load.
  76. * Allows to detect when the screen is focused
  77. */
  78. componentDidMount() {
  79. const {navigation} = this.props;
  80. navigation.addListener('focus', this.onScreenFocus);
  81. navigation.addListener('blur', this.onScreenBlur);
  82. this.lastRefresh = null;
  83. this.onRefresh();
  84. }
  85. /**
  86. * Refreshes data when focusing the screen and setup a refresh interval if asked to
  87. */
  88. onScreenFocus = () => {
  89. const {props} = this;
  90. if (props.refreshOnFocus && this.lastRefresh) this.onRefresh();
  91. if (props.autoRefreshTime > 0)
  92. this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
  93. };
  94. /**
  95. * Removes any interval on un-focus
  96. */
  97. onScreenBlur = () => {
  98. clearInterval(this.refreshInterval);
  99. };
  100. /**
  101. * Callback used when fetch is successful.
  102. * It will update the displayed data and stop the refresh animation
  103. *
  104. * @param fetchedData The newly fetched data
  105. */
  106. onFetchSuccess = (fetchedData: ApiGenericDataType) => {
  107. this.setState({
  108. fetchedData,
  109. refreshing: false,
  110. });
  111. this.lastRefresh = new Date();
  112. };
  113. /**
  114. * Callback used when fetch encountered an error.
  115. * It will reset the displayed data and show an error.
  116. */
  117. onFetchError = () => {
  118. this.setState({
  119. fetchedData: null,
  120. refreshing: false,
  121. });
  122. this.showSnackBar();
  123. };
  124. /**
  125. * Refreshes data and shows an animations while doing it
  126. */
  127. onRefresh = () => {
  128. const {fetchUrl} = this.props;
  129. let canRefresh;
  130. if (this.lastRefresh != null) {
  131. const last = this.lastRefresh;
  132. canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
  133. } else canRefresh = true;
  134. if (canRefresh) {
  135. this.setState({refreshing: true});
  136. readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
  137. }
  138. };
  139. /**
  140. * Shows the error popup
  141. */
  142. showSnackBar = () => {
  143. this.setState({snackbarVisible: true});
  144. };
  145. /**
  146. * Hides the error popup
  147. */
  148. hideSnackBar = () => {
  149. this.setState({snackbarVisible: false});
  150. };
  151. getItemLayout = (
  152. data: T,
  153. index: number,
  154. ): {length: number, offset: number, index: number} | null => {
  155. const {itemHeight} = this.props;
  156. if (itemHeight == null) return null;
  157. return {
  158. length: itemHeight,
  159. offset: itemHeight * index,
  160. index,
  161. };
  162. };
  163. getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
  164. const {renderSectionHeader} = this.props;
  165. const {refreshing} = this.state;
  166. if (renderSectionHeader != null) {
  167. return (
  168. <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
  169. {renderSectionHeader(data, refreshing)}
  170. </Animatable.View>
  171. );
  172. }
  173. return null;
  174. };
  175. getRenderItem = (data: {item: T}): React.Node => {
  176. const {renderItem} = this.props;
  177. return (
  178. <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
  179. {renderItem(data)}
  180. </Animatable.View>
  181. );
  182. };
  183. onScroll = (event: SyntheticEvent<EventTarget>) => {
  184. const {onScroll} = this.props;
  185. if (onScroll != null) onScroll(event);
  186. };
  187. render(): React.Node {
  188. const {props, state} = this;
  189. let dataset = [];
  190. if (
  191. state.fetchedData != null ||
  192. (state.fetchedData == null && !props.showError)
  193. )
  194. dataset = props.createDataset(state.fetchedData, state.refreshing);
  195. const {containerPaddingTop} = props.collapsibleStack;
  196. return (
  197. <View>
  198. <CollapsibleSectionList
  199. sections={dataset}
  200. extraData={props.updateData}
  201. refreshControl={
  202. <RefreshControl
  203. progressViewOffset={containerPaddingTop}
  204. refreshing={state.refreshing}
  205. onRefresh={this.onRefresh}
  206. />
  207. }
  208. renderSectionHeader={this.getRenderSectionHeader}
  209. renderItem={this.getRenderItem}
  210. stickySectionHeadersEnabled={props.stickyHeader}
  211. style={{minHeight: '100%'}}
  212. ListHeaderComponent={
  213. props.renderListHeaderComponent != null
  214. ? props.renderListHeaderComponent(state.fetchedData)
  215. : null
  216. }
  217. ListEmptyComponent={
  218. state.refreshing ? (
  219. <BasicLoadingScreen />
  220. ) : (
  221. <ErrorView
  222. navigation={props.navigation}
  223. errorCode={ERROR_TYPE.CONNECTION_ERROR}
  224. onRefresh={this.onRefresh}
  225. />
  226. )
  227. }
  228. getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
  229. onScroll={this.onScroll}
  230. hasTab
  231. />
  232. <Snackbar
  233. visible={state.snackbarVisible}
  234. onDismiss={this.hideSnackBar}
  235. action={{
  236. label: 'OK',
  237. onPress: () => {},
  238. }}
  239. duration={4000}
  240. style={{
  241. bottom: CustomTabBar.TAB_BAR_HEIGHT,
  242. }}>
  243. {i18n.t('general.listUpdateFail')}
  244. </Snackbar>
  245. </View>
  246. );
  247. }
  248. }
  249. export default withCollapsible(WebSectionList);