Skip to content

Commit

Permalink
Merge pull request #11219 from Expensify/marcaaron-refactorCreateOpti…
Browse files Browse the repository at this point in the history
…misticReportAction

Clean up various `buildOptimistic*` methods. Clarify `sequenceNumber` usage in doc comments.
  • Loading branch information
marcaaron authored Sep 23, 2022
2 parents 373dce8 + 7c26084 commit 3780e0f
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 227 deletions.
8 changes: 4 additions & 4 deletions src/libs/NumberUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@ function rand64() {
/**
* @returns {Number}
*/
function generateReportActionSequenceNumber() {
function generateReportActionClientID() {
// Generate a clientID so we can save the optimistic action to storage with the clientID as key. Later, we will
// remove the optimistic action when we add the real action created in the server. We do this because it's not
// safe to assume that this will use the very next sequenceNumber. An action created by another can overwrite that
// sequenceNumber if it is created before this one. We use a combination of current epoch timestamp (milliseconds)
// and a random number so that the probability of someone else having the same optimisticReportActionID is
// and a random number so that the probability of someone else having the same clientID is
// extremely low even if they left the comment at the same moment as another user on the same report. The random
// number is 3 digits because if we go any higher JS will convert the digits after the 16th position to 0's in
// optimisticReportActionID.
// clientID.
const randomNumber = Math.floor((Math.random() * (999 - 100)) + 100);
return parseInt(`${Date.now()}${randomNumber}`, 10);
}

export {
rand64,
generateReportActionSequenceNumber,
generateReportActionClientID,
};
205 changes: 197 additions & 8 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import Onyx from 'react-native-onyx';
import moment from 'moment';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
import * as Localize from './Localize';
Expand Down Expand Up @@ -46,10 +47,14 @@ Onyx.connect({
},
});

let allPersonalDetails;
let currentUserPersonalDetails;
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS,
callback: val => currentUserPersonalDetails = lodashGet(val, currentUserEmail),
callback: (val) => {
currentUserPersonalDetails = lodashGet(val, currentUserEmail);
allPersonalDetails = val;
},
});

/**
Expand Down Expand Up @@ -573,6 +578,63 @@ function hasReportNameError(report) {
return !_.isEmpty(lodashGet(report, 'errorFields.reportName', {}));
}

/**
* @param {Number} sequenceNumber sequenceNumber must be provided and it must be a number. It cannot and should not be a clientID,
* reportActionID, or anything else besides an estimate of what the next sequenceNumber will be for the
* optimistic report action. Until we deprecate sequenceNumbers please assume that all report actions
* have them and they should be numbers.
* @param {String} [text]
* @param {File} [file]
* @returns {Object}
*/
function buildOptimisticReportAction(sequenceNumber, text, file) {
// For comments shorter than 10k chars, convert the comment from MD into HTML because that's how it is stored in the database
// For longer comments, skip parsing and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!!
const parser = new ExpensiMark();
const commentText = text.length < 10000 ? parser.replace(text) : text;
const isAttachment = _.isEmpty(text) && file !== undefined;
const attachmentInfo = isAttachment ? file : {};
const htmlForNewComment = isAttachment ? 'Uploading Attachment...' : commentText;

// Remove HTML from text when applying optimistic offline comment
const textForNewComment = isAttachment ? '[Attachment]'
: parser.htmlToText(htmlForNewComment);

return {
commentText,
reportAction: {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
actorEmail: currentUserEmail,
actorAccountID: currentUserAccountID,
person: [
{
style: 'strong',
text: lodashGet(allPersonalDetails, [currentUserEmail, 'displayName'], currentUserEmail),
type: 'TEXT',
},
],
automatic: false,
sequenceNumber,
clientID: NumberUtils.generateReportActionClientID(),
avatar: lodashGet(allPersonalDetails, [currentUserEmail, 'avatar'], getDefaultAvatar(currentUserEmail)),
timestamp: moment().unix(),
message: [
{
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
html: htmlForNewComment,
text: textForNewComment,
},
],
isFirstItem: false,
isAttachment,
attachmentInfo,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
shouldShow: true,
},
};
}

/*
* Builds an optimistic IOU report with a randomly generated reportID
*/
Expand All @@ -599,6 +661,7 @@ function buildOptimisticIOUReport(ownerEmail, recipientEmail, total, chatReportI
/**
* Builds an optimistic IOU reportAction object
*
* @param {Number} sequenceNumber - Caller is responsible for providing a best guess at what the next sequenceNumber will be.
* @param {String} type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay).
* @param {Number} amount - IOU amount in cents.
* @param {String} comment - User comment for the IOU.
Expand All @@ -608,11 +671,10 @@ function buildOptimisticIOUReport(ownerEmail, recipientEmail, total, chatReportI
*
* @returns {Object}
*/
function buildOptimisticIOUReportAction(type, amount, comment, paymentType = '', existingIOUTransactionID = '', existingIOUReportID = 0) {
function buildOptimisticIOUReportAction(sequenceNumber, type, amount, comment, paymentType = '', existingIOUTransactionID = '', existingIOUReportID = 0) {
const currency = lodashGet(currentUserPersonalDetails, 'localCurrencyCode');
const IOUTransactionID = existingIOUTransactionID || NumberUtils.rand64();
const IOUReportID = existingIOUReportID || generateReportID();
const sequenceNumber = NumberUtils.generateReportActionSequenceNumber();
const originalMessage = {
amount,
comment,
Expand All @@ -637,10 +699,7 @@ function buildOptimisticIOUReportAction(type, amount, comment, paymentType = '',
actorEmail: currentUserEmail,
automatic: false,
avatar: lodashGet(currentUserPersonalDetails, 'avatar', getDefaultAvatar(currentUserEmail)),

// For now, the clientID and sequenceNumber are the same.
// We are changing that as we roll out the optimisticReportAction IDs and related refactors.
clientID: sequenceNumber,
clientID: NumberUtils.generateReportActionClientID(),
isAttachment: false,
originalMessage,
person: [{
Expand All @@ -656,6 +715,132 @@ function buildOptimisticIOUReportAction(type, amount, comment, paymentType = '',
};
}

/**
* Builds an optimistic chat report with a randomly generated reportID and as much information as we currently have
*
* @param {Array} participantList
* @param {String} reportName
* @param {String} chatType
* @param {String} policyID
* @param {String} ownerEmail
* @param {Boolean} isOwnPolicyExpenseChat
* @param {String} oldPolicyName
* @param {String} visibility
* @returns {Object}
*/
function buildOptimisticChatReport(
participantList,
reportName = 'Chat Report',
chatType = '',
policyID = CONST.POLICY.OWNER_EMAIL_FAKE,
ownerEmail = CONST.REPORT.OWNER_EMAIL_FAKE,
isOwnPolicyExpenseChat = false,
oldPolicyName = '',
visibility = undefined,
) {
return {
chatType,
hasOutstandingIOU: false,
isOwnPolicyExpenseChat,
isPinned: false,
lastActorEmail: '',
lastMessageHtml: '',
lastMessageText: null,
lastReadSequenceNumber: 0,
lastMessageTimestamp: 0,
lastVisitedTimestamp: 0,
maxSequenceNumber: 0,
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.DAILY,
oldPolicyName,
ownerEmail,
participants: participantList,
policyID,
reportID: generateReportID(),
reportName,
stateNum: 0,
statusNum: 0,
visibility,
};
}

/**
* Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically
* @param {String} ownerEmail
* @returns {Object}
*/
function buildOptimisticCreatedReportAction(ownerEmail) {
return {
0: {
actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
actorAccountID: currentUserAccountID,
message: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
text: ownerEmail === currentUserEmail ? 'You' : ownerEmail,
},
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
text: ' created this report',
},
],
person: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
text: lodashGet(allPersonalDetails, [currentUserEmail, 'displayName'], currentUserEmail),
},
],
automatic: false,
sequenceNumber: 0,
avatar: lodashGet(allPersonalDetails, [currentUserEmail, 'avatar'], getDefaultAvatar(currentUserEmail)),
timestamp: moment().unix(),
shouldShow: true,
},
};
}

/**
* @param {String} policyID
* @param {String} policyName
* @returns {Object}
*/
function buildOptimisticWorkspaceChats(policyID, policyName) {
const announceChatData = buildOptimisticChatReport(
[currentUserEmail],
CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE,
CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE,
policyID,
null,
false,
policyName,
);
const announceChatReportID = announceChatData.reportID;
const announceReportActionData = buildOptimisticCreatedReportAction(announceChatData.ownerEmail);

const adminsChatData = buildOptimisticChatReport([currentUserEmail], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policyID, null, false, policyName);
const adminsChatReportID = adminsChatData.reportID;
const adminsReportActionData = buildOptimisticCreatedReportAction(adminsChatData.ownerEmail);

const expenseChatData = buildOptimisticChatReport([currentUserEmail], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserEmail, true, policyName);
const expenseChatReportID = expenseChatData.reportID;
const expenseReportActionData = buildOptimisticCreatedReportAction(expenseChatData.ownerEmail);

return {
announceChatReportID,
announceChatData,
announceReportActionData,
adminsChatReportID,
adminsChatData,
adminsReportActionData,
expenseChatReportID,
expenseChatData,
expenseReportActionData,
};
}

/**
* @param {Object} report
* @returns {Boolean}
Expand Down Expand Up @@ -697,7 +882,11 @@ export {
navigateToDetailsPage,
generateReportID,
hasReportNameError,
isUnread,
buildOptimisticWorkspaceChats,
buildOptimisticChatReport,
buildOptimisticCreatedReportAction,
buildOptimisticIOUReport,
buildOptimisticIOUReportAction,
isUnread,
buildOptimisticReportAction,
};
3 changes: 2 additions & 1 deletion src/libs/actions/Policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as OptionsListUtils from '../OptionsListUtils';
import * as Report from './Report';
import * as Pusher from '../Pusher/pusher';
import DateUtils from '../DateUtils';
import * as ReportUtils from '../ReportUtils';

const allPolicies = {};
Onyx.connect({
Expand Down Expand Up @@ -814,7 +815,7 @@ function createWorkspace() {
expenseChatReportID,
expenseChatData,
expenseReportActionData,
} = Report.buildOptimisticWorkspaceChats(policyID, workspaceName);
} = ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName);

API.write('CreateWorkspace', {
policyID,
Expand Down
Loading

0 comments on commit 3780e0f

Please sign in to comment.