, policy: OnyxEntry, policy: OnyxEntry | undefined = undefined): string {
- const moneyRequestTotal = getMoneyRequestReimbursableTotal(report);
+ const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID));
const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', {
@@ -2154,7 +2107,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string
return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', {
formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency, TransactionUtils.isDistanceRequest(transaction)) ?? '',
- comment: transactionDetails?.comment ?? '',
+ comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '',
});
}
@@ -2201,7 +2154,7 @@ function getReportPreviewMessage(
}
}
- const totalAmount = getMoneyRequestReimbursableTotal(report);
+ const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const policyName = getPolicyName(report, false, policy);
const payerName = isExpenseReport(report) ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport);
@@ -2316,6 +2269,54 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, parentReportActionMessage: string) {
+ if (!parentReportAction?.originalMessage) {
+ return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage');
+ }
+ const originalMessage = isChangeLogObject(parentReportAction.originalMessage);
+ const participantAccountIDs = originalMessage?.targetAccountIDs ?? [];
+
+ const participants = participantAccountIDs.map((id) => {
+ const name = getDisplayNameForParticipant(id);
+ if (name && name?.length > 0) {
+ return name;
+ }
+ return Localize.translateLocal('common.hidden');
+ });
+ const users = participants.length > 1 ? participants.join(` ${Localize.translateLocal('common.and')} `) : participants[0];
+ if (!users) {
+ return parentReportActionMessage;
+ }
+ const actionType = parentReportAction.actionName;
+ const isInviteAction = actionType === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || actionType === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM;
+
+ const verbKey = isInviteAction ? 'workspace.invite.invited' : 'workspace.invite.removed';
+ const prepositionKey = isInviteAction ? 'workspace.invite.to' : 'workspace.invite.from';
+
+ const verb = Localize.translateLocal(verbKey);
+ const preposition = Localize.translateLocal(prepositionKey);
+
+ const roomName = originalMessage?.roomName ?? '';
+
+ return roomName ? `${verb} ${users} ${preposition} ${roomName}` : `${verb} ${users}`;
+}
+
/**
* Get the title for a report.
*/
@@ -2338,6 +2339,9 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu
) {
return Localize.translateLocal('parentReportAction.hiddenMessage');
}
+ if (isAdminRoom(report) || isUserCreatedPolicyRoom(report)) {
+ return getAdminRoomInvitedParticipants(parentReportAction, parentReportActionMessage);
+ }
return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage');
}
@@ -2497,12 +2501,13 @@ function getParsedComment(text: string): string {
return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text);
}
-function buildOptimisticAddCommentReportAction(text?: string, file?: File): OptimisticReportAction {
+function buildOptimisticAddCommentReportAction(text?: string, file?: File, actorAccountID?: number): OptimisticReportAction {
const parser = new ExpensiMark();
const commentText = getParsedComment(text ?? '');
const isAttachment = !text && file !== undefined;
const attachmentInfo = isAttachment ? file : {};
const htmlForNewComment = isAttachment ? CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML : commentText;
+ const accountID = actorAccountID ?? currentUserAccountID;
// Remove HTML from text when applying optimistic offline comment
const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment);
@@ -2511,16 +2516,16 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File): Opti
reportAction: {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
- actorAccountID: currentUserAccountID,
+ actorAccountID: accountID,
person: [
{
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? -1]?.displayName ?? currentUserEmail,
+ text: allPersonalDetails?.[accountID ?? -1]?.displayName ?? currentUserEmail,
type: 'TEXT',
},
],
automatic: false,
- avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: allPersonalDetails?.[accountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(accountID),
created: DateUtils.getDBTimeWithSkew(),
message: [
{
@@ -2744,7 +2749,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num
const report = getReport(iouReportID);
const amount =
type === CONST.IOU.REPORT_ACTION_TYPE.PAY
- ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(!isEmptyObject(report) ? report : null), currency)
+ ? CurrencyUtils.convertToDisplayString(getMoneyRequestSpendBreakdown(!isEmptyObject(report) ? report : null).totalDisplaySpend, currency)
: CurrencyUtils.convertToDisplayString(total, currency);
let paymentMethodMessage;
@@ -2816,7 +2821,7 @@ function buildOptimisticIOUReportAction(
comment: string,
participants: Participant[],
transactionID: string,
- paymentType: DeepValueOf,
+ paymentType: PaymentMethodType,
iouReportID = '',
isSettlingUp = false,
isSendMoneyFlow = false,
@@ -3546,8 +3551,8 @@ function shouldHideReport(report: OnyxEntry, currentReportId: string): b
/**
* Checks to see if a report's parentAction is a money request that contains a violation
*/
-function doesTransactionThreadHaveViolations(report: Report, transactionViolations: OnyxCollection, parentReportAction: ReportAction): boolean {
- if (parentReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) {
+function doesTransactionThreadHaveViolations(report: OnyxEntry, transactionViolations: OnyxCollection, parentReportAction: OnyxEntry): boolean {
+ if (parentReportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) {
return false;
}
const {IOUTransactionID, IOUReportID} = parentReportAction.originalMessage ?? {};
@@ -3557,7 +3562,7 @@ function doesTransactionThreadHaveViolations(report: Report, transactionViolatio
if (!isCurrentUserSubmitter(IOUReportID)) {
return false;
}
- if (report.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) {
+ if (report?.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report?.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) {
return false;
}
return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations);
@@ -3660,7 +3665,7 @@ function shouldReportBeInOptionList({
// All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones
if (isInGSDMode) {
- return isUnread(report);
+ return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE;
}
// Archived reports should always be shown when in default (most recent) mode. This is because you should still be able to access and search for the chats to find them.
@@ -4185,9 +4190,8 @@ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry): boolean {
);
}
+/**
+ * Assume any report without a reportID is unusable.
+ */
+function isValidReport(report?: OnyxEntry): boolean {
+ return Boolean(report?.reportID);
+}
+
+/**
+ * Check to see if we are a participant of this report.
+ */
+function isReportParticipant(accountID: number, report: OnyxEntry): boolean {
+ if (!accountID) {
+ return false;
+ }
+
+ // If we have a DM AND the accountID we are checking is the current user THEN we won't find them as a participant and must assume they are a participant
+ if (isDM(report) && accountID === currentUserAccountID) {
+ return true;
+ }
+
+ const possibleAccountIDs = report?.participantAccountIDs ?? [];
+ if (report?.ownerAccountID) {
+ possibleAccountIDs.push(report?.ownerAccountID);
+ }
+ if (report?.managerID) {
+ possibleAccountIDs.push(report?.managerID);
+ }
+ return possibleAccountIDs.includes(accountID);
+}
+
function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean {
return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report);
}
@@ -4523,6 +4557,13 @@ function getReportFieldTitle(report: OnyxEntry, reportField: PolicyRepor
});
}
+/**
+ * Given a report field, check if the field is for the report title.
+ */
+function isReportFieldOfTypeTitle(reportField: PolicyReportField): boolean {
+ return reportField.type === 'formula' && reportField.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID;
+}
+
/**
* Checks if thread replies should be displayed
*/
@@ -4540,7 +4581,7 @@ function shouldDisplayThreadReplies(reportAction: OnyxEntry, repor
* - The action is a whisper action and it's neither a report preview nor IOU action
* - The action is the thread's first chat
*/
-function shouldDisableThread(reportAction: OnyxEntry, reportID: string) {
+function shouldDisableThread(reportAction: OnyxEntry, reportID: string): boolean {
const isSplitBillAction = ReportActionsUtils.isSplitBillAction(reportAction);
const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction);
const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction);
@@ -4556,10 +4597,27 @@ function shouldDisableThread(reportAction: OnyxEntry, reportID: st
);
}
+function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry = null): boolean {
+ if (!policy) {
+ return false;
+ }
+ type CurrencyType = (typeof CONST.DIRECT_REIMBURSEMENT_CURRENCIES)[number];
+ const reimbursableTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
+ const autoReimbursementLimit = policy.autoReimbursementLimit ?? 0;
+ const isAutoReimbursable =
+ isGroupPolicy(report) &&
+ policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES &&
+ autoReimbursementLimit >= reimbursableTotal &&
+ reimbursableTotal > 0 &&
+ CONST.DIRECT_REIMBURSEMENT_CURRENCIES.includes(report?.currency as CurrencyType);
+ return isAutoReimbursable;
+}
+
export {
getReportParticipantsTitle,
isReportMessageAttachment,
findLastAccessedReport,
+ canBeAutoReimbursed,
canEditReportAction,
canFlagReportAction,
shouldShowFlagComment,
@@ -4591,7 +4649,6 @@ export {
hasExpensifyGuidesEmails,
requiresAttentionFromCurrentUser,
isIOUOwnedByCurrentUser,
- getMoneyRequestReimbursableTotal,
getMoneyRequestSpendBreakdown,
canShowReportRecipientLocalTime,
formatReportLastMessageText,
@@ -4738,6 +4795,9 @@ export {
shouldDisableThread,
doesReportBelongToWorkspace,
getChildReportNotificationPreference,
+ isReportParticipant,
+ isValidReport,
+ isReportFieldOfTypeTitle,
};
export type {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 445d9dc30dd8..9119907e9393 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -1,22 +1,21 @@
/* eslint-disable rulesdir/prefer-underscore-method */
import Str from 'expensify-common/lib/str';
-import type {OnyxCollection} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetails, TransactionViolation} from '@src/types/onyx';
+import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx';
import type Beta from '@src/types/onyx/Beta';
-import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type Policy from '@src/types/onyx/Policy';
import type Report from '@src/types/onyx/Report';
import type {ReportActions} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
import * as CollectionUtils from './CollectionUtils';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import * as OptionsListUtils from './OptionsListUtils';
-import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
import * as ReportUtils from './ReportUtils';
import * as TaskUtils from './TaskUtils';
@@ -49,21 +48,6 @@ Onyx.connect({
},
});
-// Session can remain stale because the only way for the current user to change is to
-// sign out and sign in, which would clear out all the Onyx
-// data anyway and cause SidebarLinks to rerender.
-let currentUserAccountID: number | undefined;
-Onyx.connect({
- key: ONYXKEYS.SESSION,
- callback: (session) => {
- if (!session) {
- return;
- }
-
- currentUserAccountID = session.accountID;
- },
-});
-
let resolveSidebarIsReadyPromise: (args?: unknown[]) => void;
let sidebarIsReadyPromise = new Promise((resolve) => {
@@ -147,6 +131,7 @@ function getOrderedReportIDs(
const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD;
const isInDefaultMode = !isInGSDMode;
const allReportsDictValues = Object.values(allReports);
+
// Filter out all the reports that shouldn't be displayed
let reportsToDisplay = allReportsDictValues.filter((report) => {
const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`;
@@ -199,7 +184,7 @@ function getOrderedReportIDs(
report.displayName = ReportUtils.getReportName(report);
// eslint-disable-next-line no-param-reassign
- report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReports);
+ report.iouReportAmount = ReportUtils.getMoneyRequestSpendBreakdown(report, allReports).totalDisplaySpend;
const isPinned = report.isPinned ?? false;
const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? '');
@@ -238,13 +223,6 @@ function getOrderedReportIDs(
return LHNReports;
}
-type ActorDetails = {
- displayName?: string;
- firstName?: string;
- lastName?: string;
- accountID?: number;
-};
-
/**
* Gets all the data necessary for rendering an OptionRowLHN component
*/
@@ -257,12 +235,12 @@ function getOptionData({
parentReportAction,
hasViolations,
}: {
- report: Report;
- reportActions: Record;
- personalDetails: Record