// @flow import * as React from 'react'; import {ERROR_TYPE, readData} from "../../utils/WebData"; import i18n from "i18n-js"; import {Snackbar} from 'react-native-paper'; import {Animated, RefreshControl, View} from "react-native"; import ErrorView from "./ErrorView"; import BasicLoadingScreen from "./BasicLoadingScreen"; import {withCollapsible} from "../../utils/withCollapsible"; import * as Animatable from 'react-native-animatable'; import CustomTabBar from "../Tabbar/CustomTabBar"; import {Collapsible} from "react-navigation-collapsible"; type Props = { navigation: { [key: string]: any }, fetchUrl: string, autoRefreshTime: number, refreshOnFocus: boolean, renderItem: (data: { [key: string]: any }) => React.Node, createDataset: (data: { [key: string]: any }) => Array, onScroll: (event: SyntheticEvent) => void, collapsibleStack: Collapsible, showError: boolean, itemHeight?: number, updateData?: number, renderSectionHeader?: (data: { [key: string]: any }) => React.Node, stickyHeader?: boolean, } type State = { refreshing: boolean, firstLoading: boolean, fetchedData: { [key: string]: any } | null, snackbarVisible: boolean }; const MIN_REFRESH_TIME = 5 * 1000; /** * 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 { static defaultProps = { stickyHeader: false, updateData: 0, showError: true, }; scrollRef: { current: null | Animated.SectionList }; refreshInterval: IntervalID; lastRefresh: Date | null; state = { refreshing: false, firstLoading: true, fetchedData: null, snackbarVisible: false }; /** * Registers react navigation events on first screen load. * Allows to detect when the screen is focused */ componentDidMount() { const onScreenFocus = this.onScreenFocus.bind(this); const onScreenBlur = this.onScreenBlur.bind(this); this.props.navigation.addListener('focus', onScreenFocus); this.props.navigation.addListener('blur', onScreenBlur); this.scrollRef = React.createRef(); this.onRefresh(); this.lastRefresh = null; } /** * Refreshes data when focusing the screen and setup a refresh interval if asked to */ onScreenFocus() { if (this.props.refreshOnFocus && this.lastRefresh) this.onRefresh(); if (this.props.autoRefreshTime > 0) this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime) // if (this.scrollRef.current) // Reset scroll to top // this.scrollRef.current.getNode().scrollToLocation({animated:false, itemIndex:0, sectionIndex:0}); } /** * Removes any interval on un-focus */ onScreenBlur() { 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: { [key: string]: any }) => { this.setState({ fetchedData: fetchedData, refreshing: false, firstLoading: 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, firstLoading: false }); this.showSnackBar(); }; /** * Refreshes data and shows an animations while doing it */ onRefresh = () => { 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(this.props.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}); itemLayout = (data: { [key: string]: any }, index: number) => { const height = this.props.itemHeight; if (height == null) return undefined; return { length: height, offset: height * index, index } }; renderSectionHeader = (data: { section: { [key: string]: any } }) => { if (this.props.renderSectionHeader != null) { return ( {this.props.renderSectionHeader(data)} ); } else return null; } renderItem = (data: { item: { [key: string]: any }, index: number, section: { [key: string]: any }, separators: { [key: string]: any }, }) => { return ( {this.props.renderItem(data)} ); } onScroll = (event: SyntheticEvent) => { if (this.props.onScroll) this.props.onScroll(event); } render() { let dataset = []; if (this.state.fetchedData != null || (this.state.fetchedData == null && !this.props.showError)) { if (this.state.fetchedData == null) dataset = this.props.createDataset({}); else dataset = this.props.createDataset(this.state.fetchedData); } const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack; return ( } renderSectionHeader={this.renderSectionHeader} renderItem={this.renderItem} stickySectionHeadersEnabled={this.props.stickyHeader} style={{minHeight: '100%'}} ListEmptyComponent={this.state.refreshing ? : } getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined} // Animations onScroll={onScrollWithListener(this.onScroll)} contentContainerStyle={{ paddingTop: containerPaddingTop, paddingBottom: CustomTabBar.TAB_BAR_HEIGHT, minHeight: '100%' }} scrollIndicatorInsets={{top: scrollIndicatorInsetTop}} /> { }, }} duration={4000} style={{ bottom: CustomTabBar.TAB_BAR_HEIGHT }} > {i18n.t("homeScreen.listUpdateFail")} ); } } export default withCollapsible(WebSectionList);