diff --git a/src/components/CopySelectionHelper.js b/src/components/CopySelectionHelper.js deleted file mode 100644 index 5f00bab3146b..000000000000 --- a/src/components/CopySelectionHelper.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import CONST from '../CONST'; -import KeyboardShortcut from '../libs/KeyboardShortcut'; -import Clipboard from '../libs/Clipboard'; -import SelectionScraper from '../libs/SelectionScraper'; - -class CopySelectionHelper extends React.Component { - componentDidMount() { - const copyShortcutConfig = CONST.KEYBOARD_SHORTCUTS.COPY; - this.unsubscribeCopyShortcut = KeyboardShortcut.subscribe( - copyShortcutConfig.shortcutKey, - this.copySelectionToClipboard, - copyShortcutConfig.descriptionKey, - copyShortcutConfig.modifiers, - false, - ); - } - - componentWillUnmount() { - if (!this.unsubscribeCopyShortcut) { - return; - } - - this.unsubscribeCopyShortcut(); - } - - copySelectionToClipboard() { - const selectionMarkdown = SelectionScraper.getAsMarkdown(); - Clipboard.setString(selectionMarkdown); - } - - render() { - return null; - } -} - -export default CopySelectionHelper; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js deleted file mode 100644 index 2648769e0a57..000000000000 --- a/src/pages/home/report/ReportActionsList.js +++ /dev/null @@ -1,189 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {ActivityIndicator, View} from 'react-native'; -import InvertedFlatList from '../../../components/InvertedFlatList'; -import withDrawerState, {withDrawerPropTypes} from '../../../components/withDrawerState'; -import compose from '../../../libs/compose'; -import * as ReportScrollManager from '../../../libs/ReportScrollManager'; -import styles from '../../../styles/styles'; -import themeColors from '../../../styles/themes/default'; -import * as ReportUtils from '../../../libs/ReportUtils'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import {withPersonalDetails} from '../../../components/OnyxProvider'; -import ReportActionItem from './ReportActionItem'; -import variables from '../../../styles/variables'; -import participantPropTypes from '../../../components/participantPropTypes'; -import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; -import reportActionPropTypes from './reportActionPropTypes'; - -const propTypes = { - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** The report currently being looked at */ - report: PropTypes.shape({ - /** Number of actions unread */ - unreadActionCount: PropTypes.number, - - /** The largest sequenceNumber on this report */ - maxSequenceNumber: PropTypes.number, - - /** The current position of the new marker */ - newMarkerSequenceNumber: PropTypes.number, - - /** Whether there is an outstanding amount in IOU */ - hasOutstandingIOU: PropTypes.bool, - }).isRequired, - - /** Sorted actions prepared for display */ - sortedReportActions: PropTypes.arrayOf(PropTypes.shape({ - /** Index of the action in the array */ - index: PropTypes.number, - - /** The action itself */ - action: PropTypes.shape(reportActionPropTypes), - })).isRequired, - - /** The sequence number of the most recent IOU report connected with the shown report */ - mostRecentIOUReportSequenceNumber: PropTypes.number, - - /** Are we loading more report actions? */ - isLoadingReportActions: PropTypes.bool.isRequired, - - /** Callback executed on list layout */ - onLayout: PropTypes.func.isRequired, - - /** Callback executed on scroll */ - onScroll: PropTypes.func.isRequired, - - ...withDrawerPropTypes, - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - personalDetails: {}, - mostRecentIOUReportSequenceNumber: undefined, -}; - -class ReportActionsList extends React.Component { - constructor(props) { - super(props); - this.renderItem = this.renderItem.bind(this); - this.renderCell = this.renderCell.bind(this); - this.keyExtractor = this.keyExtractor.bind(this); - } - - /** - * 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 minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom - + variables.fontSizeNormalHeight; - const availableHeight = this.props.windowHeight - - (styles.chatFooter.minHeight + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); - } - - /** - * Create a unique key for Each Action in the FlatList. - * We use a combination of sequenceNumber and clientID in case the clientID are the same - which - * shouldn't happen, but might be possible in some rare cases. - * @param {Object} item - * @return {String} - */ - keyExtractor(item) { - return `${item.action.sequenceNumber}${item.action.clientID}`; - } - - /** - * 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 {Object} args.item - * @param {Number} args.index - * - * @returns {React.Component} - */ - renderItem({ - item, - index, - }) { - const shouldDisplayNewIndicator = this.props.report.newMarkerSequenceNumber > 0 - && item.action.sequenceNumber === this.props.report.newMarkerSequenceNumber; - return ( - - ); - } - - /** - * This function overrides the CellRendererComponent (defaults to a plain View), giving each ReportActionItem a - * higher z-index than the one below it. This prevents issues where the ReportActionContextMenu overlapping between - * rows is hidden beneath other rows. - * - * @param {Object} index - The ReportAction item in the FlatList. - * @param {Object|Array} style – The default styles of the CellRendererComponent provided by the CellRenderer. - * @param {Object} props – All the other Props provided to the CellRendererComponent by default. - * @returns {React.Component} - */ - renderCell({item, style, ...props}) { - const cellStyle = [ - style, - {zIndex: item.action.sequenceNumber}, - ]; - // eslint-disable-next-line react/jsx-props-no-spreading - return ; - } - - 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.report.newMarkerSequenceNumber : undefined; - const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(this.props.personalDetails, this.props.report); - return ( - - : null} - keyboardShouldPersistTaps="handled" - onLayout={this.props.onLayout} - onScroll={this.props.onScroll} - extraData={extraData} - /> - ); - } -} - -ReportActionsList.propTypes = propTypes; -ReportActionsList.defaultProps = defaultProps; - -export default compose( - withDrawerState, - withWindowDimensions, - withPersonalDetails(), -)(ReportActionsList); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 877868f0cd82..c6689dca247e 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,18 +1,27 @@ import React from 'react'; import { + View, Keyboard, AppState, + ActivityIndicator, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; +import Clipboard from '../../../libs/Clipboard'; import * as Report from '../../../libs/actions/Report'; +import KeyboardShortcut from '../../../libs/KeyboardShortcut'; +import SelectionScraper from '../../../libs/SelectionScraper'; +import ReportActionItem from './ReportActionItem'; +import styles from '../../../styles/styles'; import reportActionPropTypes from './reportActionPropTypes'; +import InvertedFlatList from '../../../components/InvertedFlatList'; import * as CollectionUtils from '../../../libs/CollectionUtils'; import Visibility from '../../../libs/Visibility'; import Timing from '../../../libs/actions/Timing'; import CONST from '../../../CONST'; +import themeColors from '../../../styles/themes/default'; import compose from '../../../libs/compose'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import withDrawerState, {withDrawerPropTypes} from '../../../components/withDrawerState'; @@ -21,15 +30,16 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import PopoverReportActionContextMenu from './ContextMenu/PopoverReportActionContextMenu'; +import variables from '../../../styles/variables'; import Performance from '../../../libs/Performance'; +import * as ReportUtils from '../../../libs/ReportUtils'; import ONYXKEYS from '../../../ONYXKEYS'; -import {withNetwork} from '../../../components/OnyxProvider'; +import {withNetwork, withPersonalDetails} from '../../../components/OnyxProvider'; +import participantPropTypes from '../../../components/participantPropTypes'; +import EmojiPicker from '../../../components/EmojiPicker/EmojiPicker'; import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction'; import FloatingMessageCounter from './FloatingMessageCounter'; import networkPropTypes from '../../../components/networkPropTypes'; -import ReportActionsList from './ReportActionsList'; -import CopySelectionHelper from '../../../components/CopySelectionHelper'; -import EmojiPicker from '../../../components/EmojiPicker/EmojiPicker'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; const propTypes = { @@ -68,6 +78,9 @@ const propTypes = { /** Are we waiting for more report data? */ isLoadingReportData: PropTypes.bool, + /** Personal details of all the users */ + personalDetails: PropTypes.objectOf(participantPropTypes), + /** Information about the network */ network: networkPropTypes.isRequired, @@ -86,12 +99,19 @@ const defaultProps = { session: {}, isLoadingReportActions: false, isLoadingReportData: false, + personalDetails: {}, }; class ReportActionsView extends React.Component { constructor(props) { super(props); + this.renderItem = this.renderItem.bind(this); + this.renderCell = this.renderCell.bind(this); + this.scrollToBottomAndUpdateLastRead = this.scrollToBottomAndUpdateLastRead.bind(this); + this.onVisibilityChange = this.onVisibilityChange.bind(this); + this.recordTimeToMeasureItemLayout = this.recordTimeToMeasureItemLayout.bind(this); + this.loadMoreChats = this.loadMoreChats.bind(this); this.appStateChangeListener = null; this.didLayout = false; @@ -104,25 +124,19 @@ class ReportActionsView extends React.Component { this.currentScrollOffset = 0; this.sortedReportActions = ReportActionsUtils.getSortedReportActions(props.reportActions); this.mostRecentIOUReportSequenceNumber = ReportActionsUtils.getMostRecentIOUReportSequenceNumber(props.reportActions); + this.keyExtractor = this.keyExtractor.bind(this); this.trackScroll = this.trackScroll.bind(this); this.showFloatingMessageCounter = this.showFloatingMessageCounter.bind(this); this.hideFloatingMessageCounter = this.hideFloatingMessageCounter.bind(this); this.toggleFloatingMessageCounter = this.toggleFloatingMessageCounter.bind(this); this.updateNewMarkerPosition = this.updateNewMarkerPosition.bind(this); this.updateMessageCounterCount = this.updateMessageCounterCount.bind(this); - this.recordTimeToMeasureItemLayout = this.recordTimeToMeasureItemLayout.bind(this); this.scrollToBottomAndUpdateLastRead = this.scrollToBottomAndUpdateLastRead.bind(this); this.updateNewMarkerAndMarkReadOnce = _.once(this.updateNewMarkerAndMarkRead.bind(this)); } componentDidMount() { - this.appStateChangeListener = AppState.addEventListener('change', () => { - if (!Visibility.isVisible() || this.props.isDrawerOpen) { - return; - } - - Report.updateLastReadActionID(this.props.reportID); - }); + this.appStateChangeListener = AppState.addEventListener('change', this.onVisibilityChange); // If the reportID is not found then we have either not loaded this chat or the user is unable to access it. // We will attempt to fetch it and redirect if still not accessible. @@ -142,6 +156,15 @@ class ReportActionsView extends React.Component { } this.fetchData(); + + const copyShortcutConfig = CONST.KEYBOARD_SHORTCUTS.COPY; + this.unsubscribeCopyShortcut = KeyboardShortcut.subscribe( + copyShortcutConfig.shortcutKey, + this.copySelectionToClipboard, + copyShortcutConfig.descriptionKey, + copyShortcutConfig.modifiers, + false, + ); } shouldComponentUpdate(nextProps, nextState) { @@ -206,28 +229,26 @@ class ReportActionsView extends React.Component { const previousLastSequenceNumber = lodashGet(CollectionUtils.lastItem(prevProps.reportActions), 'sequenceNumber'); const currentLastSequenceNumber = lodashGet(CollectionUtils.lastItem(this.props.reportActions), 'sequenceNumber'); - // Record the max action when window is visible and the sidebar is not covering the report view on a small screen - const isSidebarCoveringReportView = this.props.isSmallScreenWidth && this.props.isDrawerOpen; - const shouldRecordMaxAction = Visibility.isVisible() && !isSidebarCoveringReportView; - - const sidebarClosed = prevProps.isDrawerOpen && !this.props.isDrawerOpen; - const screenSizeIncreased = prevProps.isSmallScreenWidth && !this.props.isSmallScreenWidth; - const reportBecomeVisible = sidebarClosed || screenSizeIncreased; + // Record the max action when window is visible except when Drawer is open on small screen + const shouldRecordMaxAction = Visibility.isVisible() + && (!this.props.isSmallScreenWidth || !this.props.isDrawerOpen); if (previousLastSequenceNumber !== currentLastSequenceNumber) { + // If a new comment is added and it's from the current user scroll to the bottom otherwise + // leave the user positioned where they are now in the list. const lastAction = CollectionUtils.lastItem(this.props.reportActions); - const isLastActionFromCurrentUser = lodashGet(lastAction, 'actorEmail', '') === lodashGet(this.props.session, 'email', ''); - if (isLastActionFromCurrentUser) { - // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where they are now in the list. + if (lastAction && (lastAction.actorEmail === this.props.session.email)) { ReportScrollManager.scrollToBottom(); - } else { + } + + if (lodashGet(lastAction, 'actorEmail', '') !== lodashGet(this.props.session, 'email', '')) { // Only update the unread count when the floating message counter is visible // Otherwise counter will be shown on scrolling up from the bottom even if user have read those messages if (this.state.isFloatingMessageCounterVisible) { this.updateMessageCounterCount(!shouldRecordMaxAction); } - // Show new floating message counter when there is a new message + // show new floating message counter when there is a new message this.toggleFloatingMessageCounter(); } @@ -236,10 +257,10 @@ class ReportActionsView extends React.Component { if (shouldRecordMaxAction) { Report.updateLastReadActionID(this.props.reportID); } - } - - // Update the new marker position and last read action when we are closing the sidebar or moving from a small to large screen size - if (shouldRecordMaxAction && reportBecomeVisible) { + } else if (shouldRecordMaxAction && ( + prevProps.isDrawerOpen !== this.props.isDrawerOpen + || prevProps.isSmallScreenWidth !== this.props.isSmallScreenWidth + )) { this.updateNewMarkerPosition(this.props.report.unreadActionCount); Report.updateLastReadActionID(this.props.reportID); } @@ -255,6 +276,38 @@ class ReportActionsView extends React.Component { } Report.unsubscribeFromReportChannel(this.props.reportID); + + if (this.unsubscribeCopyShortcut) { + this.unsubscribeCopyShortcut(); + } + } + + /** + * Records the max action on app visibility change event. + */ + onVisibilityChange() { + if (!Visibility.isVisible() || this.props.isDrawerOpen) { + return; + } + + Report.updateLastReadActionID(this.props.reportID); + } + + copySelectionToClipboard() { + const selectionMarkdown = SelectionScraper.getAsMarkdown(); + + Clipboard.setString(selectionMarkdown); + } + + /** + * Create a unique key for Each Action in the FlatList. + * We use a combination of sequenceNumber and clientID in case the clientID are the same - which + * shouldn't happen, but might be possible in some rare cases. + * @param {Object} item + * @return {String} + */ + keyExtractor(item) { + return `${item.action.sequenceNumber}${item.action.clientID}`; } fetchData() { @@ -286,6 +339,19 @@ class ReportActionsView extends React.Component { Report.fetchActionsWithLoadingState(this.props.reportID, offset); } + /** + * 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 minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + + variables.fontSizeNormalHeight; + const availableHeight = this.props.windowHeight + - (styles.chatFooter.minHeight + variables.contentHeaderHeight); + return Math.ceil(availableHeight / minimumReportActionHeight); + } + /** * This function is triggered from the ref callback for the scrollview. That way it can be scrolled once all the * items have been rendered. If the number of actions has changed since it was last rendered, then @@ -396,12 +462,67 @@ class ReportActionsView extends React.Component { } } + /** + * This function overrides the CellRendererComponent (defaults to a plain View), giving each ReportActionItem a + * higher z-index than the one below it. This prevents issues where the ReportActionContextMenu overlapping between + * rows is hidden beneath other rows. + * + * @param {Object} index - The ReportAction item in the FlatList. + * @param {Object|Array} style – The default styles of the CellRendererComponent provided by the CellRenderer. + * @param {Object} props – All the other Props provided to the CellRendererComponent by default. + * @returns {React.Component} + */ + renderCell({item, style, ...props}) { + const cellStyle = [ + style, + {zIndex: item.action.sequenceNumber}, + ]; + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + + /** + * 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 {Object} args.item + * @param {Number} args.index + * + * @returns {React.Component} + */ + renderItem({ + item, + index, + }) { + const shouldDisplayNewIndicator = this.props.report.newMarkerSequenceNumber > 0 + && item.action.sequenceNumber === this.props.report.newMarkerSequenceNumber; + return ( + + ); + } + render() { // Comments have not loaded at all yet do nothing if (!_.size(this.props.reportActions)) { return null; } + // 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.report.newMarkerSequenceNumber : undefined; + const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(this.props.personalDetails, this.props.report); + return ( <> - + : null} + keyboardShouldPersistTaps="handled" onLayout={this.recordTimeToMeasureItemLayout} - sortedReportActions={this.sortedReportActions} - mostRecentIOUReportSequenceNumber={this.mostRecentIOUReportSequenceNumber} - isLoadingReportActions={this.props.isLoadingReportActions} + onScroll={this.trackScroll} + extraData={extraData} /> - ); } @@ -434,6 +568,7 @@ export default compose( withWindowDimensions, withDrawerState, withLocalize, + withPersonalDetails(), withNetwork(), withOnyx({ isLoadingReportData: {