Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB

Suggested change
* Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically
* Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically

@Julesssss Julesssss Sep 23, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB, but maybe we should use this opportunity to better align the function comments, which are quite different:

  • Builds an optimistic chat report
  • Returns the necessary reportAction onyx data

* @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 @@ -807,7 +808,7 @@ function createWorkspace() {
expenseChatReportID,
expenseChatData,
expenseReportActionData,
} = Report.buildOptimisticWorkspaceChats(policyID, workspaceName);
} = ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName);

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