Skip to content
Draft
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/libs/API/parameters/AddCommentOrAttachmentParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type AddCommentOrAttachmentParams = {
clientCreatedTime?: string;
isOldDotConciergeChat?: boolean;
idempotencyKey?: string;
pageHTML?: string;
};

export default AddCommentOrAttachmentParams;
10 changes: 10 additions & 0 deletions src/libs/PageHTMLCapture/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Captures simplified HTML content from the main app window.
* On native platforms, this is not applicable, so we return an empty string.
* @returns Empty string on native platforms
*/
function capturePageHTML(): string {
return '';
}

export default capturePageHTML;
151 changes: 151 additions & 0 deletions src/libs/PageHTMLCapture/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Navigation from '@libs/Navigation/Navigation';

/**
* Captures simplified HTML content from the main app window.
* Extracts semantic HTML elements, visible text, and useful attributes.
* @returns Simplified HTML string wrapped with page URL
*/
function capturePageHTML(): string {
try {
// Get the current page URL
const currentPath = Navigation.getActiveRoute();
const pageURL = currentPath || '/';

// Get the main content container (excluding the side panel)
const mainContent = document.querySelector<HTMLElement>('#root');
if (!mainContent) {
return '';
}

// Extract simplified HTML
const simplifiedHTML = extractSimplifiedHTML(mainContent);

// Wrap with page URL
return `<page url="${pageURL}">\n${simplifiedHTML}\n</page>`;
} catch (error) {
console.error('[PageHTMLCapture] Error capturing page HTML:', error);
return '';
}
}

/**
* Recursively extracts simplified HTML from an element.
* Includes semantic elements, visible text, and useful attributes.
*/
function extractSimplifiedHTML(element: HTMLElement): string {
const result: string[] = [];

// Skip side panel and modal content
if (
element.classList.contains('side-panel') ||
element.getAttribute('data-testid') === 'side-panel' ||
element.getAttribute('role') === 'dialog' ||
element.classList.contains('modal')
) {
return '';
}

// Process child nodes
const childNodes = Array.from(element.childNodes);
for (const node of childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
// Include visible text content
const text = node.textContent?.trim();
if (text) {
result.push(text);
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
const childElement = node as HTMLElement;

// Skip hidden elements
const style = window.getComputedStyle(childElement);
if (style.display === 'none' || style.visibility === 'hidden') {
continue;
}

const tagName = childElement.tagName.toLowerCase();

// Check if this is a semantic element we want to capture
const semanticElements = [
'button',
'a',
'input',
'textarea',
'select',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'nav',
'header',
'footer',
'article',
'section',
'aside',
'main',
'form',
'label',
'img',
];

if (semanticElements.includes(tagName)) {
// Build simplified element representation
const attrs: string[] = [];

// Include useful attributes
const ariaLabel = childElement.getAttribute('aria-label');
if (ariaLabel) {
attrs.push(`aria-label="${ariaLabel}"`);
}

const href = childElement.getAttribute('href');
if (href && tagName === 'a') {
attrs.push(`href="${href}"`);
}

const alt = childElement.getAttribute('alt');
if (alt && tagName === 'img') {
attrs.push(`alt="${alt}"`);
}

const placeholder = childElement.getAttribute('placeholder');
if (placeholder && (tagName === 'input' || tagName === 'textarea')) {
attrs.push(`placeholder="${placeholder}"`);
}

const type = childElement.getAttribute('type');
if (type && tagName === 'input') {
attrs.push(`type="${type}"`);
}

const value = (childElement as HTMLInputElement).value;
if (value && (tagName === 'input' || tagName === 'textarea')) {
attrs.push(`value="${value}"`);
}

// Get inner content
const innerContent = extractSimplifiedHTML(childElement);

// Build element string
const attrString = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
if (innerContent) {
result.push(`<${tagName}${attrString}>${innerContent}</${tagName}>`);
} else {
result.push(`<${tagName}${attrString} />`);
}
} else {
// For non-semantic elements, just extract their content
const innerContent = extractSimplifiedHTML(childElement);
if (innerContent) {
result.push(innerContent);
}
}
}
}

return result.join(' ');
}

export default capturePageHTML;
43 changes: 25 additions & 18 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils';
import LocalNotification from '@libs/Notification/LocalNotification';
import {rand64} from '@libs/NumberUtils';
import capturePageHTML from '@libs/PageHTMLCapture';
import Parser from '@libs/Parser';
import {getParsedMessageWithShortMentions} from '@libs/ParsingUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -150,6 +151,7 @@
getReportViolations,
getTitleReportField,
hasOutstandingChildRequest,
isAdminRoom,
isChatThread as isChatThreadReportUtils,
isConciergeChatReport,
isCurrentUserSubmitter,
Expand Down Expand Up @@ -276,7 +278,7 @@
let currentUserAccountID = -1;
let currentUserEmail: string | undefined;

Onyx.connect({

Check warning on line 281 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 @@ -289,7 +291,7 @@
},
});

Onyx.connect({

Check warning on line 294 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),
});
Expand All @@ -297,7 +299,7 @@
// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection<ReportActions> = {};

Onyx.connect({

Check warning on line 302 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 All @@ -309,14 +311,14 @@
});

let allTransactionViolations: OnyxCollection<TransactionViolations> = {};
Onyx.connect({

Check warning on line 314 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.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => (allTransactionViolations = value),
});

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

Check warning on line 321 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,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -325,7 +327,7 @@
});

let allPersonalDetails: OnyxEntry<PersonalDetailsList> = {};
Onyx.connect({

Check warning on line 330 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.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand All @@ -340,7 +342,7 @@
});

let onboarding: OnyxEntry<Onboarding>;
Onyx.connect({

Check warning on line 345 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.NVP_ONBOARDING,
callback: (val) => {
if (Array.isArray(val)) {
Expand All @@ -351,13 +353,13 @@
});

let introSelected: OnyxEntry<IntroSelected> = {};
Onyx.connect({

Check warning on line 356 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.NVP_INTRO_SELECTED,
callback: (val) => (introSelected = val),
});

let allReportDraftComments: Record<string, string | undefined> = {};
Onyx.connect({

Check warning on line 362 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_DRAFT_COMMENT,
waitForCollectionCallback: true,
callback: (value) => (allReportDraftComments = value),
Expand Down Expand Up @@ -550,8 +552,9 @@
*
* @param reportID - The report ID where the comment should be added
* @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 isInSidePanel - Whether the comment is being added from the side panel
*/
function addActions(reportID: string, notifyReportID: string, ancestors: Ancestor[], timezoneParam: Timezone, text = '', file?: FileObject) {
function addActions(reportID: string, notifyReportID: string, ancestors: Ancestor[], timezoneParam: Timezone, text = '', file?: FileObject, isInSidePanel = false) {
let reportCommentText = '';
let reportCommentAction: OptimisticAddCommentReportAction | undefined;
let attachmentAction: OptimisticAddCommentReportAction | undefined;
Expand Down Expand Up @@ -625,6 +628,14 @@
parameters.isOldDotConciergeChat = true;
}

// Capture page HTML when sending from side panel to Concierge or admins room
if (isInSidePanel && (isConciergeChatReport(report) || isAdminRoom(report))) {
const pageHTML = capturePageHTML();
if (pageHTML) {
parameters.pageHTML = pageHTML;
}
}

const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -722,6 +733,7 @@
text = '',
timezone: Timezone = CONST.DEFAULT_TIME_ZONE,
shouldPlaySound = false,
isInSidePanel = false,
) {
if (!reportID) {
return;
Expand All @@ -736,29 +748,29 @@

// Single attachment
if (!Array.isArray(attachments)) {
addActions(reportID, notifyReportID, ancestors, timezone, text, attachments);
addActions(reportID, notifyReportID, ancestors, timezone, text, attachments, isInSidePanel);
handlePlaySound();
return;
}

// Multiple attachments - first: combine text + first attachment as a single action
addActions(reportID, notifyReportID, ancestors, timezone, text, attachments?.at(0));
addActions(reportID, notifyReportID, ancestors, timezone, text, attachments?.at(0), isInSidePanel);

// Remaining: attachment-only actions (no text duplication)
for (let i = 1; i < attachments?.length; i += 1) {
addActions(reportID, notifyReportID, ancestors, timezone, '', attachments?.at(i));
addActions(reportID, notifyReportID, ancestors, timezone, '', attachments?.at(i), isInSidePanel);
}

// Play sound once
handlePlaySound();
}

/** Add a single comment to a report */
function addComment(reportID: string, notifyReportID: string, ancestors: Ancestor[], text: string, timezoneParam: Timezone, shouldPlaySound?: boolean) {
function addComment(reportID: string, notifyReportID: string, ancestors: Ancestor[], text: string, timezoneParam: Timezone, shouldPlaySound?: boolean, isInSidePanel?: boolean) {
if (shouldPlaySound) {
playSound(SOUNDS.DONE);
}
addActions(reportID, notifyReportID, ancestors, timezoneParam, text);
addActions(reportID, notifyReportID, ancestors, timezoneParam, text, undefined, isInSidePanel);
}

function reportActionsExist(reportID: string): boolean {
Expand Down Expand Up @@ -967,7 +979,6 @@
transaction?: Transaction,
transactionViolations?: TransactionViolations,
parentReportID?: string,
shouldAddPendingFields = true,
optimisticSelfDMReport?: Report,
) {
if (!reportID) {
Expand Down Expand Up @@ -1050,7 +1061,6 @@
accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '',
parentReportActionID,
transactionID: transaction?.transactionID,
includePartiallySetupBankAccounts: isConciergeChatReport(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]),
};

if (optimisticSelfDMReport) {
Expand Down Expand Up @@ -1234,12 +1244,10 @@
...optimisticReport,
reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
...newReportObject,
pendingFields: shouldAddPendingFields
? {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
...(isGroupChat && {reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}),
}
: undefined,
pendingFields: {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
...(isGroupChat && {reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}),
},
};
}

Expand All @@ -1252,7 +1260,7 @@
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[optimisticCreatedAction.reportActionID]: {...optimisticCreatedAction, pendingAction: shouldAddPendingFields ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}},
value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction},
},
{
onyxMethod: Onyx.METHOD.SET,
Expand Down Expand Up @@ -1441,7 +1449,6 @@

const optimisticTransactionThreadReportID = generateReportID();
const optimisticTransactionThread = buildTransactionThread(iouReportAction, reportToUse, undefined, optimisticTransactionThreadReportID);
const shouldAddPendingFields = transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || iouReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
openReport(
optimisticTransactionThreadReportID,
undefined,
Expand All @@ -1455,7 +1462,6 @@
transaction,
transactionViolations,
selfDMReportID,
shouldAddPendingFields,
optimisticSelfDMReport,
);
return optimisticTransactionThread;
Expand Down Expand Up @@ -3078,7 +3084,7 @@
const reportActionID = rand64();
const reportPreviewReportActionID = rand64();

const {parentReportID, reportPreviewAction, optimisticData, successData, failureData, optimisticReportData} = buildNewReportOptimisticData(
const {optimisticReportName, parentReportID, reportPreviewAction, optimisticData, successData, failureData, optimisticReportData} = buildNewReportOptimisticData(
policy,
optimisticReportID,
reportActionID,
Expand All @@ -3095,6 +3101,7 @@
API.write(
WRITE_COMMANDS.CREATE_APP_REPORT,
{
reportName: optimisticReportName,
type: CONST.REPORT.TYPE.EXPENSE,
policyID: policy?.id,
reportID: optimisticReportID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@

/** Whether the main composer was hidden */
didHideComposerInput?: boolean;

/** Whether the report screen is being displayed in the side panel */
isInSidePanel?: boolean;
};

// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
Expand All @@ -136,6 +139,7 @@
didHideComposerInput,
reportTransactions,
transactionThreadReportID,
isInSidePanel = false,
}: ReportActionComposeProps) {
const styles = useThemeStyles();
const theme = useTheme();
Expand Down Expand Up @@ -332,7 +336,16 @@
}

if (attachmentFileRef.current) {
addAttachmentWithComment(transactionThreadReportID ?? reportID, reportID, ancestors, attachmentFileRef.current, newCommentTrimmed, personalDetail.timezone, true);
addAttachmentWithComment(
transactionThreadReportID ?? reportID,
reportID,
ancestors,
attachmentFileRef.current,
newCommentTrimmed,
personalDetail.timezone,
true,
isInSidePanel,
);
attachmentFileRef.current = null;
} else {
Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
Expand All @@ -348,7 +361,7 @@
onSubmit(newCommentTrimmed);
}
},
[onSubmit, ancestors, kickoffWaitingIndicator, isConciergeChat, reportID, personalDetail.timezone, transactionThreadReportID],

Check warning on line 364 in src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useCallback has a missing dependency: 'isInSidePanel'. Either include it or remove the dependency array
);

const onTriggerAttachmentPicker = useCallback(() => {
Expand Down
5 changes: 3 additions & 2 deletions src/pages/home/report/ReportFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ function ReportFooter({
// If we are adding an action on an expense report that only has a single transaction thread child report, we need to add the action to the transaction thread instead.
// This is because we need it to be associated with the transaction thread and not the expense report in order for conversational corrections to work as expected.
const targetReportID = transactionThreadReportID ?? report.reportID;
addComment(targetReportID, report.reportID, targetReportAncestors, text, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE, true);
addComment(targetReportID, report.reportID, targetReportAncestors, text, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE, true, isInSidePanel);
},
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[report.reportID, handleCreateTask, transactionThreadReportID, targetReportAncestors],
[report.reportID, handleCreateTask, transactionThreadReportID, targetReportAncestors, isInSidePanel],
);

const [didHideComposerInput, setDidHideComposerInput] = useState(!shouldShowComposeInput);
Expand Down Expand Up @@ -257,6 +257,7 @@ function ReportFooter({
didHideComposerInput={didHideComposerInput}
reportTransactions={reportTransactions}
transactionThreadReportID={transactionThreadReportID}
isInSidePanel={isInSidePanel}
/>
</SwipeableView>
</View>
Expand Down
Loading