Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HOLD] [WIP] Introduce "Mark as unread" functionality in Expensify.cash #1774

Closed
wants to merge 15 commits into from
20 changes: 4 additions & 16 deletions src/components/UnreadActionIndicator.js
Original file line number Diff line number Diff line change
@@ -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 => (
<Animated.View style={[
styles.unreadIndicatorContainer,
{opacity: props.animatedOpacity},
]}
>
const UnreadActionIndicator = () => (
<View style={styles.unreadIndicatorContainer}>
<View style={styles.unreadIndicatorLine} />
<Text style={styles.unreadIndicatorText}>
NEW
</Text>
</Animated.View>
</View>
);

UnreadActionIndicator.propTypes = propTypes;
UnreadActionIndicator.displayName = 'UnreadActionIndicator';

export default UnreadActionIndicator;
15 changes: 6 additions & 9 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,14 @@ function fetchChatReportsByIDs(chatList) {
*
* @param {Number} reportID
* @param {Number} sequenceNumber
* @param {Object} report
*/
function setLocalLastRead(reportID, sequenceNumber) {
function setLocalLastRead(reportID, sequenceNumber, report = null) {
lastReadSequenceNumbers[reportID] = sequenceNumber;

// Update the report optimistically
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
unreadActionCount: 0,
unreadActionCount: report ? report.maxSequenceNumber - sequenceNumber : 0,
lastVisitedTimestamp: Date.now(),
});
}
Expand Down Expand Up @@ -663,14 +664,10 @@ function addAction(reportID, text, file) {
*
* @param {Number} reportID
* @param {Number} sequenceNumber
* @param {Object} report
*/
function updateLastReadActionID(reportID, sequenceNumber) {
const currentMaxSequenceNumber = reportMaxSequenceNumbers[reportID];
if (sequenceNumber < currentMaxSequenceNumber) {
return;
}

setLocalLastRead(reportID, sequenceNumber);
function updateLastReadActionID(reportID, sequenceNumber, report = null) {
setLocalLastRead(reportID, sequenceNumber, report);

// Mark the report as not having any unread items
API.Report_UpdateLastRead({
Expand Down
6 changes: 5 additions & 1 deletion src/pages/home/report/ReportActionContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ 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.isRequired,
};

const defaultProps = {
Expand All @@ -65,7 +68,7 @@ const defaultProps = {
};

const ReportActionContextMenu = (props) => {
const wrapperStyle = getReportActionContextMenuStyles(props.isMini);
const wrapperStyle = getReportActionContextMenuStyles(true);
return props.isVisible && (
<View style={wrapperStyle}>
{CONTEXT_ACTIONS.map(contextAction => (
Expand All @@ -74,6 +77,7 @@ const ReportActionContextMenu = (props) => {
text={contextAction.text}
isMini={props.isMini}
key={contextAction.text}
onPress={contextAction.text === 'Mark as Unread' ? props.onMarkAsUnread : () => {}}
/>
))}
</View>
Expand Down
12 changes: 10 additions & 2 deletions src/pages/home/report/ReportActionContextMenuItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ const propTypes = {
icon: PropTypes.elementType.isRequired,
text: PropTypes.string.isRequired,
isMini: PropTypes.bool,
onPress: PropTypes.func,
};

const defaultProps = {
isMini: false,
onPress: () => {},
};

const ReportActionContextMenuItem = (props) => {
Expand All @@ -42,7 +44,10 @@ const ReportActionContextMenuItem = (props) => {
props.isMini
? (
<Tooltip text={props.text}>
<Pressable style={({hovered, pressed}) => getButtonStyle(getButtonState(hovered, pressed))}>
<Pressable
style={({hovered, pressed}) => getButtonStyle(getButtonState(hovered, pressed))}
onPress={props.onPress}
>
{({hovered, pressed}) => (
<Icon
src={props.icon}
Expand All @@ -52,7 +57,10 @@ const ReportActionContextMenuItem = (props) => {
</Pressable>
</Tooltip>
) : (
<Pressable style={({hovered, pressed}) => getButtonStyle(getButtonState(hovered, pressed))}>
<Pressable
style={({hovered, pressed}) => getButtonStyle(getButtonState(hovered, pressed))}
onPress={props.onPress}
>
{({hovered, pressed}) => (
<>
<Icon
Expand Down
13 changes: 12 additions & 1 deletion src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import PopoverWithMeasuredContent from '../../../components/PopoverWithMeasuredC
import ReportActionItemSingle from './ReportActionItemSingle';
import ReportActionItemGrouped from './ReportActionItemGrouped';
import ReportActionContextMenu from './ReportActionContextMenu';
import UnreadActionIndicator from '../../../components/UnreadActionIndicator';

const propTypes = {
// The ID of the report this action is on.
Expand All @@ -25,6 +26,11 @@ const propTypes = {
// Should the comment have the appearance of being grouped with the previous comment?
displayAsGroup: PropTypes.bool.isRequired,

//
onMarkAsUnread: PropTypes.func.isRequired,

displayNewIndicator: PropTypes.bool.isRequired,

/* --- Onyx Props --- */
// List of betas for the current user.
betas: PropTypes.arrayOf(PropTypes.string),
Expand Down Expand Up @@ -56,7 +62,8 @@ class ReportActionItem extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.state.isPopoverVisible !== nextState.isPopoverVisible
|| this.props.displayAsGroup !== nextProps.displayAsGroup
|| !_.isEqual(this.props.action, nextProps.action);
|| !_.isEqual(this.props.action, nextProps.action)
|| this.props.displayNewIndicator !== nextProps.displayNewIndicator;
}

/**
Expand Down Expand Up @@ -107,6 +114,7 @@ class ReportActionItem extends Component {
<Hoverable>
{hovered => (
<View>
{this.props.displayNewIndicator && (<UnreadActionIndicator />)}
<View style={getReportActionItemStyles(hovered)}>
{!this.props.displayAsGroup
? <ReportActionItemSingle action={this.props.action} />
Expand All @@ -121,6 +129,7 @@ class ReportActionItem extends Component {
&& this.isInReportActionContextMenuBeta()
&& !this.state.isPopoverVisible
}
onMarkAsUnread={this.props.onMarkAsUnread}
isMini
/>
</View>
Expand All @@ -134,13 +143,15 @@ class ReportActionItem extends Component {
isVisible
reportID={-1}
reportActionID={-1}
onMarkAsUnread={() => {}}
/>
)}
>
<ReportActionContextMenu
isVisible={this.state.isPopoverVisible}
reportID={this.props.reportID}
reportActionID={this.props.action.sequenceNumber}
onMarkAsUnread={this.props.onMarkAsUnread}
/>
</PopoverWithMeasuredContent>
</View>
Expand Down
43 changes: 24 additions & 19 deletions src/pages/home/report/ReportActionsView.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import {
Animated,
View,
Keyboard,
AppState,
Expand Down Expand Up @@ -39,6 +38,9 @@ const propTypes = {
report: PropTypes.shape({
// Number of actions unread
unreadActionCount: PropTypes.number,

// Number of the last sequence
maxSequenceNumber: PropTypes.number,
}),

// Array of report actions for this report
Expand All @@ -54,6 +56,7 @@ const propTypes = {
const defaultProps = {
report: {
unreadActionCount: 0,
maxSequenceNumber: 0,
},
reportActions: {},
session: {},
Expand All @@ -68,19 +71,19 @@ class ReportActionsView extends React.Component {
this.scrollToListBottom = this.scrollToListBottom.bind(this);
this.recordMaxAction = this.recordMaxAction.bind(this);
this.onVisibilityChange = this.onVisibilityChange.bind(this);
this.onMarkAsUnread = this.onMarkAsUnread.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
this.shouldShowUnreadActionIndicator = true;

this.state = {
refetchNeeded: true,
unreadActionCount: 0,
isLoadingMoreChats: false,
};
}
Expand All @@ -102,6 +105,10 @@ class ReportActionsView extends React.Component {
return true;
}

if (nextState.unreadActionCount !== this.state.unreadActionCount) {
return true;
}

if (nextState.isLoadingMoreChats !== this.state.isLoadingMoreChats) {
return true;
}
Expand Down Expand Up @@ -155,6 +162,13 @@ class ReportActionsView extends React.Component {
}
}

onMarkAsUnread(actionIndex) {
updateLastReadActionID(this.props.reportID, actionIndex, this.props.report);
this.setState({
unreadActionCount: this.props.report.maxSequenceNumber - actionIndex,
});
}

/**
* Checks if the unreadActionIndicator should be shown.
* If it does, starts a timeout for the fading out animation and creates
Expand All @@ -165,17 +179,9 @@ class ReportActionsView extends React.Component {
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.setState({
unreadActionCount: this.props.report.unreadActionCount,
});

this.shouldShowUnreadActionIndicator = false;
}
Expand Down Expand Up @@ -335,15 +341,14 @@ class ReportActionsView extends React.Component {
// <InvertedFlatList /> are implemented on native and web/desktop which leads to
// the unread indicator on native to render below the message instead of above it.
<View>
{this.unreadActionCount > 0 && index === this.unreadActionCount - 1 && (
<UnreadActionIndicator animatedOpacity={this.unreadIndicatorOpacity} />
)}
<ReportActionItem
reportID={this.props.reportID}
action={item.action}
displayAsGroup={this.isConsecutiveActionMadeByPreviousActor(index)}
onLayout={onLayout}
needsLayoutCalculation={needsLayoutCalculation}
onMarkAsUnread={() => this.onMarkAsUnread(item.action.sequenceNumber - 1)}
displayNewIndicator={this.state.unreadActionCount > 0 && index === this.state.unreadActionCount - 1}
/>
</View>
);
Expand Down
7 changes: 4 additions & 3 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,6 @@ const styles = {
left: 0,
top: 0,
bottom: 0,
zIndex: 2,
},

sidebarVisible: {
Expand Down Expand Up @@ -947,6 +946,7 @@ const styles = {
...positioning.tn4,
...positioning.r4,
position: 'absolute',
zIndex: 999,
},

reportActionContextMenuText: {
Expand Down Expand Up @@ -1034,13 +1034,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: 1,
},

unreadIndicatorLine: {
Expand Down
Loading