diff --git a/src/components/UnreadActionIndicator.js b/src/components/UnreadActionIndicator.js index 83154a4cbf83..d2ffcc232c2a 100644 --- a/src/components/UnreadActionIndicator.js +++ b/src/components/UnreadActionIndicator.js @@ -1,29 +1,17 @@ import React from 'react'; -import {Animated, View} from 'react-native'; -import PropTypes from 'prop-types'; +import {View} from 'react-native'; import styles from '../styles/styles'; import Text from './Text'; -const propTypes = { - // Animated opacity - // eslint-disable-next-line react/forbid-prop-types - animatedOpacity: PropTypes.object.isRequired, -}; - -const UnreadActionIndicator = props => ( - +const UnreadActionIndicator = () => ( + NEW - + ); -UnreadActionIndicator.propTypes = propTypes; UnreadActionIndicator.displayName = 'UnreadActionIndicator'; export default UnreadActionIndicator; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 11988ae99b21..79350d2260eb 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -204,7 +204,7 @@ function setLocalLastRead(reportID, sequenceNumber) { // Update the report optimistically Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - unreadActionCount: 0, + unreadActionCount: Math.max(reportMaxSequenceNumbers[reportID] - sequenceNumber, 0), lastVisitedTimestamp: Date.now(), }); } @@ -669,16 +669,18 @@ function addAction(reportID, text, file) { * * @param {Number} reportID * @param {Number} sequenceNumber + * @param {Boolean} ignoreOrder If set to true, we will not enforce the latest read action to be at the bottom of the + * chat. */ -function updateLastReadActionID(reportID, sequenceNumber) { +function updateLastReadActionID(reportID, sequenceNumber, ignoreOrder = false) { const currentMaxSequenceNumber = reportMaxSequenceNumbers[reportID]; - if (sequenceNumber < currentMaxSequenceNumber) { + if (!ignoreOrder && sequenceNumber < currentMaxSequenceNumber) { return; } setLocalLastRead(reportID, sequenceNumber); - // Mark the report as not having any unread items + // Mark the unread items in the report API.Report_UpdateLastRead({ accountID: currentUserAccountID, reportID, diff --git a/src/pages/home/report/ReportActionContextMenu.js b/src/pages/home/report/ReportActionContextMenu.js index 80156f9443e3..1b44290a51ab 100644 --- a/src/pages/home/report/ReportActionContextMenu.js +++ b/src/pages/home/report/ReportActionContextMenu.js @@ -7,41 +7,6 @@ import { import getReportActionContextMenuStyles from '../../../styles/getReportActionContextMenuStyles'; import ReportActionContextMenuItem from './ReportActionContextMenuItem'; -/** - * A list of all the context actions in this menu. - */ -const CONTEXT_ACTIONS = [ - // Copy to clipboard - { - text: 'Copy to Clipboard', - icon: Clipboard, - }, - - // Copy chat link - { - text: 'Copy Link', - icon: LinkCopy, - }, - - // Mark as Unread - { - text: 'Mark as Unread', - icon: Mail, - }, - - // Edit Comment - { - text: 'Edit Comment', - icon: Pencil, - }, - - // Delete Comment - { - text: 'Delete Comment', - icon: Trashcan, - }, -]; - const propTypes = { // The ID of the report this report action is attached to. // eslint-disable-next-line react/no-unused-prop-types @@ -57,14 +22,57 @@ const propTypes = { // Controls the visibility of this component. isVisible: PropTypes.bool, + + // Function to trigger when we try to mark a message as unread + onMarkAsUnread: PropTypes.func, }; const defaultProps = { isMini: false, isVisible: false, + onMarkAsUnread: () => {}, }; const ReportActionContextMenu = (props) => { + /** + * A list of all the context actions in this menu. + */ + const CONTEXT_ACTIONS = [ + // Copy to clipboard + { + text: 'Copy to Clipboard', + icon: Clipboard, + onPress: () => {}, + }, + + // Copy chat link + { + text: 'Copy Link', + icon: LinkCopy, + onPress: () => {}, + }, + + // Mark as Unread + { + text: 'Mark as Unread', + icon: Mail, + onPress: props.onMarkAsUnread, + }, + + // Edit Comment + { + text: 'Edit Comment', + icon: Pencil, + onPress: () => {}, + }, + + // Delete Comment + { + text: 'Delete Comment', + icon: Trashcan, + onPress: () => {}, + }, + ]; const wrapperStyle = getReportActionContextMenuStyles(props.isMini); return props.isVisible && ( @@ -74,6 +82,7 @@ const ReportActionContextMenu = (props) => { text={contextAction.text} isMini={props.isMini} key={contextAction.text} + onPress={contextAction.onPress} /> ))} diff --git a/src/pages/home/report/ReportActionContextMenuItem.js b/src/pages/home/report/ReportActionContextMenuItem.js index 6b60ce74d9f5..a57715e6d0ee 100644 --- a/src/pages/home/report/ReportActionContextMenuItem.js +++ b/src/pages/home/report/ReportActionContextMenuItem.js @@ -27,13 +27,23 @@ function getButtonState(isHovered = false, isPressed = false) { } const propTypes = { + + // Icon to display in the menu icon: PropTypes.elementType.isRequired, + + // Text for the action text: PropTypes.string.isRequired, + + // If true, we are displaying the mini hover-menu isMini: PropTypes.bool, + + // Function to trigger when the action is pressed + onPress: PropTypes.func, }; const defaultProps = { isMini: false, + onPress: () => {}, }; const ReportActionContextMenuItem = (props) => { @@ -42,7 +52,10 @@ const ReportActionContextMenuItem = (props) => { props.isMini ? ( - getButtonStyle(getButtonState(hovered, pressed))}> + getButtonStyle(getButtonState(hovered, pressed))} + onPress={props.onPress} + > {({hovered, pressed}) => ( { ) : ( - getButtonStyle(getButtonState(hovered, pressed))}> + getButtonStyle(getButtonState(hovered, pressed))} + onPress={props.onPress} + > {({hovered, pressed}) => ( <> 0 + && ((this.props.index === this.props.report.unreadActionCount - 1) + !== (nextProps.index === nextProps.report.unreadActionCount - 1)); + return this.state.isPopoverVisible !== nextState.isPopoverVisible || this.props.displayAsGroup !== nextProps.displayAsGroup + || hasNewDisplayChanged || !_.isEqual(this.props.action, nextProps.action); } @@ -102,11 +123,14 @@ class ReportActionItem extends Component { } render() { + const displayNewIndicator = this.props.report.unreadActionCount > 0 + && this.props.index === this.props.report.unreadActionCount - 1; return ( {hovered => ( + {displayNewIndicator && } {!this.props.displayAsGroup ? @@ -121,6 +145,7 @@ class ReportActionItem extends Component { && this.isInReportActionContextMenuBeta() && !this.state.isPopoverVisible } + onMarkAsUnread={this.props.onMarkAsUnread} isMini /> @@ -141,6 +166,7 @@ class ReportActionItem extends Component { isVisible={this.state.isPopoverVisible} reportID={this.props.reportID} reportActionID={this.props.action.sequenceNumber} + onMarkAsUnread={this.props.onMarkAsUnread} /> @@ -158,4 +184,7 @@ export default withOnyx({ betas: { key: ONYXKEYS.BETAS, }, + report: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + }, })(ReportActionItem); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 4d626ebba79b..41ba98a496bb 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,6 +1,5 @@ import React from 'react'; import { - Animated, View, Keyboard, AppState, @@ -11,7 +10,6 @@ import _ from 'underscore'; import lodashGet from 'lodash.get'; import {withOnyx} from 'react-native-onyx'; import Text from '../../../components/Text'; -import UnreadActionIndicator from '../../../components/UnreadActionIndicator'; import { fetchActions, updateLastReadActionID, @@ -34,13 +32,6 @@ const propTypes = { reportID: PropTypes.number.isRequired, /* Onyx Props */ - - // The report currently being looked at - report: PropTypes.shape({ - // Number of actions unread - unreadActionCount: PropTypes.number, - }), - // Array of report actions for this report reportActions: PropTypes.objectOf(PropTypes.shape(ReportActionPropTypes)), @@ -52,9 +43,6 @@ const propTypes = { }; const defaultProps = { - report: { - unreadActionCount: 0, - }, reportActions: {}, session: {}, }; @@ -68,13 +56,10 @@ class ReportActionsView extends React.Component { this.scrollToListBottom = this.scrollToListBottom.bind(this); this.recordMaxAction = this.recordMaxAction.bind(this); this.onVisibilityChange = this.onVisibilityChange.bind(this); + this.sortedReportActions = this.updateSortedReportActions(); this.loadMoreChats = this.loadMoreChats.bind(this); this.sortedReportActions = []; this.timers = []; - this.unreadIndicatorOpacity = new Animated.Value(1); - - // Helper variable that keeps track of the unread action count before it updates to zero - this.unreadActionCount = 0; // Helper variable that prevents the unread indicator to show up for new messages // received while the report is still active @@ -113,6 +98,7 @@ class ReportActionsView extends React.Component { // We have switched to a new report if (prevProps.reportID !== this.props.reportID) { this.reset(prevProps.reportID); + this.shouldShowUnreadActionIndicator = true; return; } @@ -155,31 +141,6 @@ class ReportActionsView extends React.Component { } } - /** - * Checks if the unreadActionIndicator should be shown. - * If it does, starts a timeout for the fading out animation and creates - * a flag to not show it again if the report is still open - */ - setUpUnreadActionIndicator() { - if (!this.shouldShowUnreadActionIndicator) { - return; - } - - this.unreadActionCount = this.props.report.unreadActionCount; - - if (this.unreadActionCount > 0) { - this.unreadIndicatorOpacity = new Animated.Value(1); - this.timers.push(setTimeout(() => { - Animated.timing(this.unreadIndicatorOpacity, { - toValue: 0, - useNativeDriver: false, - }).start(); - }, 3000)); - } - - this.shouldShowUnreadActionIndicator = false; - } - /** * Actions to run when the report has been updated * @param {Number} oldReportID @@ -274,7 +235,6 @@ class ReportActionsView extends React.Component { .pluck('sequenceNumber') .max() .value(); - updateLastReadActionID(this.props.reportID, maxVisibleSequenceNumber); } @@ -335,15 +295,15 @@ class ReportActionsView extends React.Component { // are implemented on native and web/desktop which leads to // the unread indicator on native to render below the message instead of above it. - {this.unreadActionCount > 0 && index === this.unreadActionCount - 1 && ( - - )} updateLastReadActionID(this.props.reportID, + item.action.sequenceNumber - 1, true)} /> ); @@ -364,7 +324,7 @@ class ReportActionsView extends React.Component { ); } - this.setUpUnreadActionIndicator(); + this.shouldShowUnreadActionIndicator = false; this.updateSortedReportActions(); return ( `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - }, reportActions: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, canEvict: false, diff --git a/src/styles/styles.js b/src/styles/styles.js index 1f44256c7858..c173abf5f33b 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -772,7 +772,6 @@ const styles = { left: 0, top: 0, bottom: 0, - zIndex: 2, }, sidebarVisible: { @@ -951,6 +950,7 @@ const styles = { ...positioning.tn4, ...positioning.r4, position: 'absolute', + zIndex: variables.zIndexTop, }, reportActionContextMenuText: { @@ -1038,13 +1038,14 @@ const styles = { unreadIndicatorContainer: { position: 'absolute', - top: -10, + top: -5, left: 0, width: '100%', - height: 20, + height: 10, paddingHorizontal: 20, flexDirection: 'row', alignItems: 'center', + zIndex: variables.zIndexMiddle, }, unreadIndicatorLine: { diff --git a/src/styles/variables.js b/src/styles/variables.js index d4b5ecc592a3..1f0a5fe00f44 100644 --- a/src/styles/variables.js +++ b/src/styles/variables.js @@ -19,4 +19,6 @@ export default { safeInsertPercentage: 0.7, sideBarWidth: 375, pdfPageMaxWidth: 992, + zIndexMiddle: 50, + zIndexTop: 999, };