/* * Copyright (c) 2019 - 2020 Arnaud Vergnet. * * This file is part of Campus INSAT. * * Campus INSAT is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Campus INSAT is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Campus INSAT. If not, see . */ import * as React from 'react'; import i18n from 'i18n-js'; import { Snackbar } from 'react-native-paper'; import { NativeSyntheticEvent, RefreshControl, SectionListData, StyleSheet, View, } from 'react-native'; import * as Animatable from 'react-native-animatable'; import { Collapsible } from 'react-navigation-collapsible'; import { StackNavigationProp } from '@react-navigation/stack'; import ErrorView from './ErrorView'; import BasicLoadingScreen from './BasicLoadingScreen'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; import { ERROR_TYPE, readData } from '../../utils/WebData'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import GENERAL_STYLES from '../../constants/Styles'; export type SectionListDataType = Array<{ title: string; icon?: string; data: Array; keyExtractor?: (data: ItemT) => string; }>; type PropsType = { navigation: StackNavigationProp; fetchUrl: string; autoRefreshTime: number; refreshOnFocus: boolean; renderItem: (data: { item: ItemT }) => React.ReactNode; createDataset: ( data: RawData | null, isLoading?: boolean ) => SectionListDataType; onScroll?: (event: NativeSyntheticEvent) => void; showError?: boolean; itemHeight?: number | null; updateData?: number; renderListHeaderComponent?: ( data: RawData | null ) => React.ComponentType | React.ReactElement | null; renderSectionHeader?: ( data: { section: SectionListData }, isLoading?: boolean ) => React.ReactElement | null; stickyHeader?: boolean; }; type StateType = { refreshing: boolean; fetchedData: RawData | null; snackbarVisible: boolean; }; const MIN_REFRESH_TIME = 5 * 1000; const styles = StyleSheet.create({ container: { minHeight: '100%', }, }); /** * Component used to render a SectionList with data fetched from the web * * This is a pure component, meaning it will only update if a shallow comparison of state and props is different. * To force the component to update, change the value of updateData. */ class WebSectionList extends React.PureComponent< PropsType, StateType > { static defaultProps = { showError: true, itemHeight: null, updateData: 0, renderListHeaderComponent: () => null, renderSectionHeader: () => null, stickyHeader: false, }; refreshInterval: NodeJS.Timeout | undefined; lastRefresh: Date | undefined; constructor(props: PropsType) { super(props); this.state = { refreshing: false, fetchedData: null, snackbarVisible: false, }; } /** * Registers react navigation events on first screen load. * Allows to detect when the screen is focused */ componentDidMount() { const { navigation } = this.props; navigation.addListener('focus', this.onScreenFocus); navigation.addListener('blur', this.onScreenBlur); this.lastRefresh = undefined; this.onRefresh(); } /** * Refreshes data when focusing the screen and setup a refresh interval if asked to */ onScreenFocus = () => { const { props } = this; if (props.refreshOnFocus && this.lastRefresh) { setTimeout(this.onRefresh, 200); } if (props.autoRefreshTime > 0) { this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime); } }; /** * Removes any interval on un-focus */ onScreenBlur = () => { if (this.refreshInterval) { clearInterval(this.refreshInterval); } }; /** * Callback used when fetch is successful. * It will update the displayed data and stop the refresh animation * * @param fetchedData The newly fetched data */ onFetchSuccess = (fetchedData: RawData) => { this.setState({ fetchedData, refreshing: false, }); this.lastRefresh = new Date(); }; /** * Callback used when fetch encountered an error. * It will reset the displayed data and show an error. */ onFetchError = () => { this.setState({ fetchedData: null, refreshing: false, }); this.showSnackBar(); }; /** * Refreshes data and shows an animations while doing it */ onRefresh = () => { const { fetchUrl } = this.props; let canRefresh; if (this.lastRefresh != null) { const last = this.lastRefresh; canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME; } else { canRefresh = true; } if (canRefresh) { this.setState({ refreshing: true }); readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError); } }; /** * Shows the error popup */ showSnackBar = () => { this.setState({ snackbarVisible: true }); }; /** * Hides the error popup */ hideSnackBar = () => { this.setState({ snackbarVisible: false }); }; getItemLayout = ( height: number, data: Array> | null, index: number ): { length: number; offset: number; index: number } => { return { length: height, offset: height * index, index, }; }; getRenderSectionHeader = (data: { section: SectionListData }) => { const { renderSectionHeader } = this.props; const { refreshing } = this.state; if (renderSectionHeader != null) { return ( {renderSectionHeader(data, refreshing)} ); } return null; }; getRenderItem = (data: { item: ItemT }) => { const { renderItem } = this.props; return ( {renderItem(data)} ); }; onScroll = (event: NativeSyntheticEvent) => { const { onScroll } = this.props; if (onScroll != null) { onScroll(event); } }; render() { const { props, state } = this; const { itemHeight } = props; let dataset: SectionListDataType = []; if ( state.fetchedData != null || (state.fetchedData == null && !props.showError) ) { dataset = props.createDataset(state.fetchedData, state.refreshing); } return ( ({ refreshControl: ( ), })} renderSectionHeader={this.getRenderSectionHeader} renderItem={this.getRenderItem} stickySectionHeadersEnabled={props.stickyHeader} style={styles.container} ListHeaderComponent={ props.renderListHeaderComponent != null ? props.renderListHeaderComponent(state.fetchedData) : null } ListEmptyComponent={ state.refreshing ? ( ) : ( ) } getItemLayout={ itemHeight ? (data, index) => this.getItemLayout(itemHeight, data, index) : undefined } onScroll={this.onScroll} hasTab={true} /> {}, }} duration={4000} style={{ bottom: TAB_BAR_HEIGHT, }} > {i18n.t('general.listUpdateFail')} ); } } export default WebSectionList;