application-amicale/components/FetchedDataSectionList.js
2020-02-23 18:15:30 +01:00

398 lines
12 KiB
JavaScript

// @flow
import * as React from 'react';
import WebDataManager from "../utils/WebDataManager";
import {H3, Spinner, Tab, TabHeading, Tabs, Text} from "native-base";
import {RefreshControl, SectionList, View} from "react-native";
import CustomMaterialIcon from "./CustomMaterialIcon";
import i18n from 'i18n-js';
import ThemeManager from "../utils/ThemeManager";
import BaseContainer from "./BaseContainer";
type Props = {
navigation: Object,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
fetchedData: Object,
machinesWatched: Array<string>,
};
/**
* Class used to create a basic list view using online json data.
* Used by inheriting from it and redefining getters.
*/
export default class FetchedDataSectionList extends React.Component<Props, State> {
webDataManager: WebDataManager;
willFocusSubscription: function;
willBlurSubscription: function;
refreshInterval: IntervalID;
refreshTime: number;
lastRefresh: Date;
minTimeBetweenRefresh = 60;
state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
machinesWatched: [],
};
onRefresh: Function;
renderSectionHeaderEmpty: Function;
renderSectionHeaderNotEmpty: Function;
renderItemEmpty: Function;
renderItemNotEmpty: Function;
constructor(fetchUrl: string, refreshTime: number) {
super();
this.webDataManager = new WebDataManager(fetchUrl);
this.refreshTime = refreshTime;
// creating references to functions used in render()
this.onRefresh = this.onRefresh.bind(this);
this.renderSectionHeaderEmpty = this.renderSectionHeader.bind(this, true);
this.renderSectionHeaderNotEmpty = this.renderSectionHeader.bind(this, false);
this.renderItemEmpty = this.renderItem.bind(this, true);
this.renderItemNotEmpty = this.renderItem.bind(this, false);
}
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
return this.state.refreshing !== nextState.refreshing ||
nextState.firstLoading !== this.state.firstLoading ||
nextState.machinesWatched.length !== this.state.machinesWatched.length ||
nextState.fetchedData.len !== this.state.fetchedData.len;
}
/**
* Get the translation for the header in the current language
* @return {string}
*/
getHeaderTranslation(): string {
return "Header";
}
/**
* Get the translation for the toasts in the current language
* @return {string}
*/
getUpdateToastTranslations(): Array<string> {
return ["whoa", "nah"];
}
setMinTimeRefresh(value: number) {
this.minTimeBetweenRefresh = value;
}
/**
* Register react navigation events on first screen load.
* Allows to detect when the screen is focused
*/
componentDidMount() {
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus', this.onScreenFocus.bind(this));
this.willBlurSubscription = this.props.navigation.addListener(
'willBlur', this.onScreenBlur.bind(this));
}
/**
* Refresh data when focusing the screen and setup a refresh interval if asked to
*/
onScreenFocus() {
this.onRefresh();
if (this.refreshTime > 0)
this.refreshInterval = setInterval(this.onRefresh.bind(this), this.refreshTime)
}
/**
* Remove any interval on un-focus
*/
onScreenBlur() {
clearInterval(this.refreshInterval);
}
/**
* Unregister from event when un-mounting components
*/
componentWillUnmount() {
if (this.willBlurSubscription !== undefined)
this.willBlurSubscription.remove();
if (this.willFocusSubscription !== undefined)
this.willFocusSubscription.remove();
}
/**
* Refresh data and show a toast if any error occurred
* @private
*/
onRefresh() {
let canRefresh;
if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh;
else
canRefresh = true;
if (canRefresh) {
this.setState({refreshing: true});
this.webDataManager.readData()
.then((fetchedData) => {
this.setState({
fetchedData: fetchedData,
refreshing: false,
firstLoading: false
});
this.lastRefresh = new Date();
})
.catch(() => {
this.setState({
fetchedData: {},
refreshing: false,
firstLoading: false
});
this.webDataManager.showUpdateToast(this.getUpdateToastTranslations()[0], this.getUpdateToastTranslations()[1]);
});
}
}
/**
* Get the render item to be used for display in the list.
* Must be overridden by inheriting class.
*
* @param item
* @param section
* @return {*}
*/
getRenderItem(item: Object, section: Object) {
return <View/>;
}
/**
* Get the render item to be used for the section title in the list.
* Must be overridden by inheriting class.
*
* @param title
* @return {*}
*/
getRenderSectionHeader(title: string) {
return <View/>;
}
/**
* Get the render item to be used when the list is empty.
* No need to be overridden, has good defaults.
*
* @param text
* @param isSpinner
* @param icon
* @return {*}
*/
getEmptyRenderItem(text: string, isSpinner: boolean, icon: string) {
return (
<View>
<View style={{
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: 100,
marginBottom: 20
}}>
{isSpinner ?
<Spinner/>
:
<CustomMaterialIcon
icon={icon}
fontSize={100}
width={100}
color={ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText}/>}
</View>
<H3 style={{
textAlign: 'center',
marginRight: 20,
marginLeft: 20,
color: ThemeManager.getCurrentThemeVariables().fetchedDataSectionListErrorText
}}>
{text}
</H3>
</View>);
}
/**
* Create the dataset to be used in the list from the data fetched.
* Must be overridden.
*
* @param fetchedData {Object}
* @return {Array}
*/
createDataset(fetchedData: Object): Array<Object> {
return [];
}
datasetKeyExtractor(item: Object) {
return item.text
}
/**
* Create the dataset when no fetched data is available.
* No need to be overridden, has good defaults.
*
* @return
*/
createEmptyDataset() {
return [
{
title: '',
data: [
{
text: this.state.refreshing ?
i18n.t('general.loading') :
i18n.t('general.networkError'),
isSpinner: this.state.refreshing,
icon: this.state.refreshing ?
'refresh' :
'access-point-network-off'
}
],
keyExtractor: this.datasetKeyExtractor,
}
];
}
/**
* Should the app use a tab layout instead of a section list ?
* If yes, each section will be rendered in a new tab.
* Can be overridden.
*
* @return {boolean}
*/
hasTabs() {
return false;
}
hasBackButton() {
return false;
}
getRightButton() {
return <View/>
}
hasStickyHeader() {
return false;
}
hasSideMenu() {
return true;
}
renderSectionHeader(isEmpty: boolean, {section: {title}} : Object) {
return isEmpty ?
<View/> :
this.getRenderSectionHeader(title)
}
renderItem(isEmpty: boolean, {item, section}: Object) {
return isEmpty ?
this.getEmptyRenderItem(item.text, item.isSpinner, item.icon) :
this.getRenderItem(item, section)
}
/**
* Get the section list render using the generated dataset
*
* @param dataset
* @return
*/
getSectionList(dataset: Array<Object>) {
let isEmpty = dataset[0].data.length === 0;
if (isEmpty)
dataset = this.createEmptyDataset();
return (
<SectionList
sections={dataset}
stickySectionHeadersEnabled={this.hasStickyHeader()}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
/>
}
renderSectionHeader={isEmpty ? this.renderSectionHeaderEmpty : this.renderSectionHeaderNotEmpty}
renderItem={isEmpty ? this.renderItemEmpty : this.renderItemNotEmpty}
style={{minHeight: 300, width: '100%'}}
contentContainerStyle={
isEmpty ?
{flexGrow: 1, justifyContent: 'center', alignItems: 'center'} : {}
}
/>
);
}
/**
* Generate the tabs containing the lists
*
* @param dataset
* @return
*/
getTabbedView(dataset: Array<Object>) {
let tabbedView = [];
for (let i = 0; i < dataset.length; i++) {
tabbedView.push(
<Tab heading={
<TabHeading>
<CustomMaterialIcon
icon={dataset[i].icon}
color={ThemeManager.getCurrentThemeVariables().tabIconColor}
fontSize={20}
/>
<Text>{dataset[i].title}</Text>
</TabHeading>}
key={dataset[i].title}
style={{backgroundColor: ThemeManager.getCurrentThemeVariables().containerBgColor}}>
{this.getSectionList(
[
{
title: dataset[i].title,
data: dataset[i].data,
extraData: dataset[i].extraData,
keyExtractor: dataset[i].keyExtractor
}
]
)}
</Tab>);
}
return tabbedView;
}
render() {
// console.log("rendering FetchedDataSectionList");
const dataset = this.createDataset(this.state.fetchedData);
return (
<BaseContainer
navigation={this.props.navigation}
headerTitle={this.getHeaderTranslation()}
headerRightButton={this.getRightButton()}
hasTabs={this.hasTabs()}
hasBackButton={this.hasBackButton()}
hasSideMenu={this.hasSideMenu()}
>
{this.hasTabs() ?
<Tabs
tabContainerStyle={{
elevation: 0, // Fix for android shadow
}}
>
{this.getTabbedView(dataset)}
</Tabs>
:
this.getSectionList(dataset)
}
</BaseContainer>
);
}
}