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
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,7 @@ const CONST = {
UNSHARE: 'UNSHARE', // OldDot Action
UPDATE_GROUP_CHAT_MEMBER_ROLE: 'UPDATEGROUPCHATMEMBERROLE',
CONCIERGE_CATEGORY_OPTIONS: 'CONCIERGECATEGORYOPTIONS',
CONCIERGE_DESCRIPTION_OPTIONS: 'CONCIERGEDESCRIPTIONOPTIONS',
CONCIERGE_AUTO_MAP_MCC_GROUPS: 'CONCIERGEAUTOMAPMCCGROUPS',
POLICY_CHANGE_LOG: {
ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE',
Expand Down
32 changes: 28 additions & 4 deletions src/libs/ModifiedExpenseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils';

let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
Onyx.connect({

Check warning on line 22 in src/libs/ModifiedExpenseMessage.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function

Check warning on line 22 in src/libs/ModifiedExpenseMessage.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_TAGS,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -241,11 +241,19 @@

const hasModifiedComment = isReportActionOriginalMessageAnObject && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage;
if (hasModifiedComment) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
let descriptionLabel = translateLocal('common.description');

// Add attribution suffix based on AI-generated descriptions
if (reportActionOriginalMessage?.aiGenerated) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
descriptionLabel += ` ${translateLocal('iou.basedOnAI')}`;
}

buildMessageFragmentForValue(
Parser.htmlToMarkdown(reportActionOriginalMessage?.newComment ?? ''),
Parser.htmlToMarkdown(reportActionOriginalMessage?.oldComment ?? ''),
// eslint-disable-next-line @typescript-eslint/no-deprecated
translateLocal('common.description'),
descriptionLabel,
true,
setFragments,
removalFragments,
Expand Down Expand Up @@ -398,6 +406,14 @@
// eslint-disable-next-line @typescript-eslint/no-deprecated
getMessageLine(`\n${translateLocal('iou.removed')}`, removalFragments);
if (message === '') {
// If we don't have enough structured information to build a detailed message but we
// know the change was AI-generated, fall back to an AI-attributed generic summary so
// users can still understand that Concierge updated the expense automatically.
if (reportActionOriginalMessage?.aiGenerated) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return `${translateLocal('iou.changedTheExpense')} ${translateLocal('iou.basedOnAI')}`;
}

// eslint-disable-next-line @typescript-eslint/no-deprecated
return translateLocal('iou.changedTheExpense');
}
Expand Down Expand Up @@ -463,11 +479,19 @@

const hasModifiedComment = isReportActionOriginalMessageAnObject && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage;
if (hasModifiedComment) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
let descriptionLabel = translateLocal('common.description');

// Add attribution suffix based on AI-generated descriptions
if (reportActionOriginalMessage?.aiGenerated) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
descriptionLabel += ` ${translateLocal('iou.basedOnAI')}`;
}

buildMessageFragmentForValue(
Parser.htmlToMarkdown(reportActionOriginalMessage?.newComment ?? ''),
Parser.htmlToMarkdown(reportActionOriginalMessage?.oldComment ?? ''),
// eslint-disable-next-line @typescript-eslint/no-deprecated
translateLocal('common.description'),
descriptionLabel,
true,
setFragments,
removalFragments,
Expand Down
17 changes: 16 additions & 1 deletion src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement;

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 60 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -69,7 +69,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 72 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -78,14 +78,14 @@
});

let isNetworkOffline = false;
Onyx.connect({

Check warning on line 81 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NETWORK,
callback: (val) => (isNetworkOffline = val?.isOffline ?? false),
});

let currentUserAccountID: number | undefined;
let currentEmail = '';
Onyx.connect({

Check warning on line 88 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, value is undefined
Expand All @@ -99,7 +99,7 @@
});

let privatePersonalDetails: PrivatePersonalDetails | undefined;
Onyx.connect({

Check warning on line 102 in src/libs/ReportActionsUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
callback: (personalDetails) => {
privatePersonalDetails = personalDetails;
Expand Down Expand Up @@ -871,6 +871,15 @@
return !!selectedCategory;
}

/**
* Checks whether an action is concierge description options and resolved.
*/
function isResolvedConciergeDescriptionOptions(reportAction: OnyxEntry<ReportAction>): boolean {
const originalMessage = getOriginalMessage(reportAction);
const selectedDescription = originalMessage && typeof originalMessage === 'object' && 'selectedDescription' in originalMessage ? originalMessage?.selectedDescription : null;
return !!selectedDescription;
}

/**
* Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid
* and supported type, it's not deleted and also not closed.
Expand Down Expand Up @@ -1937,7 +1946,7 @@
return [{text: message, html: `<muted-text>${message}</muted-text>`, type: 'COMMENT'}];
}

if (isConciergeCategoryOptions(action)) {
if (isConciergeCategoryOptions(action) || isConciergeDescriptionOptions(action)) {
const message = getReportActionMessageText(action);
return [{text: message, html: message, type: 'COMMENT'}];
}
Expand Down Expand Up @@ -2038,6 +2047,10 @@
return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_CATEGORY_OPTIONS);
}

function isConciergeDescriptionOptions(reportAction: OnyxEntry<ReportAction>): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.CONCIERGE_DESCRIPTION_OPTIONS> {
return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_DESCRIPTION_OPTIONS);
}

function getActionableJoinRequestPendingReportAction(reportID: string): OnyxEntry<ReportAction> {
const findPendingRequest = Object.values(getAllReportActions(reportID)).find((reportActionItem) => isActionableJoinRequestPendingReportAction(reportActionItem));
return findPendingRequest;
Expand Down Expand Up @@ -3384,7 +3397,9 @@
isActionableTrackExpense,
isExpenseChatWelcomeWhisper,
isConciergeCategoryOptions,
isConciergeDescriptionOptions,
isResolvedConciergeCategoryOptions,
isResolvedConciergeDescriptionOptions,
isAddCommentAction,
isApprovedOrSubmittedReportAction,
isIOURequestReportAction,
Expand Down
59 changes: 52 additions & 7 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
let currentUserAccountID = -1;
let currentUserEmail: string | undefined;

Onyx.connect({

Check warning on line 270 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -280,13 +280,13 @@
},
});

Onyx.connect({

Check warning on line 283 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => (conciergeReportID = value),
});

let preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE;
Onyx.connect({

Check warning on line 289 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
callback: (value) => {
preferredSkinTone = EmojiUtils.getPreferredSkinToneIndex(value);
Expand All @@ -296,7 +296,7 @@
// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection<ReportActions> = {};

Onyx.connect({

Check warning on line 299 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
if (!key || !actions) {
Expand Down Expand Up @@ -5977,35 +5977,79 @@
}

/**
* Resolves Concierge category options by adding a comment and updating the report action
* Generic helper function to resolve Concierge AI-suggested options (category or description)
* @param reportID - The report ID where the comment should be added and the report action should be updated
* @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report.
* @param notifyReportID - The report ID we should notify for new actions
* @param reportActionID - The specific report action ID to update
* @param selectedCategory - The category selected by the user
* @param selectedValue - The value selected by the user
* @param timezoneParam - The user's timezone
* @param selectedField - The field to update in the original message ('selectedCategory' or 'selectedDescription')
* @param ancestors - Array of ancestor reports for proper threading
*/
function resolveConciergeCategoryOptions(
function resolveConciergeOptions(
reportID: string | undefined,
notifyReportID: string | undefined,
reportActionID: string | undefined,
selectedCategory: string,
selectedValue: string,
timezoneParam: Timezone,
selectedField: 'selectedCategory' | 'selectedDescription',
ancestors: Ancestor[] = [],
) {
if (!reportID || !reportActionID) {
return;
}

addComment(reportID, notifyReportID ?? reportID, ancestors, selectedCategory, timezoneParam);
addComment(reportID, notifyReportID ?? reportID, ancestors, selectedValue, timezoneParam);

Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
[reportActionID]: {
originalMessage: {
selectedCategory,
[selectedField]: selectedValue,
},
},
} as Partial<ReportActions>);
}

/**
* Resolves Concierge category options by adding a comment and updating the report action
* @param reportID - The report ID where the comment should be added and the report action should be updated
* @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report.
* @param reportActionID - The specific report action ID to update
* @param selectedCategory - The category selected by the user
* @param timezoneParam - The user's timezone
* @param ancestors - Array of ancestor reports for proper threading
*/
function resolveConciergeCategoryOptions(
reportID: string | undefined,
notifyReportID: string | undefined,
reportActionID: string | undefined,
selectedCategory: string,
timezoneParam: Timezone,
ancestors: Ancestor[] = [],
) {
resolveConciergeOptions(reportID, notifyReportID, reportActionID, selectedCategory, timezoneParam, 'selectedCategory', ancestors);
}

/**
* Resolves Concierge description options by adding a comment and updating the report action
* @param reportID - The report ID where the comment should be added and the report action should be updated
* @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report.
* @param reportActionID - The specific report action ID to update
* @param selectedDescription - The description selected by the user
* @param timezoneParam - The user's timezone
* @param ancestors - Array of ancestor reports for proper threading
*/
function resolveConciergeDescriptionOptions(
reportID: string | undefined,
notifyReportID: string | undefined,
reportActionID: string | undefined,
selectedDescription: string,
timezoneParam: Timezone,
ancestors: Ancestor[] = [],
) {
resolveConciergeOptions(reportID, notifyReportID, reportActionID, selectedDescription, timezoneParam, 'selectedDescription', ancestors);
}

/**
* Enhances existing transaction thread reports with additional context for navigation
*
Expand Down Expand Up @@ -6104,6 +6148,7 @@
resolveActionableMentionConfirmWhisper,
resolveActionableReportMentionWhisper,
resolveConciergeCategoryOptions,
resolveConciergeDescriptionOptions,
savePrivateNotesDraft,
saveReportActionDraft,
saveReportDraftComment,
Expand Down
44 changes: 41 additions & 3 deletions src/pages/home/report/PureReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
isCardIssuedAction,
isChronosOOOListAction,
isConciergeCategoryOptions,
isConciergeDescriptionOptions,
isCreatedTaskReportAction,
isDeletedAction,
isDeletedParentAction as isDeletedParentActionUtils,
Expand All @@ -130,6 +131,7 @@ import {
isReimbursementQueuedAction,
isRenamedAction,
isResolvedConciergeCategoryOptions,
isResolvedConciergeDescriptionOptions,
isSplitBillAction as isSplitBillActionReportActionsUtils,
isTagModificationAction,
isTaskAction,
Expand Down Expand Up @@ -171,7 +173,13 @@ import {openPersonalBankAccountSetupView} from '@userActions/BankAccounts';
import {resolveFraudAlert} from '@userActions/Card';
import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction';
import {acceptJoinRequest, declineJoinRequest} from '@userActions/Policy/Member';
import {createTransactionThreadReport, expandURLPreview, resolveActionableMentionConfirmWhisper, resolveConciergeCategoryOptions} from '@userActions/Report';
import {
createTransactionThreadReport,
expandURLPreview,
resolveActionableMentionConfirmWhisper,
resolveConciergeCategoryOptions,
resolveConciergeDescriptionOptions,
} from '@userActions/Report';
import type {IgnoreDirection} from '@userActions/ReportActions';
import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session';
import {isBlockedFromConcierge} from '@userActions/User';
Expand Down Expand Up @@ -776,6 +784,29 @@ function PureReportActionItem({
}));
}

if (isConciergeDescriptionOptions(action)) {
const options = getOriginalMessage(action)?.options;
if (!options) {
return [];
}

if (isResolvedConciergeDescriptionOptions(action)) {
return [];
}

if (!reportActionReportID) {
return [];
}

return options.map((option, i) => ({
text: `${i + 1} - ${option}`,
key: `${action.reportActionID}-conciergeDescriptionOptions-${option}`,
onPress: () => {
resolveConciergeDescriptionOptions(reportActionReportID, reportID, action.reportActionID, option, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE);
},
}));
}

if (!isActionableWhisper && !isActionableCardFraudAlert(action) && (!isActionableJoinRequest(action) || getOriginalMessage(action)?.choice !== ('' as JoinWorkspaceResolution))) {
return [];
}
Expand Down Expand Up @@ -1464,8 +1495,15 @@ function PureReportActionItem({
{actionableItemButtons.length > 0 && (
<ActionableItemButtons
items={actionableItemButtons}
layout={isActionableTrackExpense(action) || isConciergeCategoryOptions(action) || isActionableMentionWhisper(action) ? 'vertical' : 'horizontal'}
shouldUseLocalization={!isConciergeCategoryOptions(action)}
layout={
isActionableTrackExpense(action) ||
isConciergeCategoryOptions(action) ||
isConciergeDescriptionOptions(action) ||
isActionableMentionWhisper(action)
? 'vertical'
: 'horizontal'
}
shouldUseLocalization={!isConciergeCategoryOptions(action) && !isConciergeDescriptionOptions(action)}
/>
)}
</View>
Expand Down
24 changes: 20 additions & 4 deletions src/types/onyx/OriginalMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,10 @@ type OriginalMessageModifiedExpense = {
newAttendees?: Attendee[];

/** Source of category change (agentZero, mccMapping, or manual) */
source?: string;
source?: ValueOf<typeof CONST.CATEGORY_SOURCE>;

/** Whether the updated description was generated by AI */
aiGenerated?: boolean;
};

/** Model of a `travel update` report action */
Expand All @@ -690,24 +693,36 @@ type OriginalMessageDeletedTransaction = {
currency?: string;
};

/** Model of `concierge category options` report action */
type OriginalMessageConciergeCategoryOptions = {
/** The options we present to the user when confidence in the predicted category is low */
/** Common model for Concierge category/description options actions */
type OriginalMessageConciergeBaseOptions = {
/** The options we present to the user when confidence in the prediction is low */
options: string[];

/** The confidence levels for each option */
confidenceLevels?: number[];

/** The transaction ID associated with this action */
transactionID?: string;
};

/** Model of `concierge category options` report action */
type OriginalMessageConciergeCategoryOptions = OriginalMessageConciergeBaseOptions & {
/** The category selected by the user (set when the action is resolved) */
selectedCategory?: string;

/** Agent Zero metadata (optional) */
agentZero?: Record<string, unknown>;
};

/** Model of `concierge description options` report action */
type OriginalMessageConciergeDescriptionOptions = OriginalMessageConciergeBaseOptions & {
/** The description selected by the user (set when the action is resolved) */
selectedDescription?: string;

/** Agent Zero metadata (optional) */
agentZero?: Record<string, unknown>;
};

/** Model of `concierge auto map mcc groups` report action */
type OriginalMessageConciergeAutoMapMccGroups = {
/** The policy ID for which MCC groups were auto-mapped */
Expand Down Expand Up @@ -1089,6 +1104,7 @@ type OriginalMessageMap = {
[CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED]: OriginalMessageIntegrationSyncFailed;
[CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION]: OriginalMessageDeletedTransaction;
[CONST.REPORT.ACTIONS.TYPE.CONCIERGE_CATEGORY_OPTIONS]: OriginalMessageConciergeCategoryOptions;
[CONST.REPORT.ACTIONS.TYPE.CONCIERGE_DESCRIPTION_OPTIONS]: OriginalMessageConciergeDescriptionOptions;
[CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_MAP_MCC_GROUPS]: OriginalMessageConciergeAutoMapMccGroups;
[CONST.REPORT.ACTIONS.TYPE.RETRACTED]: never;
[CONST.REPORT.ACTIONS.TYPE.REOPENED]: never;
Expand Down
Loading
Loading