diff --git a/src/components/UnreadActionIndicator.js b/src/components/UnreadActionIndicator.js new file mode 100644 index 000000000000..83154a4cbf83 --- /dev/null +++ b/src/components/UnreadActionIndicator.js @@ -0,0 +1,29 @@ +import React from 'react'; +import {Animated, View} from 'react-native'; +import PropTypes from 'prop-types'; +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 => ( + + + + NEW + + +); + +UnreadActionIndicator.propTypes = propTypes; +UnreadActionIndicator.displayName = 'UnreadActionIndicator'; + +export default UnreadActionIndicator; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 506c8acff05e..92f249c25107 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,10 +1,16 @@ import React from 'react'; -import {View, Keyboard, AppState} from 'react-native'; +import { + Animated, + View, + Keyboard, + AppState, +} from 'react-native'; import PropTypes from 'prop-types'; 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} from '../../../libs/actions/Report'; import ONYXKEYS from '../../../ONYXKEYS'; import ReportActionItem from './ReportActionItem'; @@ -23,6 +29,12 @@ const propTypes = { /* 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)), @@ -34,6 +46,9 @@ const propTypes = { }; const defaultProps = { + report: { + unreadActionCount: 0, + }, reportActions: {}, session: {}, }; @@ -46,8 +61,17 @@ 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.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 + this.shouldShowUnreadActionIndicator = true; this.state = { refetchNeeded: true, @@ -145,6 +169,31 @@ class ReportActionsView extends React.Component { this.setState({refetchNeeded}); } + /** + * 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.props.isActiveReport || !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; + } + /** * Updates and sorts the report actions by sequence number */ @@ -239,12 +288,21 @@ class ReportActionsView extends React.Component { needsLayoutCalculation, }) { return ( - + + // Using instead of a Fragment because there is a difference between how + // 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 && ( + + )} + + ); } @@ -263,6 +321,7 @@ class ReportActionsView extends React.Component { ); } + this.setUpUnreadActionIndicator(); this.updateSortedReportActions(); return ( `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + }, reportActions: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, canEvict: props => !props.isActiveReport, diff --git a/src/styles/styles.js b/src/styles/styles.js index db23694b1bc2..975179b65a02 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -948,6 +948,32 @@ const styles = { marginLeft: 8, }, + unreadIndicatorContainer: { + position: 'absolute', + top: -10, + left: 0, + width: '100%', + height: 20, + paddingHorizontal: 20, + flexDirection: 'row', + alignItems: 'center', + }, + + unreadIndicatorLine: { + height: 1, + backgroundColor: themeColors.unreadIndicator, + flexGrow: 1, + marginRight: 8, + opacity: 0.5, + }, + + unreadIndicatorText: { + color: themeColors.unreadIndicator, + fontFamily: fontFamily.GTA_BOLD, + fontSize: variables.fontSizeSmall, + fontWeight: fontWeightBold, + }, + flipUpsideDown: { transform: [{rotate: '180deg'}], }, diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js index 2f57fb5b72cc..ea81000323fb 100644 --- a/src/styles/themes/default.js +++ b/src/styles/themes/default.js @@ -30,4 +30,5 @@ export default { pillBG: colors.gray2, buttonDisabledBG: colors.gray2, buttonHoveredBG: colors.gray1, + unreadIndicator: colors.green, };