<commit_before>// @flow

import {
  type EntryInfo,
  entryInfoPropType,
  type CalendarQuery,
  type CalendarQueryUpdateResult,
} from 'lib/types/entry-types';
import type { AppState } from '../redux/redux-setup';
import type {
  CalendarItem,
  SectionHeaderItem,
  SectionFooterItem,
  LoaderItem,
} from '../selectors/calendar-selectors';
import type { ViewToken } from '../types/react-native';
import type { DispatchActionPromise } from 'lib/utils/action-utils';
import type { KeyboardEvent } from '../keyboard/keyboard';
import type { TextToMeasure } from '../text-height-measurer.react';
import type { NavigationScreenProp, NavigationRoute } from 'react-navigation';
import {
  type CalendarFilter,
  calendarFilterPropType,
} from 'lib/types/filter-types';
import { type Dimensions, dimensionsPropType } from 'lib/types/media-types';
import {
  type LoadingStatus,
  loadingStatusPropType,
} from 'lib/types/loading-types';
import {
  type ConnectionStatus,
  connectionStatusPropType,
} from 'lib/types/socket-types';
import { type ThreadInfo, threadInfoPropType } from 'lib/types/thread-types';

import * as React from 'react';
import {
  View,
  Text,
  FlatList,
  AppState as NativeAppState,
  Platform,
  LayoutAnimation,
  TouchableWithoutFeedback,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import PropTypes from 'prop-types';
import invariant from 'invariant';
import _findIndex from 'lodash/fp/findIndex';
import _isEqual from 'lodash/fp/isEqual';
import _map from 'lodash/fp/map';
import _find from 'lodash/fp/find';
import _differenceWith from 'lodash/fp/differenceWith';
import _filter from 'lodash/fp/filter';
import _sum from 'lodash/fp/sum';
import _pickBy from 'lodash/fp/pickBy';
import _size from 'lodash/fp/size';
import _throttle from 'lodash/throttle';

import { entryKey } from 'lib/shared/entry-utils';
import { dateString, prettyDate, dateFromString } from 'lib/utils/date-utils';
import {
  updateCalendarQueryActionTypes,
  updateCalendarQuery,
} from 'lib/actions/entry-actions';
import { connect } from 'lib/utils/redux-utils';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';

import { Entry, InternalEntry, entryStyles } from './entry.react';
import {
  dimensionsSelector,
  contentVerticalOffsetSelector,
  tabBarSize,
} from '../selectors/dimension-selectors';
import { calendarListData } from '../selectors/calendar-selectors';
import {
  createIsForegroundSelector,
  createActiveTabSelector,
} from '../navigation/nav-selectors';
import TextHeightMeasurer from '../text-height-measurer.react';
import ListLoadingIndicator from '../components/list-loading-indicator.react';
import SectionFooter from './section-footer.react';
import CalendarInputBar from './calendar-input-bar.react';
import {
  addKeyboardShowListener,
  addKeyboardDismissListener,
  addKeyboardDidDismissListener,
  removeKeyboardListener,
} from '../keyboard/keyboard';
import KeyboardAvoidingView from '../keyboard/keyboard-avoiding-view.react';
import {
  CalendarRouteName,
  ThreadPickerModalRouteName,
} from '../navigation/route-names';
import DisconnectedBar from '../navigation/disconnected-bar.react';
import SafeAreaView from '../components/safe-area-view.react';
import {
  type Colors,
  colorsPropType,
  colorsSelector,
  styleSelector,
} from '../themes/colors';
import ContentLoading from '../components/content-loading.react';
import {
  connectNav,
  type NavContextType,
} from '../navigation/navigation-context';

export type EntryInfoWithHeight = {|
  ...EntryInfo,
  textHeight: number,
|};
type CalendarItemWithHeight =
  | LoaderItem
  | SectionHeaderItem
  | SectionFooterItem
  | {|
      itemType: 'entryInfo',
      entryInfo: EntryInfoWithHeight,
      threadInfo: ThreadInfo,
    |};
type ExtraData = $ReadOnly<{|
  activeEntries: { [key: string]: boolean },
  visibleEntries: { [key: string]: boolean },
|}>;

// This is v sad :(
// Overall, I have to say, this component is probably filled with more sadness
// than any other component in this project. This owes mostly to its complex
// infinite-scrolling behavior.
// But not this particular piece of sadness, actually. We have to cache the
// current Calendar ref here so we can access it from the statically defined
// navigationOptions.tabBarOnPress below.
let currentCalendarRef: ?Calendar = null;

type Props = {
  navigation: NavigationScreenProp<NavigationRoute>,
  route: NavigationRoute,
  // Redux state
  listData: ?$ReadOnlyArray<CalendarItem>,
  calendarActive: boolean,
  startDate: string,
  endDate: string,
  calendarFilters: $ReadOnlyArray<CalendarFilter>,
  dimensions: Dimensions,
  contentVerticalOffset: number,
  loadingStatus: LoadingStatus,
  connectionStatus: ConnectionStatus,
  colors: Colors,
  styles: typeof styles,
  // Redux dispatch functions
  dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  updateCalendarQuery: (
    calendarQuery: CalendarQuery,
    reduxAlreadyUpdated?: boolean,
  ) => Promise<CalendarQueryUpdateResult>,
};
type State = {|
  textToMeasure: TextToMeasure[],
  listDataWithHeights: ?$ReadOnlyArray<CalendarItemWithHeight>,
  readyToShowList: boolean,
  extraData: ExtraData,
|};
class Calendar extends React.PureComponent<Props, State> {
  static propTypes = {
    navigation: PropTypes.shape({
      navigate: PropTypes.func.isRequired,
    }).isRequired,
    route: PropTypes.shape({
      key: PropTypes.string.isRequired,
    }).isRequired,
    listData: PropTypes.arrayOf(
      PropTypes.oneOfType([
        PropTypes.shape({
          itemType: PropTypes.oneOf(['loader']),
          key: PropTypes.string.isRequired,
        }),
        PropTypes.shape({
          itemType: PropTypes.oneOf(['header']),
          dateString: PropTypes.string.isRequired,
        }),
        PropTypes.shape({
          itemType: PropTypes.oneOf(['entryInfo']),
          entryInfo: entryInfoPropType.isRequired,
          threadInfo: threadInfoPropType.isRequired,
        }),
        PropTypes.shape({
          itemType: PropTypes.oneOf(['footer']),
          dateString: PropTypes.string.isRequired,
        }),
      ]),
    ),
    calendarActive: PropTypes.bool.isRequired,
    startDate: PropTypes.string.isRequired,
    endDate: PropTypes.string.isRequired,
    calendarFilters: PropTypes.arrayOf(calendarFilterPropType).isRequired,
    dimensions: dimensionsPropType.isRequired,
    contentVerticalOffset: PropTypes.number.isRequired,
    loadingStatus: loadingStatusPropType.isRequired,
    connectionStatus: connectionStatusPropType.isRequired,
    colors: colorsPropType.isRequired,
    styles: PropTypes.objectOf(PropTypes.object).isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    updateCalendarQuery: PropTypes.func.isRequired,
  };
  static navigationOptions = {
    tabBarLabel: 'Calendar',
    tabBarIcon: ({ tintColor }) => (
      <Icon name="calendar" style={[styles.icon, { color: tintColor }]} />
    ),
    tabBarOnPress: ({
      navigation,
      defaultHandler,
    }: {
      navigation: NavigationScreenProp<NavigationRoute>,
      defaultHandler: () => void,
    }) => {
      if (!navigation.isFocused()) {
        defaultHandler();
      } else if (currentCalendarRef) {
        currentCalendarRef.scrollToToday();
      }
    },
  };
  flatList: ?FlatList<CalendarItemWithHeight> = null;
  textHeights: ?Map<string, number> = null;
  currentState: ?string = NativeAppState.currentState;
  lastForegrounded = 0;
  lastCalendarReset = 0;
  currentScrollPosition: ?number = null;
  // We don't always want an extraData update to trigger a state update, so we
  // cache the most recent value as a member here
  latestExtraData: ExtraData;
  // For some reason, we have to delay the scrollToToday call after the first
  // scroll upwards on Android. I don't know why. Might only be on the emulator.
  firstScrollUpOnAndroidComplete = false;
  // When an entry becomes active, we make a note of its key so that once the
  // keyboard event happens, we know where to move the scrollPos to
  lastEntryKeyActive: ?string = null;
  keyboardShowListener: ?{ +remove: () => void };
  keyboardDismissListener: ?{ +remove: () => void };
  keyboardShownHeight: ?number = null;
  // If the query fails, we try it again
  topLoadingFromScroll: ?CalendarQuery = null;
  bottomLoadingFromScroll: ?CalendarQuery = null;
  // We wait until the loaders leave view before letting them be triggered again
  topLoaderWaitingToLeaveView = true;
  bottomLoaderWaitingToLeaveView = true;
  // We keep refs to the entries so CalendarInputBar can save them
  entryRefs = new Map();
  currentlyEditing = new Set();

  constructor(props: Props) {
    super(props);
    const textToMeasure = props.listData
      ? Calendar.textToMeasureFromListData(props.listData)
      : [];
    this.latestExtraData = {
      activeEntries: {},
      visibleEntries: {},
    };
    this.state = {
      textToMeasure,
      listDataWithHeights: null,
      readyToShowList: false,
      extraData: this.latestExtraData,
    };
    currentCalendarRef = this;
  }

  static textToMeasureFromListData(listData: $ReadOnlyArray<CalendarItem>) {
    const textToMeasure = [];
    for (let item of listData) {
      if (item.itemType !== 'entryInfo') {
        continue;
      }
      textToMeasure.push({
        id: entryKey(item.entryInfo),
        text: item.entryInfo.text,
      });
    }
    return textToMeasure;
  }

  componentDidMount() {
    NativeAppState.addEventListener('change', this.handleAppStateChange);
    this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow);
    this.keyboardDismissListener = addKeyboardDismissListener(
      this.keyboardDismiss,
    );
  }

  componentWillUnmount() {
    NativeAppState.removeEventListener('change', this.handleAppStateChange);
    if (this.keyboardShowListener) {
      removeKeyboardListener(this.keyboardShowListener);
      this.keyboardShowListener = null;
    }
    if (this.keyboardDismissListener) {
      removeKeyboardListener(this.keyboardDismissListener);
      this.keyboardDismissListener = null;
    }
  }

  handleAppStateChange = (nextAppState: ?string) => {
    const lastState = this.currentState;
    this.currentState = nextAppState;
    if (
      !lastState ||
      !lastState.match(/inactive|background/) ||
      this.currentState !== 'active'
    ) {
      // We're only handling foregrounding here
      return;
    }
    if (Date.now() - this.lastCalendarReset < 500) {
      // If the calendar got reset right before this callback triggered, that
      // indicates we should reset the scroll position
      this.lastCalendarReset = 0;
      this.scrollToToday(false);
    } else {
      // Otherwise, it's possible that the calendar is about to get reset. We
      // record a timestamp here so we can scrollToToday there.
      this.lastForegrounded = Date.now();
    }
  };

  componentDidUpdate(prevProps: Props, prevState: State) {
    if (this.props.listData !== prevProps.listData) {
      this.handleNewTextToMeasure();
    }

    const { loadingStatus, connectionStatus } = this.props;
    const {
      loadingStatus: prevLoadingStatus,
      connectionStatus: prevConnectionStatus,
    } = prevProps;
    if (
      (loadingStatus === 'error' && prevLoadingStatus === 'loading') ||
      (connectionStatus === 'connected' && prevConnectionStatus !== 'connected')
    ) {
      this.loadMoreAbove();<commit_msg>[native] Move Calendar center-on-tab-press behavior to event listener<commit_after>// @flow

import {
  type EntryInfo,
  entryInfoPropType,
  type CalendarQuery,
  type CalendarQueryUpdateResult,
} from 'lib/types/entry-types';
import type { AppState } from '../redux/redux-setup';
import type {
  CalendarItem,
  SectionHeaderItem,
  SectionFooterItem,
  LoaderItem,
} from '../selectors/calendar-selectors';
import type { ViewToken } from '../types/react-native';
import type { DispatchActionPromise } from 'lib/utils/action-utils';
import type { KeyboardEvent } from '../keyboard/keyboard';
import type { TextToMeasure } from '../text-height-measurer.react';
import type { NavigationScreenProp, NavigationRoute } from 'react-navigation';
import {
  type CalendarFilter,
  calendarFilterPropType,
} from 'lib/types/filter-types';
import { type Dimensions, dimensionsPropType } from 'lib/types/media-types';
import {
  type LoadingStatus,
  loadingStatusPropType,
} from 'lib/types/loading-types';
import {
  type ConnectionStatus,
  connectionStatusPropType,
} from 'lib/types/socket-types';
import { type ThreadInfo, threadInfoPropType } from 'lib/types/thread-types';

import * as React from 'react';
import {
  View,
  Text,
  FlatList,
  AppState as NativeAppState,
  Platform,
  LayoutAnimation,
  TouchableWithoutFeedback,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import PropTypes from 'prop-types';
import invariant from 'invariant';
import _findIndex from 'lodash/fp/findIndex';
import _isEqual from 'lodash/fp/isEqual';
import _map from 'lodash/fp/map';
import _find from 'lodash/fp/find';
import _differenceWith from 'lodash/fp/differenceWith';
import _filter from 'lodash/fp/filter';
import _sum from 'lodash/fp/sum';
import _pickBy from 'lodash/fp/pickBy';
import _size from 'lodash/fp/size';
import _throttle from 'lodash/throttle';

import { entryKey } from 'lib/shared/entry-utils';
import { dateString, prettyDate, dateFromString } from 'lib/utils/date-utils';
import {
  updateCalendarQueryActionTypes,
  updateCalendarQuery,
} from 'lib/actions/entry-actions';
import { connect } from 'lib/utils/redux-utils';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';

import { Entry, InternalEntry, entryStyles } from './entry.react';
import {
  dimensionsSelector,
  contentVerticalOffsetSelector,
  tabBarSize,
} from '../selectors/dimension-selectors';
import { calendarListData } from '../selectors/calendar-selectors';
import {
  createIsForegroundSelector,
  createActiveTabSelector,
} from '../navigation/nav-selectors';
import TextHeightMeasurer from '../text-height-measurer.react';
import ListLoadingIndicator from '../components/list-loading-indicator.react';
import SectionFooter from './section-footer.react';
import CalendarInputBar from './calendar-input-bar.react';
import {
  addKeyboardShowListener,
  addKeyboardDismissListener,
  addKeyboardDidDismissListener,
  removeKeyboardListener,
} from '../keyboard/keyboard';
import KeyboardAvoidingView from '../keyboard/keyboard-avoiding-view.react';
import {
  CalendarRouteName,
  ThreadPickerModalRouteName,
} from '../navigation/route-names';
import DisconnectedBar from '../navigation/disconnected-bar.react';
import SafeAreaView from '../components/safe-area-view.react';
import {
  type Colors,
  colorsPropType,
  colorsSelector,
  styleSelector,
} from '../themes/colors';
import ContentLoading from '../components/content-loading.react';
import {
  connectNav,
  type NavContextType,
} from '../navigation/navigation-context';

export type EntryInfoWithHeight = {|
  ...EntryInfo,
  textHeight: number,
|};
type CalendarItemWithHeight =
  | LoaderItem
  | SectionHeaderItem
  | SectionFooterItem
  | {|
      itemType: 'entryInfo',
      entryInfo: EntryInfoWithHeight,
      threadInfo: ThreadInfo,
    |};
type ExtraData = $ReadOnly<{|
  activeEntries: { [key: string]: boolean },
  visibleEntries: { [key: string]: boolean },
|}>;

type Props = {
  navigation: NavigationScreenProp<NavigationRoute>,
  route: NavigationRoute,
  // Redux state
  listData: ?$ReadOnlyArray<CalendarItem>,
  calendarActive: boolean,
  startDate: string,
  endDate: string,
  calendarFilters: $ReadOnlyArray<CalendarFilter>,
  dimensions: Dimensions,
  contentVerticalOffset: number,
  loadingStatus: LoadingStatus,
  connectionStatus: ConnectionStatus,
  colors: Colors,
  styles: typeof styles,
  // Redux dispatch functions
  dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  updateCalendarQuery: (
    calendarQuery: CalendarQuery,
    reduxAlreadyUpdated?: boolean,
  ) => Promise<CalendarQueryUpdateResult>,
};
type State = {|
  textToMeasure: TextToMeasure[],
  listDataWithHeights: ?$ReadOnlyArray<CalendarItemWithHeight>,
  readyToShowList: boolean,
  extraData: ExtraData,
|};
class Calendar extends React.PureComponent<Props, State> {
  static propTypes = {
    navigation: PropTypes.shape({
      navigate: PropTypes.func.isRequired,
      addListener: PropTypes.func.isRequired,
      removeListener: PropTypes.func.isRequired,
      isFocused: PropTypes.func.isRequired,
    }).isRequired,
    route: PropTypes.shape({
      key: PropTypes.string.isRequired,
    }).isRequired,
    listData: PropTypes.arrayOf(
      PropTypes.oneOfType([
        PropTypes.shape({
          itemType: PropTypes.oneOf(['loader']),
          key: PropTypes.string.isRequired,
        }),
        PropTypes.shape({
          itemType: PropTypes.oneOf(['header']),
          dateString: PropTypes.string.isRequired,
        }),
        PropTypes.shape({
          itemType: PropTypes.oneOf(['entryInfo']),
          entryInfo: entryInfoPropType.isRequired,
          threadInfo: threadInfoPropType.isRequired,
        }),
        PropTypes.shape({
          itemType: PropTypes.oneOf(['footer']),
          dateString: PropTypes.string.isRequired,
        }),
      ]),
    ),
    calendarActive: PropTypes.bool.isRequired,
    startDate: PropTypes.string.isRequired,
    endDate: PropTypes.string.isRequired,
    calendarFilters: PropTypes.arrayOf(calendarFilterPropType).isRequired,
    dimensions: dimensionsPropType.isRequired,
    contentVerticalOffset: PropTypes.number.isRequired,
    loadingStatus: loadingStatusPropType.isRequired,
    connectionStatus: connectionStatusPropType.isRequired,
    colors: colorsPropType.isRequired,
    styles: PropTypes.objectOf(PropTypes.object).isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    updateCalendarQuery: PropTypes.func.isRequired,
  };
  static navigationOptions = {
    tabBarLabel: 'Calendar',
    tabBarIcon: ({ tintColor }) => (
      <Icon name="calendar" style={[styles.icon, { color: tintColor }]} />
    ),
  };
  flatList: ?FlatList<CalendarItemWithHeight> = null;
  textHeights: ?Map<string, number> = null;
  currentState: ?string = NativeAppState.currentState;
  lastForegrounded = 0;
  lastCalendarReset = 0;
  currentScrollPosition: ?number = null;
  // We don't always want an extraData update to trigger a state update, so we
  // cache the most recent value as a member here
  latestExtraData: ExtraData;
  // For some reason, we have to delay the scrollToToday call after the first
  // scroll upwards on Android. I don't know why. Might only be on the emulator.
  firstScrollUpOnAndroidComplete = false;
  // When an entry becomes active, we make a note of its key so that once the
  // keyboard event happens, we know where to move the scrollPos to
  lastEntryKeyActive: ?string = null;
  keyboardShowListener: ?{ +remove: () => void };
  keyboardDismissListener: ?{ +remove: () => void };
  keyboardShownHeight: ?number = null;
  // If the query fails, we try it again
  topLoadingFromScroll: ?CalendarQuery = null;
  bottomLoadingFromScroll: ?CalendarQuery = null;
  // We wait until the loaders leave view before letting them be triggered again
  topLoaderWaitingToLeaveView = true;
  bottomLoaderWaitingToLeaveView = true;
  // We keep refs to the entries so CalendarInputBar can save them
  entryRefs = new Map();
  currentlyEditing = new Set();

  constructor(props: Props) {
    super(props);
    const textToMeasure = props.listData
      ? Calendar.textToMeasureFromListData(props.listData)
      : [];
    this.latestExtraData = {
      activeEntries: {},
      visibleEntries: {},
    };
    this.state = {
      textToMeasure,
      listDataWithHeights: null,
      readyToShowList: false,
      extraData: this.latestExtraData,
    };
  }

  static textToMeasureFromListData(listData: $ReadOnlyArray<CalendarItem>) {
    const textToMeasure = [];
    for (let item of listData) {
      if (item.itemType !== 'entryInfo') {
        continue;
      }
      textToMeasure.push({
        id: entryKey(item.entryInfo),
        text: item.entryInfo.text,
      });
    }
    return textToMeasure;
  }

  componentDidMount() {
    NativeAppState.addEventListener('change', this.handleAppStateChange);
    this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow);
    this.keyboardDismissListener = addKeyboardDismissListener(
      this.keyboardDismiss,
    );
    this.props.navigation.addListener('tabPress', this.onTabPress);
  }

  componentWillUnmount() {
    NativeAppState.removeEventListener('change', this.handleAppStateChange);
    if (this.keyboardShowListener) {
      removeKeyboardListener(this.keyboardShowListener);
      this.keyboardShowListener = null;
    }
    if (this.keyboardDismissListener) {
      removeKeyboardListener(this.keyboardDismissListener);
      this.keyboardDismissListener = null;
    }
    this.props.navigation.removeListener('tabPress', this.onTabPress);
  }

  handleAppStateChange = (nextAppState: ?string) => {
    const lastState = this.currentState;
    this.currentState = nextAppState;
    if (
      !lastState ||
      !lastState.match(/inactive|background/) ||
      this.currentState !== 'active'
    ) {
      // We're only handling foregrounding here
      return;
    }
    if (Date.now() - this.lastCalendarReset < 500) {
      // If the calendar got reset right before this callback triggered, that
      // indicates we should reset the scroll position
      this.lastCalendarReset = 0;
      this.scrollToToday(false);
    } else {
      // Otherwise, it's possible that the calendar is about to get reset. We
      // record a timestamp here so we can scrollToToday there.
      this.lastForegrounded = Date.now();
    }
  };

  onTabPress = () => {
    if (this.props.navigation.isFocused()) {
      this.scrollToToday();
    }
  };

  componentDidUpdate(prevProps: Props, prevState: State) {
    if (this.props.listData !== prevProps.listData) {
      this.handleNewTextToMeasure();
    }

    const { loadingStatus, connectionStatus } = this.props;
    const {
      loadingStatus: prevLoadingStatus,
      connectionStatus: prevConnectionStatus,
    } = prevProps;
    if (
      (loadingStatus === 'error' && prevLoadingStatus === 'loading') ||
      (connectionStatus === 'connected' && prevConnectionStatus !== 'connected')
    ) {
      this.loadMoreAbove();
{'fraction_non_alphanumeric': 0.1097690469869923, 'fraction_numerical': 0.0005309264666843643, 'mean_word_length': 3.8850226928895615, 'pattern_counts': {'":': 0, '<': 28, '<?xml version=': 0, '>': 39, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '106610', 'n_tokens_mistral': 6476, 'n_tokens_neox': 6435, 'n_words': 1558}