Skip to content

Commit

Permalink
Merge pull request #2433 from Expensify/alberto-markAsUnread
Browse files Browse the repository at this point in the history
Introduce "Mark as unread" functionality in Expensify.cash
  • Loading branch information
marcaaron authored May 5, 2021
2 parents 0ddd07f + 0f4c6c8 commit 5a04fa0
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 125 deletions.
41 changes: 29 additions & 12 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,14 +387,19 @@ function setLocalIOUReportData(iouReportObject, chatReportID) {
* Update the lastRead actionID and timestamp in local memory and Onyx
*
* @param {Number} reportID
* @param {Number} sequenceNumber
* @param {Number} lastReadSequenceNumber
*/
function setLocalLastRead(reportID, sequenceNumber) {
lastReadSequenceNumbers[reportID] = sequenceNumber;
function setLocalLastRead(reportID, lastReadSequenceNumber) {
lastReadSequenceNumbers[reportID] = lastReadSequenceNumber;
const reportMaxSequenceNumber = reportMaxSequenceNumbers[reportID];

// Determine the number of unread actions by deducting the last read sequence from the total. If, for some reason,
// the last read sequence is higher than the actual last sequence, let's just assume all actions are read
const unreadActionCount = Math.max(reportMaxSequenceNumber - lastReadSequenceNumber, 0);

// Update the report optimistically
// Update the report optimistically.
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
unreadActionCount: 0,
unreadActionCount,
lastVisitedTimestamp: Date.now(),
});
}
Expand Down Expand Up @@ -930,21 +935,32 @@ function addAction(reportID, text, file) {
* network layer handle the delayed write.
*
* @param {Number} reportID
* @param {Number} sequenceNumber
* @param {Number} [sequenceNumber] This can be used to set the last read actionID to a specific
* spot (eg. mark-as-unread). Otherwise, when this param is omitted, the highest sequence number becomes the one that
* is last read (meaning that the entire report history has been read)
*/
function updateLastReadActionID(reportID, sequenceNumber) {
const currentMaxSequenceNumber = reportMaxSequenceNumbers[reportID];
if (sequenceNumber < currentMaxSequenceNumber) {
return;
}
// Need to subtract 1 from sequenceNumber so that the "New" marker appears in the right spot (the last read
// action). If 1 isn't subtracted then the "New" marker appears one row below the action (the first unread action)
const lastReadSequenceNumber = (sequenceNumber - 1) || reportMaxSequenceNumbers[reportID];

setLocalLastRead(reportID, sequenceNumber);
setLocalLastRead(reportID, lastReadSequenceNumber);

// Mark the report as not having any unread items
API.Report_UpdateLastRead({
accountID: currentUserAccountID,
reportID,
sequenceNumber,
sequenceNumber: lastReadSequenceNumber,
});
}

/**
* @param {Number} reportID
* @param {Number} sequenceNumber
*/
function setNewMarkerPosition(reportID, sequenceNumber) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
newMarkerSequenceNumber: sequenceNumber,
});
}

Expand Down Expand Up @@ -1030,6 +1046,7 @@ export {
fetchOrCreateChatReport,
addAction,
updateLastReadActionID,
setNewMarkerPosition,
subscribeToReportTypingEvents,
subscribeToUserEvents,
unsubscribeFromReportChannel,
Expand Down
171 changes: 88 additions & 83 deletions src/pages/home/report/ReportActionContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,81 +7,19 @@ import {
Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark,
} from '../../../components/Icon/Expensicons';
import getReportActionContextMenuStyles from '../../../styles/getReportActionContextMenuStyles';
import {setNewMarkerPosition, updateLastReadActionID} from '../../../libs/actions/Report';
import ReportActionContextMenuItem from './ReportActionContextMenuItem';
import ReportActionPropTypes from './ReportActionPropTypes';
import Clipboard from '../../../libs/Clipboard';
import {isReportMessageAttachment} from '../../../libs/reportUtils';

/**
* A list of all the context actions in this menu.
*/
const CONTEXT_ACTIONS = [
// Copy to clipboard
{
text: 'Copy to Clipboard',
icon: ClipboardIcon,
successText: 'Copied!',
successIcon: Checkmark,
shouldShow: true,

// If return value is true, we switch the `text` and `icon` on
// `ReportActionContextMenuItem` with `successText` and `successIcon` which will fallback to
// the `text` and `icon`
onPress: (action) => {
const message = _.last(lodashGet(action, 'message', null));
const html = lodashGet(message, 'html', '');
const text = lodashGet(message, 'text', '');
const isAttachment = _.has(action, 'isAttachment')
? action.isAttachment
: isReportMessageAttachment(text);
if (!isAttachment) {
Clipboard.setString(text);
} else {
Clipboard.setString(html);
}
},
},

// Copy chat link
{
text: 'Copy Link',
icon: LinkCopy,
shouldShow: false,
onPress: () => {},
},

// Mark as Unread
{
text: 'Mark as Unread',
icon: Mail,
shouldShow: false,
onPress: () => {},
},

// Edit Comment
{
text: 'Edit Comment',
icon: Pencil,
shouldShow: false,
onPress: () => {},
},

// Delete Comment
{
text: 'Delete Comment',
icon: Trashcan,
shouldShow: false,
onPress: () => {},
},
];

const propTypes = {
// The ID of the report this report action is attached to.
// eslint-disable-next-line react/no-unused-prop-types
reportID: PropTypes.number.isRequired,

// The report action this context menu is attached to.
reportAction: PropTypes.shape(ReportActionPropTypes),
reportAction: PropTypes.shape(ReportActionPropTypes).isRequired,

// If true, this component will be a small, row-oriented menu that displays icons but not text.
// If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row.
Expand All @@ -92,29 +30,96 @@ const propTypes = {
};

const defaultProps = {
reportAction: {},
isMini: false,
isVisible: false,
};

const ReportActionContextMenu = (props) => {
const wrapperStyle = getReportActionContextMenuStyles(props.isMini);
return props.isVisible && (
<View style={wrapperStyle}>
{CONTEXT_ACTIONS.map(contextAction => contextAction.shouldShow && (
<ReportActionContextMenuItem
icon={contextAction.icon}
text={contextAction.text}
successIcon={contextAction.successIcon}
successText={contextAction.successText}
isMini={props.isMini}
onPress={() => contextAction.onPress(props.reportAction)}
key={contextAction.text}
/>
))}
</View>
);
};
class ReportActionContextMenu extends React.Component {
constructor(props) {
super(props);

// A list of all the context actions in this menu.
this.CONTEXT_ACTIONS = [
// Copy to clipboard
{
text: 'Copy to Clipboard',
icon: ClipboardIcon,
successText: 'Copied!',
successIcon: Checkmark,
shouldShow: true,

// If return value is true, we switch the `text` and `icon` on
// `ReportActionContextMenuItem` with `successText` and `successIcon` which will fallback to
// the `text` and `icon`
onPress: () => {
const message = _.last(lodashGet(this.props.reportAction, 'message', null));
const html = lodashGet(message, 'html', '');
const text = lodashGet(message, 'text', '');
const isAttachment = _.has(this.props.reportAction, 'isAttachment')
? this.props.reportAction.isAttachment
: isReportMessageAttachment(text);
if (!isAttachment) {
Clipboard.setString(text);
} else {
Clipboard.setString(html);
}
},
},

{
text: 'Copy Link',
icon: LinkCopy,
shouldShow: false,
onPress: () => {},
},

{
text: 'Mark as Unread',
icon: Mail,
successIcon: Checkmark,
shouldShow: true,
onPress: () => {
updateLastReadActionID(this.props.reportID, this.props.reportAction.sequenceNumber);
setNewMarkerPosition(this.props.reportID, this.props.reportAction.sequenceNumber);
},
},

{
text: 'Edit Comment',
icon: Pencil,
shouldShow: false,
onPress: () => {},
},

{
text: 'Delete Comment',
icon: Trashcan,
shouldShow: false,
onPress: () => {},
},
];

this.wrapperStyle = getReportActionContextMenuStyles(this.props.isMini);
}

render() {
return this.props.isVisible && (
<View style={this.wrapperStyle}>
{this.CONTEXT_ACTIONS.map(contextAction => contextAction.shouldShow && (
<ReportActionContextMenuItem
icon={contextAction.icon}
text={contextAction.text}
successIcon={contextAction.successIcon}
successText={contextAction.successText}
isMini={this.props.isMini}
key={contextAction.text}
onPress={contextAction.onPress}
/>
))}
</View>
);
}
}

ReportActionContextMenu.propTypes = propTypes;
ReportActionContextMenu.defaultProps = defaultProps;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class ReportActionItem extends Component {
measureContent={() => (
<ReportActionContextMenu
isVisible
reportID={-1}
reportID={this.props.reportID}
reportAction={this.props.action}
/>
)}
Expand Down
Loading

0 comments on commit 5a04fa0

Please sign in to comment.