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.

WebSectionList.js 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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) {
  91. setTimeout(this.onRefresh, 200);
  92. }
  93. if (props.autoRefreshTime > 0)
  94. this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
  95. };
  96. /**
  97. * Removes any interval on un-focus
  98. */
  99. onScreenBlur = () => {
  100. clearInterval(this.refreshInterval);
  101. };
  102. /**
  103. * Callback used when fetch is successful.
  104. * It will update the displayed data and stop the refresh animation
  105. *
  106. * @param fetchedData The newly fetched data
  107. */
  108. onFetchSuccess = (fetchedData: ApiGenericDataType) => {
  109. this.setState({
  110. fetchedData,
  111. refreshing: false,
  112. });
  113. this.lastRefresh = new Date();
  114. };
  115. /**
  116. * Callback used when fetch encountered an error.
  117. * It will reset the displayed data and show an error.
  118. */
  119. onFetchError = () => {
  120. this.setState({
  121. fetchedData: null,
  122. refreshing: false,
  123. });
  124. this.showSnackBar();
  125. };
  126. /**
  127. * Refreshes data and shows an animations while doing it
  128. */
  129. onRefresh = () => {
  130. const {fetchUrl} = this.props;
  131. let canRefresh;
  132. if (this.lastRefresh != null) {
  133. const last = this.lastRefresh;
  134. canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
  135. } else canRefresh = true;
  136. if (canRefresh) {
  137. this.setState({refreshing: true});
  138. readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
  139. }
  140. };
  141. /**
  142. * Shows the error popup
  143. */
  144. showSnackBar = () => {
  145. this.setState({snackbarVisible: true});
  146. };
  147. /**
  148. * Hides the error popup
  149. */
  150. hideSnackBar = () => {
  151. this.setState({snackbarVisible: false});
  152. };
  153. getItemLayout = (
  154. data: T,
  155. index: number,
  156. ): {length: number, offset: number, index: number} | null => {
  157. const {itemHeight} = this.props;
  158. if (itemHeight == null) return null;
  159. return {
  160. length: itemHeight,
  161. offset: itemHeight * index,
  162. index,
  163. };
  164. };
  165. getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
  166. const {renderSectionHeader} = this.props;
  167. const {refreshing} = this.state;
  168. if (renderSectionHeader != null) {
  169. return (
  170. <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
  171. {renderSectionHeader(data, refreshing)}
  172. </Animatable.View>
  173. );
  174. }
  175. return null;
  176. };
  177. getRenderItem = (data: {item: T}): React.Node => {
  178. const {renderItem} = this.props;
  179. return (
  180. <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
  181. {renderItem(data)}
  182. </Animatable.View>
  183. );
  184. };
  185. onScroll = (event: SyntheticEvent<EventTarget>) => {
  186. const {onScroll} = this.props;
  187. if (onScroll != null) onScroll(event);
  188. };
  189. render(): React.Node {
  190. const {props, state} = this;
  191. let dataset = [];
  192. if (
  193. state.fetchedData != null ||
  194. (state.fetchedData == null && !props.showError)
  195. )
  196. dataset = props.createDataset(state.fetchedData, state.refreshing);
  197. const {containerPaddingTop} = props.collapsibleStack;
  198. return (
  199. <View>
  200. <CollapsibleSectionList
  201. sections={dataset}
  202. extraData={props.updateData}
  203. refreshControl={
  204. <RefreshControl
  205. progressViewOffset={containerPaddingTop}
  206. refreshing={state.refreshing}
  207. onRefresh={this.onRefresh}
  208. />
  209. }
  210. renderSectionHeader={this.getRenderSectionHeader}
  211. renderItem={this.getRenderItem}
  212. stickySectionHeadersEnabled={props.stickyHeader}
  213. style={{minHeight: '100%'}}
  214. ListHeaderComponent={
  215. props.renderListHeaderComponent != null
  216. ? props.renderListHeaderComponent(state.fetchedData)
  217. : null
  218. }
  219. ListEmptyComponent={
  220. state.refreshing ? (
  221. <BasicLoadingScreen />
  222. ) : (
  223. <ErrorView
  224. navigation={props.navigation}
  225. errorCode={ERROR_TYPE.CONNECTION_ERROR}
  226. onRefresh={this.onRefresh}
  227. />
  228. )
  229. }
  230. getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
  231. onScroll={this.onScroll}
  232. hasTab
  233. />
  234. <Snackbar
  235. visible={state.snackbarVisible}
  236. onDismiss={this.hideSnackBar}
  237. action={{
  238. label: 'OK',
  239. onPress: () => {},
  240. }}
  241. duration={4000}
  242. style={{
  243. bottom: CustomTabBar.TAB_BAR_HEIGHT,
  244. }}>
  245. {i18n.t('general.listUpdateFail')}
  246. </Snackbar>
  247. </View>
  248. );
  249. }
  250. }
  251. export default withCollapsible(WebSectionList);