jfilter/frag-den-staat-app

View on GitHub
src/containers/foiRequests/FoiRequestsMasterScreen.js

Summary

Maintainability
C
1 day
Test Coverage
import {
  Alert,
  Animated,
  FlatList,
  Platform,
  RefreshControl,
  Text,
  View,
} from 'react-native';
import { NavigationActions, DrawerActions } from 'react-navigation';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import cache from 'react-native-modest-cache';

import { LIST_HEADER_HEIGHT, styles } from './styles';
import {
  foiRequestsFetchData,
  foiRequestsRefreshData,
  foiRequestsErrorClearAction,
} from '../../actions/foiRequests';
import { primaryColor, primaryColorLight } from '../../globals/colors';
import { FOI_REQUESTS_PAGE_SIZE } from '../../globals';
import FoiRequestsListHeader from './FoiRequestsListHeader';
import ListFooter from '../../components/library/ListFooter';
import ListHeader from '../../components/library/ListHeader';
import ListItem from '../../components/library/ListItem';
import NavBarIcon from '../../components/foiRequests/NavBarIcon';
import Seperator from '../../components/library/Seperator';
import { clearCache } from '../../utils/networking';

const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);

class FoiRequestsMasterScreen extends React.Component {
  constructor(props) {
    super(props);

    const scrollAnim = new Animated.Value(0);
    const offsetAnim = new Animated.Value(0);

    this.state = {
      scrollAnim,
      offsetAnim,
      clampedScroll: Animated.diffClamp(
        Animated.add(
          scrollAnim.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 1],
            extrapolateLeft: 'clamp',
          }),
          offsetAnim
        ),
        0,
        LIST_HEADER_HEIGHT
      ),
    };
  }

  componentDidMount() {
    if (!this.props.onboardingFinished) {
      if (Platform.OS === 'android') {
        // has to done with some delay or otherwise it's broken on android
        setTimeout(() => {
          this.props.navigateToOnboarding();
        }, 200);
      } else {
        setTimeout(() => {
          this.props.navigateToOnboarding();
        }, 10);
      }
    }

    if (!this.props.isPending) {
      this.props.fetchData();
    }
  }

  componentDidUpdate() {
    // https://github.com/facebook/react-native/issues/1878
    if (
      Platform.OS === 'ios' &&
      this.props.requests.length <= FOI_REQUESTS_PAGE_SIZE &&
      !this.props.isRefreshing &&
      !(this.props.isPending && this.props.requests.length !== 0)
    ) {
      this.listRef.getNode().scrollToOffset({
        offset: -LIST_HEADER_HEIGHT,
        animated: false,
      });
    }
  }

  _fetchData = () => {
    if (!this.props.isPending) {
      // only fetch if there is more data available
      if (this.props.nResults > FOI_REQUESTS_PAGE_SIZE) {
        const timeDif = Date.now() - this.props.firstPageFetchedAt;
        // Prevent fetching data twice on initalizition because the `onEndReachEd` event fires with an empy list
        // value in mili seconds
        if (timeDif > 1000) {
          this.props.fetchData();
        }
      }
    }
  };

  _refreshData = () => {
    cache.flush(); // NB: This deletes the *whole* cache and not only for the current filter
    this.props.refreshData();
  };

  _renderNumberOfResultHeader = () => (
    <ListHeader numResults={this.props.nResults} />
  );

  _renderFooter = () => <ListFooter isPending={this.props.isPending} />;

  _renderItem = ({ item }) => {
    const onPress = () => this.props.navigateToSingle({ request: item });
    return <ListItem item={item} onPress={onPress} />;
  };

  render() {
    const { clampedScroll } = this.state;

    const headerTranslate = clampedScroll.interpolate({
      inputRange: [0, LIST_HEADER_HEIGHT],
      outputRange: [0, -LIST_HEADER_HEIGHT],
      extrapolate: 'clamp',
    });

    const headerOpacity = clampedScroll.interpolate({
      inputRange: [0, LIST_HEADER_HEIGHT],
      outputRange: [1, 0],
      extrapolate: 'clamp',
    });

    const hasError = this.props.error !== '';
    if (hasError) {
      const errorText = `Sorry, there has been an error with the message: '${
        this.props.error
      }'`;
      Alert.alert(
        'Error',
        errorText,
        [
          {
            text: 'OK',
            onPress: () => {
              this.props.clearError();
              clearCache();
            },
          },
        ],
        { cancelable: false }
      );
    }

    const androidContainerStyle =
      Platform.OS === 'android' ? { paddingTop: LIST_HEADER_HEIGHT } : null;

    return (
      <View style={styles.background}>
        <AnimatedFlatList
          refreshControl={
            <RefreshControl
              refreshing={this.props.isRefreshing}
              onRefresh={this._refreshData}
              progressViewOffset={LIST_HEADER_HEIGHT}
              tintColor={primaryColorLight}
              progressBackgroundColor={primaryColorLight}
            />
          }
          contentInset={
            { top: LIST_HEADER_HEIGHT } // progresViewOffset for anodroid
          }
          data={
            this.props.requests // iOS
          }
          extraData={this.props.isPending}
          contentContainerStyle={androidContainerStyle}
          renderItem={this._renderItem}
          onEndReached={this._fetchData}
          onEndReachedThreshold={0.5}
          ListFooterComponent={this._renderFooter}
          ListHeaderComponent={this._renderNumberOfResultHeader}
          ItemSeparatorComponent={Seperator}
          ref={ref => (this.listRef = ref)}
          onScroll={Animated.event(
            [
              {
                nativeEvent: {
                  contentOffset: { y: this.state.scrollAnim },
                },
              },
            ],
            { useNativeDriver: true }
          )}
          scrollEventThrottle={10}
          style={
            { backgroundColor: 'white' } // this fixes a bug with not appearing activity spinner
          }
        />
        <Animated.View
          style={[
            styles.header,
            { transform: [{ translateY: headerTranslate }] },
          ]}
        >
          <Animated.View style={[styles.title, { opacity: headerOpacity }]}>
            <FoiRequestsListHeader />
          </Animated.View>
        </Animated.View>
      </View>
    );
  }
}

// eslint-disable-next-line react/no-multi-comp
class MyIconLeft extends React.Component {
  navigateToFilter = () =>
    this.props.dispatch(
      DrawerActions.toggleDrawer({
        params: { filter: this.props.filter },
      })
    );
  render() {
    const { filterUser, currentUserId, filterFollower } = this.props;
    let secondIcon = null;

    if (filterUser != null) {
      if (currentUserId === filterUser) {
        secondIcon = { iconName: 'person' };
      } else {
        secondIcon = { iconName: 'wrench', iconType: 'font-awesome' };
      }
    } else {
      if (filterFollower != null) {
        secondIcon = { iconName: 'star' };
      }
    }

    return (
      <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
        <NavBarIcon
          onPress={this.navigateToFilter}
          iconName="menu"
          additionalStyles={{ marginHorizontal: 0, marginLeft: 5 }}
        />

        {secondIcon && (
          <NavBarIcon
            onPress={this.navigateToFilter}
            {...secondIcon}
            additionalStyles={{ marginHorizontal: 0, paddingHorizontal: 0 }}
            iconSize={20}
          />
        )}
      </View>
    );
  }
}

const MyConnectedIcon = connect(state => {
  return {
    filterUser: state.foiRequests.filter.user,
    filterFollower: state.foiRequests.filter.follower,
    currentUserId: state.authentication.userId,
  };
})(MyIconLeft);

FoiRequestsMasterScreen.navigationOptions = ({ navigation }) => {
  const navigateToOnboarding = () =>
    navigation.dispatch(
      NavigationActions.navigate({ routeName: 'FoiRequestsOnboarding' })
    );

  return {
    headerTitle: (
      <Text
        style={{
          fontFamily: 'Kreon-Bold',
          alignSelf: 'center',
          fontSize: Platform.OS === 'ios' ? 17 : 20,
          color: primaryColor,
          marginHorizontal: 16,
        }}
      >
        FragDenStaat
      </Text>
    ),
    headerBackTitle: null, // disables default
    headerRight: (
      <NavBarIcon onPress={navigateToOnboarding} iconName="info-outline" />
    ),
    headerLeft: <MyConnectedIcon />,
  };
};

FoiRequestsMasterScreen.propTypes = {
  // eslint-disable-next-line react/forbid-prop-types
  requests: PropTypes.array.isRequired,
  fetchData: PropTypes.func.isRequired,
  isRefreshing: PropTypes.bool.isRequired,
  isPending: PropTypes.bool.isRequired,
  nResults: PropTypes.number.isRequired,
  // eslint-disable-next-line react/require-default-props
  firstPageFetchedAt: PropTypes.number,
  error: PropTypes.string.isRequired,
  clearError: PropTypes.func.isRequired,
  refreshData: PropTypes.func.isRequired,
  navigateToSingle: PropTypes.func.isRequired,
  onboardingFinished: PropTypes.bool.isRequired,
};

const mapStateToProps = state => {
  return {
    ...state.foiRequests,
    onboardingFinished: state.settings.onboardingFinished,
  };
};

const mapDispatchToProps = dispatch => {
  return {
    clearError: () => dispatch(foiRequestsErrorClearAction()),
    fetchData: () => dispatch(foiRequestsFetchData()),
    refreshData: () => dispatch(foiRequestsRefreshData()),
    navigateToOnboarding: () =>
      dispatch(
        NavigationActions.navigate({ routeName: 'FoiRequestsOnboarding' })
      ),
    navigateToSingle: params =>
      dispatch(
        NavigationActions.navigate({ routeName: 'FoiRequestsDetails', params })
      ),
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(FoiRequestsMasterScreen);