diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2e02a794ed03..4af8fd161546 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import {Animated} from 'react-native'; +import React, {useCallback, useEffect, useState} from 'react'; +import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '../../../components/InvertedFlatList'; import withDrawerState, {withDrawerPropTypes} from '../../../components/withDrawerState'; @@ -17,7 +17,6 @@ import participantPropTypes from '../../../components/participantPropTypes'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import reportActionPropTypes from './reportActionPropTypes'; import CONST from '../../../CONST'; -import * as StyleUtils from '../../../styles/StyleUtils'; import reportPropTypes from '../../reportPropTypes'; import networkPropTypes from '../../../components/networkPropTypes'; import withLocalize from '../../../components/withLocalize'; @@ -64,147 +63,132 @@ const defaultProps = { isLoadingMoreReportActions: false, }; -class ReportActionsList extends React.Component { - constructor(props) { - super(props); - this.renderItem = this.renderItem.bind(this); - this.keyExtractor = this.keyExtractor.bind(this); - - this.state = { - fadeInAnimation: new Animated.Value(0), - skeletonViewHeight: 0, - }; - } - - componentDidMount() { - this.fadeIn(); - } - - fadeIn() { - Animated.timing(this.state.fadeInAnimation, { - toValue: 1, - duration: 100, - useNativeDriver: true, - }).start(); - } +/** + * Create a unique key for each action in the FlatList. + * We use the reportActionID that is a string representation of a random 64-bit int, which should be + * random enough to avoid collisions + * @param {Object} item + * @param {Object} item.action + * @return {String} + */ +function keyExtractor(item) { + return item.reportActionID; +} + +const ReportActionsList = (props) => { + const opacity = useSharedValue(0); + const animatedStyles = useAnimatedStyle(() => ({ + opacity: withTiming(opacity.value, {duration: 100}), + })); + useEffect(() => { + opacity.value = 1; + }, [opacity]); + const [skeletonViewHeight, setSkeletonViewHeight] = useState(0); + + const windowHeight = props.windowHeight; /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on * the height of the smallest report action possible. * @return {Number} */ - calculateInitialNumToRender() { + const calculateInitialNumToRender = useCallback(() => { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; - const availableHeight = this.props.windowHeight + const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); return Math.ceil(availableHeight / minimumReportActionHeight); - } + }, [windowHeight]); - /** - * Create a unique key for each action in the FlatList. - * We use the reportActionID that is a string representation of a random 64-bit int, which should be - * random enough to avoid collisions - * @param {Object} item - * @param {Object} item.action - * @return {String} - */ - keyExtractor(item) { - return item.reportActionID; - } + const report = props.report; + const hasOutstandingIOU = props.report.hasOutstandingIOU; + const newMarkerReportActionID = props.newMarkerReportActionID; + const sortedReportActions = props.sortedReportActions; + const mostRecentIOUReportActionID = props.mostRecentIOUReportActionID; /** - * Do not move this or make it an anonymous function it is a method - * so it will not be recreated each time we render an item - * - * See: https://reactnative.dev/docs/optimizing-flatlist-configuration#avoid-anonymous-function-on-renderitem - * * @param {Object} args * @param {Number} args.index - * * @returns {React.Component} */ - renderItem({ + const renderItem = useCallback(({ item: reportAction, index, - }) { + }) => { // When the new indicator should not be displayed we explicitly set it to null - const shouldDisplayNewMarker = reportAction.reportActionID === this.props.newMarkerReportActionID; + const shouldDisplayNewMarker = reportAction.reportActionID === newMarkerReportActionID; return ( <ReportActionItem - report={this.props.report} + report={report} action={reportAction} - displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(this.props.sortedReportActions, index)} + displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} shouldDisplayNewMarker={shouldDisplayNewMarker} - isMostRecentIOUReportAction={reportAction.reportActionID === this.props.mostRecentIOUReportActionID} - hasOutstandingIOU={this.props.report.hasOutstandingIOU} + isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID} + hasOutstandingIOU={hasOutstandingIOU} index={index} /> ); - } - - render() { - // Native mobile does not render updates flatlist the changes even though component did update called. - // To notify there something changes we can use extraData prop to flatlist - const extraData = (!this.props.isDrawerOpen && this.props.isSmallScreenWidth) ? this.props.newMarkerReportActionID : undefined; - const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(this.props.personalDetails, this.props.report); - return ( - <Animated.View style={[StyleUtils.fade(this.state.fadeInAnimation), styles.flex1]}> - <InvertedFlatList - accessibilityLabel={this.props.translate('sidebarScreen.listOfChatMessages')} - ref={ReportScrollManager.flatListRef} - data={this.props.sortedReportActions} - renderItem={this.renderItem} - contentContainerStyle={[ - styles.chatContentScrollView, - shouldShowReportRecipientLocalTime && styles.pt0, - ]} - keyExtractor={this.keyExtractor} - initialRowHeight={32} - initialNumToRender={this.calculateInitialNumToRender()} - onEndReached={this.props.loadMoreChats} - onEndReachedThreshold={0.75} - ListFooterComponent={() => { - if (this.props.report.isLoadingMoreReportActions) { - return ( - <ReportActionsSkeletonView - containerHeight={CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT * 3} - /> - ); - } - - // Make sure the oldest report action loaded is not the first. This is so we do not show the - // skeleton view above the created action in a newly generated optimistic chat or one with not - // that many comments. - const lastReportAction = _.last(this.props.sortedReportActions) || {}; - if (this.props.report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ( - <ReportActionsSkeletonView - containerHeight={this.state.skeletonViewHeight} - animate={!this.props.network.isOffline} - /> - ); - } - - return null; - }} - keyboardShouldPersistTaps="handled" - onLayout={(event) => { - this.setState({ - skeletonViewHeight: event.nativeEvent.layout.height, - }); - this.props.onLayout(event); - }} - onScroll={this.props.onScroll} - extraData={extraData} - /> - </Animated.View> - ); - } -} + }, [report, hasOutstandingIOU, newMarkerReportActionID, sortedReportActions, mostRecentIOUReportActionID]); + + // Native mobile does not render updates flatlist the changes even though component did update called. + // To notify there something changes we can use extraData prop to flatlist + const extraData = (!props.isDrawerOpen && props.isSmallScreenWidth) ? props.newMarkerReportActionID : undefined; + const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(props.personalDetails, props.report); + return ( + <Animated.View style={[animatedStyles, styles.flex1]}> + <InvertedFlatList + accessibilityLabel={props.translate('sidebarScreen.listOfChatMessages')} + ref={ReportScrollManager.flatListRef} + data={props.sortedReportActions} + renderItem={renderItem} + contentContainerStyle={[ + styles.chatContentScrollView, + shouldShowReportRecipientLocalTime && styles.pt0, + ]} + keyExtractor={keyExtractor} + initialRowHeight={32} + initialNumToRender={calculateInitialNumToRender()} + onEndReached={props.loadMoreChats} + onEndReachedThreshold={0.75} + ListFooterComponent={() => { + if (props.report.isLoadingMoreReportActions) { + return ( + <ReportActionsSkeletonView + containerHeight={CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT * 3} + /> + ); + } + + // Make sure the oldest report action loaded is not the first. This is so we do not show the + // skeleton view above the created action in a newly generated optimistic chat or one with not + // that many comments. + const lastReportAction = _.last(props.sortedReportActions) || {}; + if (props.report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return ( + <ReportActionsSkeletonView + containerHeight={skeletonViewHeight} + animate={!props.network.isOffline} + /> + ); + } + + return null; + }} + keyboardShouldPersistTaps="handled" + onLayout={(event) => { + setSkeletonViewHeight(event.nativeEvent.layout.height); + props.onLayout(event); + }} + onScroll={props.onScroll} + extraData={extraData} + /> + </Animated.View> + ); +}; ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; +ReportActionsList.displayName = 'ReportActionsList'; export default compose( withDrawerState, diff --git a/src/setup/index.js b/src/setup/index.js index 352417242ade..eae7036331c9 100644 --- a/src/setup/index.js +++ b/src/setup/index.js @@ -53,4 +53,11 @@ export default function () { // Perform any other platform-specific setup platformSetup(); + + // Workaround to a reanimated issue -> https://github.com/software-mansion/react-native-reanimated/issues/3355 + // We can remove it as soon as we are on > reanimated 3.0.0+ + if (process.browser) { + // eslint-disable-next-line no-underscore-dangle + window._frameTimestamp = null; + } }