From feea6cdb39da28fe7367c3232b9c16da66b6ff58 Mon Sep 17 00:00:00 2001
From: Alexey Kureev <a.g.kureev@gmail.com>
Date: Thu, 4 Dec 2025 18:01:52 +0100
Subject: [PATCH 001/233] proposal: new editing mechanism for small screens

---
 .../BaseReportActionContextMenu.tsx           |  6 +-
 .../report/ContextMenu/ContextMenuActions.tsx | 27 ++++++-
 .../home/report/PureReportActionItem.tsx      | 13 ++--
 .../ComposerWithSuggestions.tsx               | 74 ++++++++++++++++---
 .../ReportActionCompose.tsx                   | 66 ++++++++++++++++-
 src/pages/home/report/ReportActionsList.tsx   | 27 ++++++-
 6 files changed, 191 insertions(+), 22 deletions(-)

diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 9729112c2bd2..db545a58f4f5 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -210,8 +210,10 @@ function BaseReportActionContextMenu({
     const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, {canBeMissing: true});
     const [moneyRequestPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport?.policyID}`, {canBeMissing: true});
     const {transactions} = useTransactionsAndViolationsForReport(childReport?.reportID);
-    const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: false});
+    const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true});
     const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;
+    const [allDraftMessages] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
+    const reportDrafts = originalReportID ? allDraftMessages?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`] : undefined;
 
     const isMoneyRequest = useMemo(() => ReportUtilsIsMoneyRequest(childReport), [childReport]);
     const isTrackExpenseReport = ReportUtilsIsTrackExpenseReport(childReport);
@@ -360,6 +362,8 @@ function BaseReportActionContextMenu({
                             reportID,
                             report,
                             draftMessage,
+                            allDraftMessages: reportDrafts,
+                            shouldUseNarrowLayout,
                             selection,
                             close: () => setShouldKeepOpen(false),
                             transitionActionSheetState: actionSheetAwareScrollViewContext.transitionActionSheetState,
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 750b5e4f8bcd..bc670aa274b0 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -145,7 +145,19 @@ import {
 import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 import ROUTES from '@src/ROUTES';
-import type {Beta, Card, Download as DownloadOnyx, OnyxInputOrEntry, Policy, PolicyTagLists, ReportAction, ReportActionReactions, Report as ReportType, Transaction} from '@src/types/onyx';
+import type {
+    Beta,
+    Card,
+    Download as DownloadOnyx,
+    OnyxInputOrEntry,
+    Policy,
+    PolicyTagLists,
+    ReportAction,
+    ReportActionReactions,
+    ReportActionsDrafts,
+    Report as ReportType,
+    Transaction,
+} from '@src/types/onyx';
 import type IconAsset from '@src/types/utils/IconAsset';
 import KeyboardUtils from '@src/utils/keyboard';
 import type {ContextMenuAnchor} from './ReportActionContextMenu';
@@ -202,6 +214,8 @@ type ContextMenuActionPayload = {
     reportID: string | undefined;
     report: OnyxEntry<ReportType>;
     draftMessage: string;
+    allDraftMessages?: ReportActionsDrafts;
+    shouldUseNarrowLayout: boolean;
     selection: string;
     close: () => void;
     transitionActionSheetState: (params: {type: string; payload?: Record<string, unknown>}) => void;
@@ -366,7 +380,7 @@ const ContextMenuActions: ContextMenuAction[] = [
         icon: Expensicons.Pencil,
         shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}) =>
             type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport,
-        onPress: (closePopover, {reportID, reportAction, draftMessage, moneyRequestAction}) => {
+        onPress: (closePopover, {reportID, reportAction, allDraftMessages, shouldUseNarrowLayout, draftMessage, moneyRequestAction}) => {
             if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) {
                 const editExpense = () => {
                     const childReportID = reportAction?.childReportID;
@@ -382,6 +396,15 @@ const ContextMenuActions: ContextMenuAction[] = [
             }
             const editAction = () => {
                 if (!draftMessage) {
+                    if (shouldUseNarrowLayout && allDraftMessages) {
+                        for (const actionID of Object.keys(allDraftMessages)) {
+                            if (actionID === reportAction.reportActionID) {
+                                continue;
+                            }
+                            deleteReportActionDraft(reportID, reportAction);
+                        }
+                    }
+
                     saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction)));
                 } else {
                     deleteReportActionDraft(reportID, reportAction);
diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx
index b96c247d4afe..234b0d123c9d 100644
--- a/src/pages/home/report/PureReportActionItem.tsx
+++ b/src/pages/home/report/PureReportActionItem.tsx
@@ -54,6 +54,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
 import type {OnyxDataWithErrors} from '@libs/ErrorUtils';
 import {getLatestErrorMessageField, isReceiptError} from '@libs/ErrorUtils';
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
+import getIsNarrowLayout from '@libs/getIsNarrowLayout';
 import {isReportMessageAttachment} from '@libs/isReportMessageAttachment';
 import Navigation from '@libs/Navigation/Navigation';
 import Permissions from '@libs/Permissions';
@@ -496,10 +497,12 @@ function PureReportActionItem({
     const prevDraftMessage = usePrevious(draftMessage);
     const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID;
     const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked);
+    const isNarrowLayout = getIsNarrowLayout();
     const isActionableWhisper =
         isActionableMentionWhisper(action) || isActionableMentionInviteToSubmitExpenseConfirmWhisper(action) || isActionableTrackExpense(action) || isActionableReportMentionWhisper(action);
     const isReportArchived = useReportIsArchived(reportID);
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
+    const isInlineEditing = !isNarrowLayout && draftMessage !== undefined;
 
     const highlightedBackgroundColorIfNeeded = useMemo(
         // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -1484,7 +1487,7 @@ function PureReportActionItem({
                 <MentionReportContext.Provider value={mentionReportContextValue}>
                     <ShowContextMenuContext.Provider value={contextValue}>
                         <AttachmentContext.Provider value={attachmentContextValue}>
-                            {draftMessage === undefined ? (
+                            {!isInlineEditing ? (
                                 <View style={displayAsGroup && hasBeenFlagged ? styles.blockquote : {}}>
                                     <ReportActionItemMessage
                                         reportID={reportID}
@@ -1622,7 +1625,7 @@ function PureReportActionItem({
             return emptyHTML;
         }
 
-        if (draftMessage !== undefined) {
+        if (!isNarrowLayout && draftMessage !== undefined) {
             return <ReportActionItemDraft>{content}</ReportActionItemDraft>;
         }
 
@@ -1630,7 +1633,7 @@ function PureReportActionItem({
             return (
                 <ReportActionItemSingle
                     action={action}
-                    showHeader={draftMessage === undefined}
+                    showHeader={draftMessage === undefined || isNarrowLayout}
                     wrapperStyle={{
                         ...(isOnSearch && styles.p0),
                         ...(isWhisper && styles.pt1),
@@ -1754,7 +1757,7 @@ function PureReportActionItem({
         >
             <Hoverable
                 shouldHandleScroll
-                isDisabled={draftMessage !== undefined}
+                isDisabled={draftMessage !== undefined && !isNarrowLayout}
                 shouldFreezeCapture={isPaymentMethodPopoverActive}
                 onHoverIn={() => {
                     setIsReportActionActive(false);
@@ -1775,7 +1778,7 @@ function PureReportActionItem({
                                 isArchivedRoom={isArchivedRoom}
                                 displayAsGroup={displayAsGroup}
                                 disabledActions={disabledActions}
-                                isVisible={hovered && draftMessage === undefined && !hasErrors}
+                                isVisible={hovered && (!draftMessage || isNarrowLayout) && !hasErrors}
                                 isThreadReportParentAction={isThreadReportParentAction}
                                 draftMessage={draftMessage}
                                 isChronosReport={isChronosReport}
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 86f27500cc9f..8a305d84e341 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -239,15 +239,57 @@ function ComposerWithSuggestions({
     const cursorPositionValue = useSharedValue({x: 0, y: 0});
     const tag = useSharedValue(-1);
     const [draftComment = ''] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true});
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+    const [allActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
+    const activeInlineEdit = useMemo(() => {
+        if (!shouldUseNarrowLayout) {
+            return null;
+        }
+
+        const reportDrafts = allActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
+
+        if (!reportDrafts) {
+            return null;
+        }
+
+        const entry = Object.entries(reportDrafts).find(([, d]) => d?.message);
+
+        if (!entry) {
+            return null;
+        }
+
+        const [reportActionID, draft] = entry;
+
+        return {
+            reportActionID,
+            message: draft?.message ?? '',
+        };
+    }, [allActionDrafts, reportID, shouldUseNarrowLayout]);
+
+    const initialValue = shouldUseNarrowLayout ? (activeInlineEdit?.message ?? draftComment) : draftComment;
+
     const [value, setValue] = useState(() => {
-        if (draftComment) {
-            emojisPresentBefore.current = extractEmojis(draftComment);
+        if (initialValue) {
+            emojisPresentBefore.current = extractEmojis(initialValue);
         }
-        return draftComment;
+        return initialValue;
     });
 
     const commentRef = useRef(value);
 
+    useEffect(() => {
+        if (!shouldUseNarrowLayout) {
+            return;
+        }
+
+        const nextValue = activeInlineEdit?.message ?? draftComment ?? '';
+
+        emojisPresentBefore.current = extractEmojis(nextValue);
+        setValue(nextValue);
+        commentRef.current = nextValue;
+    }, [activeInlineEdit, draftComment, shouldUseNarrowLayout]);
+
     const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: getPreferredSkinToneIndex, canBeMissing: true});
     const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED, {canBeMissing: true});
@@ -257,7 +299,6 @@ function ComposerWithSuggestions({
         lastTextRef.current = value;
     }, [value]);
 
-    const {shouldUseNarrowLayout} = useResponsiveLayout();
     const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;
     const shouldAutoFocus = (shouldFocusInputOnScreenFocus || !!draftComment) && !modal?.isVisible && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput;
 
@@ -300,10 +341,13 @@ function ComposerWithSuggestions({
     const debouncedSaveReportComment = useMemo(
         () =>
             lodashDebounce((selectedReportID: string, newComment: string | null) => {
+                if (shouldUseNarrowLayout) {
+                    return;
+                }
                 saveReportDraftComment(selectedReportID, newComment);
                 isCommentPendingSaved.current = false;
             }, 1000),
-        [],
+        [shouldUseNarrowLayout],
     );
 
     useEffect(() => {
@@ -427,15 +471,25 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
+            if (shouldUseNarrowLayout) {
+                const editingReportActionID = activeInlineEdit?.reportActionID;
+
+                if (editingReportActionID) {
+                    saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
+                }
+
+                if (newCommentConverted) {
+                    debouncedBroadcastUserIsTyping(reportID);
+                }
+                return;
+            }
+
             if (shouldDebounceSaveComment) {
                 isCommentPendingSaved.current = true;
                 debouncedSaveReportComment(reportID, newCommentConverted);
             } else {
                 saveReportDraftComment(reportID, newCommentConverted);
             }
-            if (newCommentConverted) {
-                debouncedBroadcastUserIsTyping(reportID);
-            }
         },
         [
             findNewlyAddedChars,
@@ -446,6 +500,8 @@ function ComposerWithSuggestions({
             suggestionsRef,
             raiseIsScrollLikelyLayoutTriggered,
             debouncedSaveReportComment,
+            activeInlineEdit,
+            shouldUseNarrowLayout,
             selection?.end,
             selection?.start,
         ],
@@ -883,7 +939,7 @@ function ComposerWithSuggestions({
                 resetKeyboardInput={resetKeyboardInput}
             />
 
-            {isValidReportIDFromPath(reportID) && (
+            {isValidReportIDFromPath(reportID) && !shouldUseNarrowLayout && (
                 <SilentCommentUpdater
                     reportID={reportID}
                     value={value}
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 1ba94b09ebf2..6c96362551f4 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -61,7 +61,7 @@ import AgentZeroProcessingRequestIndicator from '@pages/home/report/AgentZeroPro
 import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime';
 import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator';
 import {hideEmojiPicker, isActive as isActiveEmojiPickerAction} from '@userActions/EmojiPickerAction';
-import {addAttachmentWithComment, setIsComposerFullSize} from '@userActions/Report';
+import {addAttachmentWithComment, deleteReportActionDraft, editReportComment, setIsComposerFullSize} from '@userActions/Report';
 import Timing from '@userActions/Timing';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';
 import CONST from '@src/CONST';
@@ -151,6 +151,28 @@ function ReportActionCompose({
     const [initialModalState] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});
     const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, {canBeMissing: true});
     const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true});
+    const [allActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
+
+    const activeInlineDraft = useMemo(() => {
+        const reportDrafts = allActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
+
+        if (!reportDrafts) {
+            return null;
+        }
+
+        const entry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
+
+        if (!entry) {
+            return null;
+        }
+
+        const [reportActionID, draft] = entry;
+
+        return {
+            reportActionID,
+            message: draft.message,
+        };
+    }, [allActionDrafts, reportID]);
 
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
 
@@ -169,8 +191,10 @@ function ReportActionCompose({
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
+    const effectiveDraft = shouldUseNarrowLayout ? activeInlineDraft?.message : draftComment;
+
     const [isCommentEmpty, setIsCommentEmpty] = useState(() => {
-        return !draftComment || !!draftComment.match(CONST.REGEX.EMPTY_COMMENT);
+        return !effectiveDraft || !!effectiveDraft.match(CONST.REGEX.EMPTY_COMMENT);
     });
 
     /**
@@ -217,6 +241,25 @@ function ReportActionCompose({
         canBeMissing: true,
     });
 
+    const [reportActionDrafts] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, {canBeMissing: true});
+
+    const editingReportActionID = useMemo(() => {
+        if (!reportActionDrafts) {
+            return null;
+        }
+
+        const entry = Object.entries(reportActionDrafts).find(([, draft]) => draft?.message);
+        return entry?.[0] ?? null;
+    }, [reportActionDrafts]);
+
+    const editingReportAction = useMemo(() => {
+        if (!editingReportActionID || !reportActions) {
+            return null;
+        }
+
+        return reportActions[editingReportActionID] ?? null;
+    }, [editingReportActionID, reportActions]);
+
     const personalDetail = useCurrentUserPersonalDetails();
 
     const iouAction = reportActions ? Object.values(reportActions).find((action) => isMoneyRequestAction(action)) : null;
@@ -321,6 +364,18 @@ function ReportActionCompose({
         (newComment: string) => {
             const newCommentTrimmed = newComment.trim();
 
+            if (!newCommentTrimmed && !attachmentFileRef.current) {
+                return;
+            }
+
+            const isEditingInline = shouldUseNarrowLayout && !!editingReportAction;
+
+            if (isEditingInline && !attachmentFileRef.current) {
+                editReportComment(report, editingReportAction, ancestors, newCommentTrimmed, undefined, false);
+                deleteReportActionDraft(reportID, editingReportAction);
+                return;
+            }
+
             if (attachmentFileRef.current) {
                 addAttachmentWithComment(transactionThreadReportID ?? reportID, reportID, ancestors, attachmentFileRef.current, newCommentTrimmed, personalDetail.timezone, true);
                 attachmentFileRef.current = null;
@@ -338,7 +393,7 @@ function ReportActionCompose({
                 onSubmit(newCommentTrimmed);
             }
         },
-        [onSubmit, ancestors, reportID, personalDetail.timezone, transactionThreadReportID],
+        [reportID, ancestors, transactionThreadReportID, personalDetail.timezone, onSubmit, editingReportAction, shouldUseNarrowLayout],
     );
 
     const onTriggerAttachmentPicker = useCallback(() => {
@@ -366,6 +421,11 @@ function ReportActionCompose({
         onComposerFocus?.();
     }, [onComposerFocus]);
 
+    useEffect(() => {
+        const valueToCheck = shouldUseNarrowLayout ? activeInlineDraft?.message : draftComment;
+        setIsCommentEmpty(!valueToCheck || !!valueToCheck.match(CONST.REGEX.EMPTY_COMMENT));
+    }, [activeInlineDraft, draftComment, shouldUseNarrowLayout]);
+
     useEffect(() => {
         if (hasExceededMaxTaskTitleLength) {
             setExceededMaxLength(CONST.TITLE_CHARACTER_LIMIT);
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index 8bcad4613b84..9c48158e29aa 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -189,7 +189,7 @@ function ReportActionsList({
     const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}`, {canBeMissing: true});
     const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}`, {canBeMissing: true});
     const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true});
-    const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: false});
+    const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true});
     const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;
     const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(false);
     const [actionIdToHighlight, setActionIdToHighlight] = useState('');
@@ -687,12 +687,33 @@ function ReportActionsList({
         return isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report);
     }, [parentReportAction, report, sortedVisibleReportActions]);
 
+    const activeMobileEditActionID = useMemo(() => {
+        if (!shouldUseNarrowLayout || !draftMessage) {
+            return null;
+        }
+
+        for (const reportDrafts of Object.values(draftMessage)) {
+            if (!reportDrafts) {
+                continue;
+            }
+
+            for (const [actionID, draft] of Object.entries(reportDrafts)) {
+                if (draft?.message) {
+                    return actionID;
+                }
+            }
+        }
+
+        return null;
+    }, [shouldUseNarrowLayout, draftMessage]);
+
     const renderItem = useCallback(
         ({item: reportAction, index}: ListRenderItemInfo<OnyxTypes.ReportAction>) => {
             const originalReportID = getOriginalReportID(report.reportID, reportAction);
             const reportDraftMessages = draftMessage?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`];
             const matchingDraftMessage = reportDraftMessages?.[reportAction.reportActionID];
-            const matchingDraftMessageString = matchingDraftMessage?.message;
+            const matchingDraftMessageString =
+                shouldUseNarrowLayout && activeMobileEditActionID && activeMobileEditActionID !== reportAction.reportActionID ? undefined : matchingDraftMessage?.message;
 
             const actionEmojiReactions = emojiReactions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportAction.reportActionID}`];
             const transactionID = isMoneyRequestAction(reportAction) && getOriginalMessage(reportAction)?.IOUTransactionID;
@@ -760,6 +781,8 @@ function ReportActionsList({
             userBillingFundID,
             isTryNewDotNVPDismissed,
             isReportArchived,
+            activeMobileEditActionID,
+            shouldUseNarrowLayout,
         ],
     );
 

From 111bbdff8d004dbf29f08d25413427dfbbf7e51c Mon Sep 17 00:00:00 2001
From: Alexey Kureev <a.g.kureev@gmail.com>
Date: Thu, 4 Dec 2025 18:09:09 +0100
Subject: [PATCH 002/233] fix eslint issues

---
 .../ComposerWithSuggestions/ComposerWithSuggestions.tsx      | 5 +++--
 .../home/report/ReportActionCompose/ReportActionCompose.tsx  | 2 +-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 8a305d84e341..c4689a4f50db 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -288,7 +288,7 @@ function ComposerWithSuggestions({
         emojisPresentBefore.current = extractEmojis(nextValue);
         setValue(nextValue);
         commentRef.current = nextValue;
-    }, [activeInlineEdit, draftComment, shouldUseNarrowLayout]);
+    }, [activeInlineEdit?.message, activeInlineEdit?.reportActionID, draftComment, shouldUseNarrowLayout]);
 
     const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: getPreferredSkinToneIndex, canBeMissing: true});
@@ -500,7 +500,8 @@ function ComposerWithSuggestions({
             suggestionsRef,
             raiseIsScrollLikelyLayoutTriggered,
             debouncedSaveReportComment,
-            activeInlineEdit,
+            activeInlineEdit?.message,
+            activeInlineEdit?.reportActionID,
             shouldUseNarrowLayout,
             selection?.end,
             selection?.start,
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 6c96362551f4..ae8aebe4cfac 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -424,7 +424,7 @@ function ReportActionCompose({
     useEffect(() => {
         const valueToCheck = shouldUseNarrowLayout ? activeInlineDraft?.message : draftComment;
         setIsCommentEmpty(!valueToCheck || !!valueToCheck.match(CONST.REGEX.EMPTY_COMMENT));
-    }, [activeInlineDraft, draftComment, shouldUseNarrowLayout]);
+    }, [activeInlineDraft?.message, draftComment, shouldUseNarrowLayout]);
 
     useEffect(() => {
         if (hasExceededMaxTaskTitleLength) {

From 2e190d8878676a12e235622a1030d28381256a40 Mon Sep 17 00:00:00 2001
From: Alexey Kureev <a.g.kureev@gmail.com>
Date: Thu, 4 Dec 2025 18:16:52 +0100
Subject: [PATCH 003/233] fix eslint warnings

---
 src/pages/home/report/PureReportActionItem.tsx                 | 3 ++-
 .../ComposerWithSuggestions/ComposerWithSuggestions.tsx        | 1 -
 .../home/report/ReportActionCompose/ReportActionCompose.tsx    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx
index 234b0d123c9d..a389fb5a657b 100644
--- a/src/pages/home/report/PureReportActionItem.tsx
+++ b/src/pages/home/report/PureReportActionItem.tsx
@@ -14,7 +14,6 @@ import DisplayNames from '@components/DisplayNames';
 import Hoverable from '@components/Hoverable';
 import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
 import Icon from '@components/Icon';
-import {Eye} from '@components/Icon/Expensicons';
 import InlineSystemMessage from '@components/InlineSystemMessage';
 import KYCWall from '@components/KYCWall';
 import {KYCWallContext} from '@components/KYCWall/KYCWallContext';
@@ -40,6 +39,7 @@ import Text from '@components/Text';
 import TextLink from '@components/TextLink';
 import UnreadActionIndicator from '@components/UnreadActionIndicator';
 import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import usePreferredPolicy from '@hooks/usePreferredPolicy';
 import usePrevious from '@hooks/usePrevious';
@@ -503,6 +503,7 @@ function PureReportActionItem({
     const isReportArchived = useReportIsArchived(reportID);
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
     const isInlineEditing = !isNarrowLayout && draftMessage !== undefined;
+    const {Eye} = useMemoizedLazyExpensifyIcons(['Eye']);
 
     const highlightedBackgroundColorIfNeeded = useMemo(
         // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index c4689a4f50db..8f659f5e9ac9 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -500,7 +500,6 @@ function ComposerWithSuggestions({
             suggestionsRef,
             raiseIsScrollLikelyLayoutTriggered,
             debouncedSaveReportComment,
-            activeInlineEdit?.message,
             activeInlineEdit?.reportActionID,
             shouldUseNarrowLayout,
             selection?.end,
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index ae8aebe4cfac..836ee4568d42 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -393,7 +393,7 @@ function ReportActionCompose({
                 onSubmit(newCommentTrimmed);
             }
         },
-        [reportID, ancestors, transactionThreadReportID, personalDetail.timezone, onSubmit, editingReportAction, shouldUseNarrowLayout],
+        [reportID, ancestors, transactionThreadReportID, personalDetail.timezone, onSubmit, report, editingReportAction, shouldUseNarrowLayout],
     );
 
     const onTriggerAttachmentPicker = useCallback(() => {

From 3f5ea983dba680fc0f7ffdbb099b023050977f4d Mon Sep 17 00:00:00 2001
From: Alexey Kureev <a.g.kureev@gmail.com>
Date: Thu, 4 Dec 2025 18:24:45 +0100
Subject: [PATCH 004/233] stub deprecation error

---
 src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index bc670aa274b0..a4a4ee3b0d89 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -650,6 +650,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                     const taskPreviewMessage = getTaskCreatedMessage(reportAction, childReport, true);
                     Clipboard.setString(taskPreviewMessage);
                 } else if (isMemberChangeAction(reportAction)) {
+                    // @eslint-disable-next-line
                     const logMessage = getMemberChangeMessageFragment(reportAction, getReportName).html ?? '';
                     setClipboardMessage(logMessage);
                 } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) {

From 69abc8dbecee14fc4887dc9946a4faf972e4f6d8 Mon Sep 17 00:00:00 2001
From: Alexey Kureev <a.g.kureev@gmail.com>
Date: Thu, 4 Dec 2025 18:45:50 +0100
Subject: [PATCH 005/233] fix typo

---
 src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index a4a4ee3b0d89..83c970ec6d11 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -650,7 +650,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                     const taskPreviewMessage = getTaskCreatedMessage(reportAction, childReport, true);
                     Clipboard.setString(taskPreviewMessage);
                 } else if (isMemberChangeAction(reportAction)) {
-                    // @eslint-disable-next-line
+                    // eslint-disable-next-line
                     const logMessage = getMemberChangeMessageFragment(reportAction, getReportName).html ?? '';
                     setClipboardMessage(logMessage);
                 } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) {

From 576520b230a7d7b0165605a27eef191b9519ddfc Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Feb 2026 21:32:47 +0000
Subject: [PATCH 006/233] fix: do not invert conditions

---
 .../inbox/report/PureReportActionItem.tsx     | 32 +++++++++----------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 53a754d7c931..80f71c63b2f6 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -578,7 +578,7 @@ function PureReportActionItem({
         isActionableMentionWhisper(action) || isActionableMentionInviteToSubmitExpenseConfirmWhisper(action) || isActionableTrackExpense(action) || isActionableReportMentionWhisper(action);
     const isReportArchived = useReportIsArchived(reportID);
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
-    const isInlineEditing = !isNarrowLayout && draftMessage !== undefined;
+    const isEditingInline = !isNarrowLayout && draftMessage !== undefined;
     const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID);
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye'] as const);
 
@@ -1783,7 +1783,21 @@ function PureReportActionItem({
                 <MentionReportContext.Provider value={mentionReportContextValue}>
                     <ShowContextMenuContext.Provider value={contextValue}>
                         <AttachmentContext.Provider value={attachmentContextValue}>
-                            {!isInlineEditing ? (
+                            {isEditingInline ? (
+                                <ReportActionItemMessageEdit
+                                    action={action}
+                                    draftMessage={draftMessage}
+                                    reportID={reportID}
+                                    originalReportID={originalReportID}
+                                    policyID={report?.policyID}
+                                    index={index}
+                                    ref={composerTextInputRef}
+                                    shouldDisableEmojiPicker={
+                                        (chatIncludesConcierge(report) && isBlockedFromConcierge(blockedFromConcierge)) || isArchivedNonExpenseReport(report, isArchivedRoom)
+                                    }
+                                    isGroupPolicyReport={!!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE}
+                                />
+                            ) : (
                                 <View style={displayAsGroup && hasBeenFlagged ? styles.blockquote : {}}>
                                     <ReportActionItemMessage
                                         reportID={reportID}
@@ -1836,20 +1850,6 @@ function PureReportActionItem({
                                         />
                                     )}
                                 </View>
-                            ) : (
-                                <ReportActionItemMessageEdit
-                                    action={action}
-                                    draftMessage={draftMessage}
-                                    reportID={reportID}
-                                    originalReportID={originalReportID}
-                                    policyID={report?.policyID}
-                                    index={index}
-                                    ref={composerTextInputRef}
-                                    shouldDisableEmojiPicker={
-                                        (chatIncludesConcierge(report) && isBlockedFromConcierge(blockedFromConcierge)) || isArchivedNonExpenseReport(report, isArchivedRoom)
-                                    }
-                                    isGroupPolicyReport={!!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE}
-                                />
                             )}
                         </AttachmentContext.Provider>
                     </ShowContextMenuContext.Provider>

From 3fea5218344e27a3f3863437cfba8a2bf3e01404 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 08:53:13 +0000
Subject: [PATCH 007/233] refactor: extraact message edit button

---
 .../MessageEditCancelButton.tsx               | 48 ++++++++++++++
 .../ReportActionCompose.tsx                   | 55 +++++++++-------
 .../ReportActionCompose/useDeleteDraft.ts     | 64 +++++++++++++++++++
 .../report/ReportActionItemMessageEdit.tsx    | 56 ++--------------
 4 files changed, 148 insertions(+), 75 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
 create mode 100644 src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts

diff --git a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
new file mode 100644
index 000000000000..4dc175d61dbd
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
@@ -0,0 +1,48 @@
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import {PressableWithFeedback} from '@components/Pressable';
+import Tooltip from '@components/Tooltip';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type MessageEditCancelButtonProps = {
+    onCancel: () => void;
+};
+
+function MessageEditCancelButton({onCancel}: MessageEditCancelButtonProps) {
+    const styles = useThemeStyles();
+    const theme = useTheme();
+    const {translate} = useLocalize();
+    const icons = useMemoizedLazyExpensifyIcons(['Close']);
+
+    const closeButtonStyles = [styles.composerSizeButton, {marginVertical: styles.composerSizeButton.marginHorizontal}];
+
+    return (
+        <View style={[styles.justifyContentEnd, styles.mb1]}>
+            <Tooltip text={translate('common.cancel')}>
+                <PressableWithFeedback
+                    onPress={onCancel}
+                    style={closeButtonStyles}
+                    role={CONST.ROLE.BUTTON}
+                    accessibilityLabel={translate('common.close')}
+                    // disable dimming
+                    hoverDimmingValue={1}
+                    pressDimmingValue={1}
+                    // Keep focus on the composer when cancel button is clicked.
+                    onMouseDown={(e) => e.preventDefault()}
+                    sentryLabel={CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_CANCEL_BUTTON}
+                >
+                    <Icon
+                        fill={theme.icon}
+                        src={icons.Close}
+                    />
+                </PressableWithFeedback>
+            </Tooltip>
+        </View>
+    );
+}
+
+export default MessageEditCancelButton;
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index d8e1ea2ca764..7e0f87a8dd2e 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -77,8 +77,10 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
 import type {ComposerRef, ComposerWithSuggestionsProps} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import MessageEditCancelButton from './MessageEditCancelButton';
 import SendButton from './SendButton';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
+import useDeleteDraft from './useDeleteDraft';
 
 type SuggestionsRef = {
     resetSuggestions: () => void;
@@ -545,6 +547,7 @@ function ReportActionCompose({
         });
     }, [isSendDisabled, debouncedValidate, isComposerFullSize, reportID, composerRefShared]);
 
+    const deleteDraft = useDeleteDraft({reportID, reportAction: editingReportAction, index: 0, isFocused});
     onSubmitAction = handleSendMessage;
 
     const emojiPositionValues = useMemo(
@@ -628,30 +631,34 @@ function ReportActionCompose({
                         ]}
                     >
                         {PDFValidationComponent}
-                        <AttachmentPickerWithMenuItems
-                            onAttachmentPicked={(files) => validateAttachments({files})}
-                            reportID={reportID}
-                            report={report}
-                            currentUserPersonalDetails={currentUserPersonalDetails}
-                            reportParticipantIDs={reportParticipantIDs}
-                            isFullComposerAvailable={isFullComposerAvailable}
-                            isComposerFullSize={isComposerFullSize}
-                            disabled={isBlockedFromConcierge}
-                            setMenuVisibility={setMenuVisibility}
-                            isMenuVisible={isMenuVisible}
-                            onTriggerAttachmentPicker={onTriggerAttachmentPicker}
-                            raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLayoutTriggered}
-                            onAddActionPressed={onAddActionPressed}
-                            onItemSelected={onItemSelected}
-                            onCanceledAttachmentPicker={() => {
-                                if (!shouldFocusComposerOnScreenFocus) {
-                                    return;
-                                }
-                                focus();
-                            }}
-                            actionButtonRef={actionButtonRef}
-                            shouldDisableAttachmentItem={!!exceededMaxLength}
-                        />
+                        {isEditingInline ? (
+                            <MessageEditCancelButton onCancel={deleteDraft} />
+                        ) : (
+                            <AttachmentPickerWithMenuItems
+                                onAttachmentPicked={(files) => validateAttachments({files})}
+                                reportID={reportID}
+                                report={report}
+                                currentUserPersonalDetails={currentUserPersonalDetails}
+                                reportParticipantIDs={reportParticipantIDs}
+                                isFullComposerAvailable={isFullComposerAvailable}
+                                isComposerFullSize={isComposerFullSize}
+                                disabled={isBlockedFromConcierge}
+                                setMenuVisibility={setMenuVisibility}
+                                isMenuVisible={isMenuVisible}
+                                onTriggerAttachmentPicker={onTriggerAttachmentPicker}
+                                raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLayoutTriggered}
+                                onAddActionPressed={onAddActionPressed}
+                                onItemSelected={onItemSelected}
+                                onCanceledAttachmentPicker={() => {
+                                    if (!shouldFocusComposerOnScreenFocus) {
+                                        return;
+                                    }
+                                    focus();
+                                }}
+                                actionButtonRef={actionButtonRef}
+                                shouldDisableAttachmentItem={!!exceededMaxLength}
+                            />
+                        )}
                         <ComposerWithSuggestions
                             ref={(ref) => {
                                 composerRef.current = ref;
diff --git a/src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts b/src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts
new file mode 100644
index 000000000000..c14457aee8fa
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts
@@ -0,0 +1,64 @@
+import {useEffect, useRef} from 'react';
+import {InteractionManager} from 'react-native';
+import useReportScrollManager from '@hooks/useReportScrollManager';
+import {isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
+import {deleteReportActionDraft} from '@libs/actions/Report';
+import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
+import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
+import type * as OnyxTypes from '@src/types/onyx';
+import KeyboardUtils from '@src/utils/keyboard';
+
+type UseDeleteDraftProps = {
+    reportID: string | undefined;
+    reportAction: OnyxTypes.ReportAction | null | undefined;
+    index: number;
+    isFocused: boolean;
+};
+
+/**
+ * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
+ */
+function useDeleteDraft({reportID, reportAction, index, isFocused}: UseDeleteDraftProps) {
+    const reportScrollManager = useReportScrollManager();
+    const isFocusedRef = useRef(isFocused);
+
+    useEffect(() => {
+        // required for keeping last state of isFocused variable
+        isFocusedRef.current = isFocused;
+    }, [isFocused]);
+
+    // We consider the report action active if it's focused, its emoji picker is open or its context menu is open
+    function isActive(): boolean {
+        if (!reportAction) {
+            return false;
+        }
+
+        return isFocusedRef.current || isEmojiPickerActive(reportAction.reportActionID) || ReportActionContextMenu.isActiveReportAction(reportAction.reportActionID);
+    }
+
+    function deleteDraft(): void {
+        if (!reportAction) {
+            return;
+        }
+
+        deleteReportActionDraft(reportID, reportAction);
+
+        if (isActive()) {
+            ReportActionComposeFocusManager.clear(true);
+            // Wait for report action compose re-mounting on mWeb
+            // eslint-disable-next-line @typescript-eslint/no-deprecated
+            InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus());
+        }
+
+        // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
+        if (index === 0) {
+            KeyboardUtils.dismiss().then(() => {
+                reportScrollManager.scrollToIndex(index, false);
+            });
+        }
+    }
+
+    return deleteDraft;
+}
+
+export default useDeleteDraft;
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index d1a8807a564d..450155fdb5e8 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -33,7 +33,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import {setShouldShowComposeInput} from '@libs/actions/Composer';
 import {clearActive, isActive as isEmojiPickerActive, isEmojiPickerVisible} from '@libs/actions/EmojiPickerAction';
 import {composerFocusKeepFocusOn} from '@libs/actions/InputFocus';
-import {deleteReportActionDraft, editReportComment, saveReportActionDraft} from '@libs/actions/Report';
+import {editReportComment, saveReportActionDraft} from '@libs/actions/Report';
 import {isMobileChrome} from '@libs/Browser';
 import {canSkipTriggerHotkeys, insertText} from '@libs/ComposerUtils';
 import DomUtils from '@libs/DomUtils';
@@ -53,12 +53,13 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
-import KeyboardUtils from '@src/utils/keyboard';
 import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
 import getCursorPosition from './ReportActionCompose/getCursorPosition';
 import getScrollPosition from './ReportActionCompose/getScrollPosition';
+import MessageEditCancelButton from './ReportActionCompose/MessageEditCancelButton';
 import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose';
 import Suggestions from './ReportActionCompose/Suggestions';
+import useDeleteDraft from './ReportActionCompose/useDeleteDraft';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
 
 type ReportActionItemMessageEditProps = {
@@ -188,12 +189,6 @@ function ReportActionItemMessageEdit({
         [],
     );
 
-    // We consider the report action active if it's focused, its emoji picker is open or its context menu is open
-    const isActive = useCallback(
-        () => isFocusedRef.current || isEmojiPickerActive(action.reportActionID) || ReportActionContextMenu.isActiveReportAction(action.reportActionID),
-        [action.reportActionID],
-    );
-
     /**
      * Focus the composer text input
      * @param shouldDelay - Impose delay before focusing the composer
@@ -276,26 +271,7 @@ function ReportActionItemMessageEdit({
         // eslint-disable-next-line react-hooks/exhaustive-deps -- run this only when language is changed
     }, [action.reportActionID, preferredLocale]);
 
-    /**
-     * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
-     */
-    const deleteDraft = useCallback(() => {
-        deleteReportActionDraft(reportID, action);
-
-        if (isActive()) {
-            ReportActionComposeFocusManager.clear(true);
-            // Wait for report action compose re-mounting on mWeb
-            // eslint-disable-next-line @typescript-eslint/no-deprecated
-            InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus());
-        }
-
-        // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
-        if (index === 0) {
-            KeyboardUtils.dismiss().then(() => {
-                reportScrollManager.scrollToIndex(index, false);
-            });
-        }
-    }, [action, index, reportID, reportScrollManager, isActive]);
+    const deleteDraft = useDeleteDraft({reportID, reportAction: action, index, isFocused});
 
     /**
      * Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with
@@ -477,8 +453,6 @@ function ReportActionItemMessageEdit({
         }
     }, [isFocused, hideSuggestionMenu]);
 
-    const closeButtonStyles = [styles.composerSizeButton, {marginVertical: styles.composerSizeButton.marginHorizontal}];
-
     return (
         <>
             <View
@@ -494,27 +468,7 @@ function ReportActionItemMessageEdit({
                         hasExceededMaxCommentLength && styles.borderColorDanger,
                     ]}
                 >
-                    <View style={[styles.justifyContentEnd, styles.mb1]}>
-                        <Tooltip text={translate('common.cancel')}>
-                            <PressableWithFeedback
-                                onPress={deleteDraft}
-                                style={closeButtonStyles}
-                                role={CONST.ROLE.BUTTON}
-                                accessibilityLabel={translate('common.close')}
-                                // disable dimming
-                                hoverDimmingValue={1}
-                                pressDimmingValue={1}
-                                // Keep focus on the composer when cancel button is clicked.
-                                onMouseDown={(e) => e.preventDefault()}
-                                sentryLabel={CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_CANCEL_BUTTON}
-                            >
-                                <Icon
-                                    fill={theme.icon}
-                                    src={icons.Close}
-                                />
-                            </PressableWithFeedback>
-                        </Tooltip>
-                    </View>
+                    <MessageEditCancelButton onCancel={deleteDraft} />
                     <View style={[StyleUtils.getContainerComposeStyles(), styles.textInputComposeBorder]}>
                         <Composer
                             multiline

From 81cda27aa74e76e64d37fa29f410fe0d041ae476 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 11:29:28 +0000
Subject: [PATCH 008/233] refactor: add proper types for `Composer` and
 `ComposerWithSuggestions` refs

---
 .../Composer/implementation/index.native.tsx  |  16 +--
 .../Composer/implementation/index.tsx         |  90 +++++++------
 src/components/Composer/types.ts              |   8 +-
 .../ComposerWithSuggestions.tsx               | 122 ++++++++++--------
 .../ComposerWithSuggestions/index.e2e.tsx     |   5 +-
 5 files changed, 132 insertions(+), 109 deletions(-)

diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index 6251fd7d97d7..722e13cab437 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -3,7 +3,7 @@ import mimeDb from 'mime-db';
 import React, {useCallback, useEffect, useMemo, useRef} from 'react';
 import type {NativeSyntheticEvent, TextInputChangeEvent, TextInputPasteEventData} from 'react-native';
 import {StyleSheet} from 'react-native';
-import type {ComposerProps} from '@components/Composer/types';
+import type {ComposerProps, ComposerRef} from '@components/Composer/types';
 import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
 import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
 import useMarkdownStyle from '@hooks/useMarkdownStyle';
@@ -36,7 +36,7 @@ function Composer({
     ref,
     ...props
 }: ComposerProps) {
-    const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
+    const textInputRef = useRef<AnimatedMarkdownTextInputRef | null>(null);
     const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? ''))), [value]);
     const theme = useTheme();
     const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
@@ -44,7 +44,7 @@ function Composer({
     const StyleUtils = useStyleUtils();
 
     useEffect(() => {
-        if (!textInput.current || !textInput.current.setSelection || !selection || isComposerFullSize) {
+        if (!textInputRef.current || !textInputRef.current.setSelection || !selection || isComposerFullSize) {
             return;
         }
 
@@ -53,8 +53,8 @@ function Composer({
         // (see https://github.com/Expensify/App/pull/50520#discussion_r1861960311 for more context)
         const timeoutID = setTimeout(() => {
             // We are setting selection twice to trigger a scroll to the cursor on toggling to smaller composer size.
-            textInput.current?.setSelection((selection.start || 1) - 1, selection.start);
-            textInput.current?.setSelection(selection.start, selection.start);
+            textInputRef.current?.setSelection((selection.start || 1) - 1, selection.start);
+            textInputRef.current?.setSelection(selection.start, selection.start);
         }, 0);
 
         return () => clearTimeout(timeoutID);
@@ -67,8 +67,8 @@ function Composer({
      * @param {Element} el
      */
     const setTextInputRef = useCallback((el: AnimatedMarkdownTextInputRef | null) => {
-        textInput.current = el;
-        if (typeof ref !== 'function' || textInput.current === null) {
+        textInputRef.current = el;
+        if (typeof ref !== 'function' || textInputRef.current === null) {
             return;
         }
 
@@ -76,7 +76,7 @@ function Composer({
         // get a ref to the inner textInput element e.g. if we do
         // <constructor ref={el => this.textInput = el} /> this will not
         // return a ref to the component, but rather the HTML element by default
-        ref(textInput.current);
+        ref(textInputRef.current as ComposerRef);
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, []);
 
diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 7d181775ed4e..301b74f2cacb 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, use
 // eslint-disable-next-line no-restricted-imports
 import type {TextInputKeyPressEvent, TextInputSelectionChangeEvent} from 'react-native';
 import {DeviceEventEmitter, StyleSheet} from 'react-native';
-import type {ComposerProps} from '@components/Composer/types';
+import type {ComposerProps, ComposerRef} from '@components/Composer/types';
 import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
 import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
 import useHtmlPaste from '@hooks/useHtmlPaste';
@@ -57,7 +57,7 @@ function Composer({
     const styles = useThemeStyles();
     const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
     const StyleUtils = useStyleUtils();
-    const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
+    const textInputRef = useRef<AnimatedMarkdownTextInputRef | null>(null);
     const [selection, setSelection] = useState<
         | {
               start: number;
@@ -72,7 +72,7 @@ function Composer({
     });
     const [isRendered, setIsRendered] = useState(false);
 
-    const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
+    const isScrollBarVisible = useIsScrollBarVisible(textInputRef, value ?? '');
     const [prevScroll, setPrevScroll] = useState<number | undefined>();
     const [prevHeight, setPrevHeight] = useState<number | undefined>();
     const isReportFlatListScrolling = useRef(false);
@@ -94,13 +94,13 @@ function Composer({
             const range = sel.getRangeAt(0).cloneRange();
             range.collapse(true);
             const rect = range.getClientRects()[0] || range.startContainer.parentElement?.getClientRects()[0];
-            const containerRect = textInput.current?.getBoundingClientRect();
+            const containerRect = textInputRef.current?.getBoundingClientRect();
 
             let x = 0;
             let y = 0;
             if (rect && containerRect) {
                 x = rect.left - containerRect.left;
-                y = rect.top - containerRect.top + (textInput?.current?.scrollTop ?? 0) - rect.height / 2;
+                y = rect.top - containerRect.top + (textInputRef?.current?.scrollTop ?? 0) - rect.height / 2;
             }
 
             const selectionValue = {
@@ -131,14 +131,14 @@ function Composer({
     const handlePaste = useCallback(
         (event: ClipboardEvent) => {
             const isVisible = checkComposerVisibility();
-            const isFocused = textInput.current?.isFocused();
+            const isFocused = textInputRef.current?.isFocused();
             const isContenteditableDivFocused = document.activeElement?.nodeName === 'DIV' && document.activeElement?.hasAttribute('contenteditable');
 
             if (!(isVisible || isFocused)) {
                 return true;
             }
 
-            if (textInput.current !== event.target && !(isContenteditableDivFocused && !event.clipboardData?.files.length)) {
+            if (textInputRef.current !== event.target && !(isContenteditableDivFocused && !event.clipboardData?.files.length)) {
                 const eventTarget = event.target as HTMLInputElement | HTMLTextAreaElement | null;
                 // To make sure the composer does not capture paste events from other inputs, we check where the event originated
                 // If it did originate in another input, we return early to prevent the composer from handling the paste
@@ -147,7 +147,7 @@ function Composer({
                     return true;
                 }
 
-                textInput.current?.focus();
+                textInputRef.current?.focus();
             }
 
             event.preventDefault();
@@ -202,19 +202,19 @@ function Composer({
     );
 
     useEffect(() => {
-        if (!textInput.current) {
+        if (!textInputRef.current) {
             return;
         }
         const debouncedSetPrevScroll = lodashDebounce(() => {
-            if (!textInput.current) {
+            if (!textInputRef.current) {
                 return;
             }
-            setPrevScroll(textInput.current.scrollTop);
+            setPrevScroll(textInputRef.current.scrollTop);
         }, 100);
 
-        textInput.current.addEventListener('scroll', debouncedSetPrevScroll);
+        textInputRef.current.addEventListener('scroll', debouncedSetPrevScroll);
         return () => {
-            textInput.current?.removeEventListener('scroll', debouncedSetPrevScroll);
+            textInputRef.current?.removeEventListener('scroll', debouncedSetPrevScroll);
         };
     }, []);
 
@@ -235,40 +235,40 @@ function Composer({
 
             // When the composer has no scrollable content, the stopPropagation will prevent the inverted wheel event handler on the Chat body
             // which defaults to the browser wheel behavior. This causes the chat body to scroll in the opposite direction creating jerky behavior.
-            if (textInput.current && textInput.current.scrollHeight <= textInput.current.clientHeight) {
+            if (textInputRef.current && textInputRef.current.scrollHeight <= textInputRef.current.clientHeight) {
                 return;
             }
             e.stopPropagation();
         };
-        textInput.current?.addEventListener('wheel', handleWheel, {passive: false});
+        textInputRef.current?.addEventListener('wheel', handleWheel, {passive: false});
 
         return () => {
-            textInput.current?.removeEventListener('wheel', handleWheel);
+            textInputRef.current?.removeEventListener('wheel', handleWheel);
         };
     }, []);
 
     useEffect(() => {
-        if (!textInput.current || prevScroll === undefined || prevHeight === undefined) {
+        if (!textInputRef.current || prevScroll === undefined || prevHeight === undefined) {
             return;
         }
-        textInput.current.scrollTop = prevScroll + prevHeight - textInput.current.clientHeight;
+        textInputRef.current.scrollTop = prevScroll + prevHeight - textInputRef.current.clientHeight;
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [isComposerFullSize]);
 
     const isActive = useIsFocused();
-    useHtmlPaste(textInput, handlePaste, isActive);
+    useHtmlPaste(textInputRef, handlePaste, isActive);
 
     useEffect(() => {
         setIsRendered(true);
     }, []);
 
     const clear = useCallback(() => {
-        if (!textInput.current) {
+        if (!textInputRef.current) {
             return;
         }
 
-        const currentText = textInput.current.value;
-        textInput.current.clear();
+        const currentText = textInputRef.current.value;
+        textInputRef.current.clear();
 
         // We need to reset the selection to 0,0 manually after clearing the text input on web
         const selectionEvent = {
@@ -285,24 +285,30 @@ function Composer({
         onClear(currentText);
     }, [onClear, onSelectionChange]);
 
-    useImperativeHandle(ref, () => {
-        const textInputRef = textInput.current;
-        if (!textInputRef) {
-            throw new Error('textInputRef is not available. This should never happen and indicates a developer error.');
-        }
-
-        return {
-            ...textInputRef,
-            // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works
-            clear,
-            // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly
-            blur: () => textInputRef.blur(),
-            focus: () => textInputRef.focus(),
-            get scrollTop() {
-                return textInputRef.scrollTop;
-            },
-        };
-    }, [clear]);
+    useImperativeHandle(
+        ref,
+        () =>
+            new Proxy(
+                {},
+                {
+                    get: (_target, prop) => {
+                        if (prop === 'clear') {
+                            return clear;
+                        }
+                        if (prop === 'blur') {
+                            return () => textInputRef.current?.blur();
+                        }
+                        if (prop === 'focus') {
+                            return () => textInputRef.current?.focus();
+                        }
+                        if (prop === 'scrollTop') {
+                            return textInputRef.current?.scrollTop;
+                        }
+                        return textInputRef.current?.[prop as keyof AnimatedMarkdownTextInputRef];
+                    },
+                },
+            ) as ComposerRef,
+    );
 
     const handleKeyPress = useCallback(
         (e: TextInputKeyPressEvent) => {
@@ -343,9 +349,7 @@ function Composer({
             autoComplete="off"
             autoCorrect={!isMobileSafari()}
             placeholderTextColor={theme.placeholderText}
-            ref={(el) => {
-                textInput.current = el;
-            }}
+            ref={textInputRef}
             selection={selection}
             style={[inputStyleMemo]}
             markdownStyle={markdownStyle}
diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts
index 3824aac67445..7fc8708eb9aa 100644
--- a/src/components/Composer/types.ts
+++ b/src/components/Composer/types.ts
@@ -1,5 +1,5 @@
 import type {Ref} from 'react';
-import type {StyleProp, TextInput, TextInputProps, TextInputSelectionChangeEvent, TextStyle} from 'react-native';
+import type {StyleProp, TextInputProps, TextInputSelectionChangeEvent, TextStyle} from 'react-native';
 import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
 import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
 import type {FileObject} from '@src/types/utils/Attachment';
@@ -15,6 +15,8 @@ type CustomSelectionChangeEvent = TextInputSelectionChangeEvent & {
     positionY?: number;
 };
 
+type ComposerRef = AnimatedMarkdownTextInputRef & HTMLInputElement & HTMLTextAreaElement;
+
 type ComposerProps = Omit<TextInputProps, 'onClear'> &
     ForwardedFSClassProps & {
         /** Indicate whether input is multiline */
@@ -74,7 +76,7 @@ type ComposerProps = Omit<TextInputProps, 'onClear'> &
         isGroupPolicyReport?: boolean;
 
         /** Ref exposing imperative methods on the underlying text input */
-        ref?: Ref<TextInput | HTMLInputElement | HTMLTextAreaElement | AnimatedMarkdownTextInputRef>;
+        ref?: Ref<ComposerRef>;
     };
 
-export type {TextSelection, ComposerProps, CustomSelectionChangeEvent};
+export type {TextSelection, ComposerProps, CustomSelectionChangeEvent, ComposerRef};
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index faaf1a691cec..34824fd4acf9 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -2,7 +2,7 @@ import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
 import lodashDebounce from 'lodash/debounce';
 import type {Ref, RefObject} from 'react';
 import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
-import type {BlurEvent, LayoutChangeEvent, MeasureInWindowOnSuccessCallback, TextInput, TextInputContentSizeChangeEvent, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native';
+import type {BlurEvent, LayoutChangeEvent, MeasureInWindowOnSuccessCallback, TextInputContentSizeChangeEvent, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native';
 import {DeviceEventEmitter, InteractionManager, NativeModules, StyleSheet, View} from 'react-native';
 import {useFocusedInputHandler} from 'react-native-keyboard-controller';
 import type {OnyxEntry} from 'react-native-onyx';
@@ -10,7 +10,7 @@ import {useAnimatedRef, useSharedValue} from 'react-native-reanimated';
 import type {Emoji} from '@assets/emojis/types';
 import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types';
 import Composer from '@components/Composer';
-import type {CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types';
+import type {ComposerRef, CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types';
 import {useWideRHPState} from '@components/WideRHPContextProvider';
 import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
 import useKeyboardState from '@hooks/useKeyboardState';
@@ -64,6 +64,26 @@ type SyncSelection = {
 
 type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string};
 
+type ComposerWithSuggestionsRef = ComposerRef & {
+    /** Focus the composer */
+    focus: (shouldDelay?: boolean) => void;
+
+    /** Replace the selection with text */
+    replaceSelectionWithText: OnEmojiSelected;
+
+    /** Get the current text of the composer */
+    getCurrentText: () => string;
+
+    /**
+     * Calling clear will immediately clear the input on the UI thread (its a worklet).
+     * Once the composer ahs cleared onCleared will be called with the value that was cleared.
+     */
+    clearWorklet: () => void;
+
+    /** Reset the height of the composer */
+    resetHeight: () => void;
+};
+
 type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
     ForwardedFSClassProps & {
         /** Report ID */
@@ -142,7 +162,7 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
         didHideComposerInput?: boolean;
 
         /** Reference to the outer element */
-        ref?: Ref<ComposerRef | null>;
+        ref?: Ref<ComposerWithSuggestionsRef | null>;
     };
 
 type SwitchToCurrentReportProps = {
@@ -150,26 +170,6 @@ type SwitchToCurrentReportProps = {
     reportToCopyDraftTo: string;
     callback: () => void;
 };
-
-type ComposerRef = {
-    blur: () => void;
-    focus: (shouldDelay?: boolean) => void;
-    replaceSelectionWithText: OnEmojiSelected;
-    getCurrentText: () => string;
-    isFocused: () => boolean;
-
-    /**
-     * Calling clear will immediately clear the input on the UI thread (its a worklet).
-     * Once the composer ahs cleared onCleared will be called with the value that was cleared.
-     */
-    clearWorklet: () => void;
-
-    /**
-     * Reset the height of the composer.
-     */
-    resetHeight: () => void;
-};
-
 const {RNTextInputReset} = NativeModules;
 
 const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
@@ -334,7 +334,7 @@ function ComposerWithSuggestions({
     const [composerHeightAfterClear, setDefaultComposerHeight] = useState<number | null>(null);
     const emptyComposerHeightRef = useRef<number | null>(null);
 
-    const textInputRef = useRef<TextInput | null>(null);
+    const composerRef = useRef<ComposerRef | null>(null);
 
     const syncSelectionWithOnChangeTextRef = useRef<SyncSelection | null>(null);
 
@@ -353,10 +353,10 @@ function ComposerWithSuggestions({
     /**
      * Set the TextInput Ref
      */
-    const setTextInputRef = useCallback(
-        (el: TextInput) => {
+    const setComposerRef = useCallback(
+        (el: ComposerRef) => {
             ReportActionComposeFocusManager.composerRef.current = el;
-            textInputRef.current = el;
+            composerRef.current = el;
             if (typeof animatedRef === 'function') {
                 animatedRef(el);
             }
@@ -650,7 +650,7 @@ function ComposerWithSuggestions({
                 InteractionManager.runAfterInteractions(() => {
                     // note: this implementation is only available on non-web RN, thus the wrapping
                     // 'if' block contains a redundant (since the ref is only used on iOS) platform check
-                    textInputRef.current?.setSelection(positionSnapshot, positionSnapshot);
+                    composerRef.current?.setSelection(positionSnapshot, positionSnapshot);
                 });
             }
         },
@@ -661,7 +661,7 @@ function ComposerWithSuggestions({
         (e: CustomSelectionChangeEvent) => {
             setSelection(e.nativeEvent.selection);
 
-            if (!textInputRef.current?.isFocused()) {
+            if (!composerRef.current?.isFocused()) {
                 return;
             }
             suggestionsRef.current?.onSelectionChange?.(e);
@@ -695,7 +695,7 @@ function ComposerWithSuggestions({
     const focus = useCallback((shouldDelay = false) => {
         // If we're stacked above another RHP, wait for the transition to complete before focusing.
         const delay = shouldDelayAutoFocusRef.current ? CONST.ANIMATED_TRANSITION : CONST.COMPOSER_FOCUS_DELAY;
-        focusComposerWithDelay(textInputRef.current, delay)(shouldDelay);
+        focusComposerWithDelay(composerRef.current, delay)(shouldDelay);
     }, []);
 
     /**
@@ -782,10 +782,10 @@ function ComposerWithSuggestions({
     );
 
     const blur = useCallback(() => {
-        if (!textInputRef.current) {
+        if (!composerRef.current) {
             return;
         }
-        textInputRef.current.blur();
+        composerRef.current.blur();
     }, []);
 
     const clearWorklet = useCallback(() => {
@@ -810,7 +810,7 @@ function ComposerWithSuggestions({
         const unsubscribeNavigationFocus = navigation.addListener('focus', () => {
             addKeyDownPressListener(focusComposerOnKeyPress);
             // The report isn't unmounted and can be focused again after going back from another report so we should update the composerRef again
-            ReportActionComposeFocusManager.composerRef.current = textInputRef.current;
+            ReportActionComposeFocusManager.composerRef.current = composerRef.current;
             setUpComposeFocusManager();
         });
         addKeyDownPressListener(focusComposerOnKeyPress);
@@ -838,7 +838,7 @@ function ComposerWithSuggestions({
 
         // We want to blur the input immediately when a screen is out of focus.
         if (!isFocused) {
-            textInputRef.current?.blur();
+            composerRef.current?.blur();
             return;
         }
 
@@ -863,22 +863,40 @@ function ComposerWithSuggestions({
 
     useEffect(() => {
         // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit
-        updateMultilineInputRange(textInputRef.current, !!shouldAutoFocus);
+        updateMultilineInputRange(composerRef.current, !!shouldAutoFocus);
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, []);
 
     useImperativeHandle(
         ref,
-        () => ({
-            blur,
-            focus,
-            replaceSelectionWithText,
-            isFocused: () => !!textInputRef.current?.isFocused(),
-            getCurrentText,
-            clearWorklet,
-            resetHeight,
-        }),
-        [blur, focus, replaceSelectionWithText, clearWorklet, resetHeight, getCurrentText],
+        () =>
+            new Proxy(
+                {},
+                {
+                    get: (_target, prop) => {
+                        if (prop === 'blur') {
+                            return blur;
+                        }
+                        if (prop === 'focus') {
+                            return focus;
+                        }
+                        if (prop === 'replaceSelectionWithText') {
+                            return replaceSelectionWithText;
+                        }
+                        if (prop === 'getCurrentText') {
+                            return getCurrentText;
+                        }
+                        if (prop === 'clearWorklet') {
+                            return clearWorklet;
+                        }
+                        if (prop === 'resetHeight') {
+                            return resetHeight;
+                        }
+
+                        return composerRef.current?.[prop as keyof ComposerRef];
+                    },
+                },
+            ) as ComposerWithSuggestionsRef,
     );
 
     useEffect(() => {
@@ -897,7 +915,7 @@ function ComposerWithSuggestions({
 
     useEffect(() => {
         // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data.
-        tag.set(findNodeHandle(textInputRef.current) ?? -1);
+        tag.set(findNodeHandle(composerRef.current) ?? -1);
     }, [tag]);
 
     useFocusedInputHandler(
@@ -917,7 +935,7 @@ function ComposerWithSuggestions({
     );
     const measureParentContainerAndReportCursor = useCallback(
         (callback: MeasureParentContainerAndCursorCallback) => {
-            const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef});
+            const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef: composerRef});
             const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.get(), positionOnWeb: selection});
             measureParentContainer((x, y, width, height) => {
                 callback({
@@ -974,7 +992,7 @@ function ComposerWithSuggestions({
         }
 
         queueMicrotask(() => {
-            textInputRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection);
+            composerRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection);
         });
     }, []);
 
@@ -992,7 +1010,7 @@ function ComposerWithSuggestions({
                     // So we must also prevent the TextInput's immediate `autoFocus` and rely on our delayed manual focus instead.
                     autoFocus={!!shouldAutoFocus && !shouldDelayAutoFocus}
                     multiline
-                    ref={setTextInputRef}
+                    ref={setComposerRef}
                     placeholder={inputPlaceholder}
                     placeholderTextColor={theme.placeholderText}
                     onChangeText={onChangeText}
@@ -1008,7 +1026,7 @@ function ComposerWithSuggestions({
                     onBlur={onBlur}
                     onClick={setShouldBlockSuggestionCalcToFalse}
                     onPasteFile={(files) => {
-                        textInputRef.current?.blur();
+                        composerRef.current?.blur();
                         onPasteFile(files);
                     }}
                     onClear={onClear}
@@ -1030,7 +1048,7 @@ function ComposerWithSuggestions({
 
             <Suggestions
                 ref={suggestionsRef}
-                isComposerFocused={textInputRef.current?.isFocused()}
+                isComposerFocused={composerRef.current?.isFocused()}
                 updateComment={updateComment}
                 measureParentContainerAndReportCursor={measureParentContainerAndReportCursor}
                 isGroupPolicyReport={isGroupPolicyReport}
@@ -1062,4 +1080,4 @@ function ComposerWithSuggestions({
 
 export default memo(ComposerWithSuggestions);
 
-export type {ComposerWithSuggestionsProps, ComposerRef};
+export type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef};
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx
index d9a5b4aced07..6c869c0a29dc 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx
@@ -2,8 +2,7 @@ import React, {useCallback, useRef} from 'react';
 import type {LayoutChangeEvent} from 'react-native';
 import {Keyboard} from 'react-native';
 import E2EClient from '@libs/E2E/client';
-import type {ComposerRef} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions';
+import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
 
 let rerenderCount = 0;
@@ -24,7 +23,7 @@ function ComposerWithSuggestionsE2e({ref, ...props}: ComposerWithSuggestionsProp
     // for this component. This file is only used for e2e tests, so it's okay to
     // disable compiler for this file.
 
-    const textInputRef = useRef<ComposerRef | null>(null);
+    const textInputRef = useRef<ComposerWithSuggestionsRef | null>(null);
     const hasFocusBeenRequested = useRef(false);
     const onLayout = useCallback((event: LayoutChangeEvent) => {
         const testConfig = E2EClient.getCurrentActiveTestConfig();

From 47582d8099024656dfca8c95fb613c4521034372 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 11:29:56 +0000
Subject: [PATCH 009/233] refactor: simplfiy logic around max comment length
 validation

---
 src/hooks/useHandleExceedMaxCommentLength.ts  | 18 --------
 .../useHandleExceedMaxTaskTitleLength.ts      | 16 -------
 .../useDebouncedCommentMaxLengthValidation.ts | 45 +++++++++++++++++++
 3 files changed, 45 insertions(+), 34 deletions(-)
 delete mode 100644 src/hooks/useHandleExceedMaxCommentLength.ts
 delete mode 100644 src/hooks/useHandleExceedMaxTaskTitleLength.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts

diff --git a/src/hooks/useHandleExceedMaxCommentLength.ts b/src/hooks/useHandleExceedMaxCommentLength.ts
deleted file mode 100644
index cec96b4ca8f6..000000000000
--- a/src/hooks/useHandleExceedMaxCommentLength.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {useCallback, useState} from 'react';
-import {getCommentLength} from '@libs/ReportUtils';
-import type {ParsingDetails} from '@libs/ReportUtils';
-import CONST from '@src/CONST';
-
-const useHandleExceedMaxCommentLength = () => {
-    const [hasExceededMaxCommentLength, setHasExceededMaxCommentLength] = useState(false);
-
-    const validateCommentMaxLength = useCallback((value: string, parsingDetails?: ParsingDetails) => {
-        const exceeded = getCommentLength(value, parsingDetails) > CONST.MAX_COMMENT_LENGTH;
-        setHasExceededMaxCommentLength(exceeded);
-        return !exceeded;
-    }, []);
-
-    return {hasExceededMaxCommentLength, validateCommentMaxLength, setHasExceededMaxCommentLength};
-};
-
-export default useHandleExceedMaxCommentLength;
diff --git a/src/hooks/useHandleExceedMaxTaskTitleLength.ts b/src/hooks/useHandleExceedMaxTaskTitleLength.ts
deleted file mode 100644
index 75acd73280a4..000000000000
--- a/src/hooks/useHandleExceedMaxTaskTitleLength.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {useCallback, useState} from 'react';
-import CONST from '@src/CONST';
-
-const useHandleExceedMaxTaskTitleLength = () => {
-    const [hasExceededMaxTaskTitleLength, setHasExceededMaxTitleLength] = useState(false);
-
-    const validateTaskTitleMaxLength = useCallback((title: string) => {
-        const exceeded = title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false;
-        setHasExceededMaxTitleLength(exceeded);
-        return !exceeded;
-    }, []);
-
-    return {hasExceededMaxTaskTitleLength, validateTaskTitleMaxLength, setHasExceededMaxTitleLength};
-};
-
-export default useHandleExceedMaxTaskTitleLength;
diff --git a/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts b/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
new file mode 100644
index 000000000000..e83d9a678f82
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
@@ -0,0 +1,45 @@
+import lodashDebounce from 'lodash/debounce';
+import {useState} from 'react';
+import {getCommentLength} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+
+type UseDebouncedCommentValidationProps = {
+    reportID: string | undefined;
+    isEditing?: boolean;
+};
+
+function useDebouncedCommentMaxLengthValidation({reportID, isEditing = false}: UseDebouncedCommentValidationProps) {
+    const [exceededMaxLength, setExceededMaxLength] = useState<number | null>(null);
+    const [isTaskTitle, setIsTaskTitle] = useState(false);
+
+    /**
+     * Updates the composer when the comment length is exceeded
+     * Shows red borders and prevents the comment from being sent
+     */
+    function validateMaxLength(value: string) {
+        const taskCommentMatch = value?.match(CONST.REGEX.TASK_TITLE_WITH_OPTIONAL_SHORT_MENTION);
+
+        if (!isEditing && taskCommentMatch) {
+            const title = taskCommentMatch?.[3] ? taskCommentMatch[3].trim().replaceAll('\n', ' ') : '';
+            const exceeded = title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false;
+
+            setIsTaskTitle(exceeded);
+            setExceededMaxLength(exceeded ? CONST.TITLE_CHARACTER_LIMIT : null);
+
+            return !exceeded;
+        }
+
+        const exceeded = getCommentLength(value, {reportID}) > CONST.MAX_COMMENT_LENGTH;
+
+        setIsTaskTitle(false);
+        setExceededMaxLength(exceeded ? CONST.MAX_COMMENT_LENGTH : null);
+
+        return !exceeded;
+    }
+
+    const debouncedCommentMaxLengthValidation = lodashDebounce(validateMaxLength, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME, {leading: true});
+
+    return {debouncedCommentMaxLengthValidation, exceededMaxLength, isTaskTitle, isExceedingMaxLength: !!exceededMaxLength};
+}
+
+export default useDebouncedCommentMaxLengthValidation;

From 44918d343eacdf39d882c5573e76219f1a598fa0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 11:30:04 +0000
Subject: [PATCH 010/233] fix: missing `import React`

---
 .../inbox/report/ReportActionCompose/MessageEditCancelButton.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
index 4dc175d61dbd..12bdb74b7c4c 100644
--- a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
 import {View} from 'react-native';
 import Icon from '@components/Icon';
 import {PressableWithFeedback} from '@components/Pressable';

From ee5ae8b6d5773068a937a60d2100e1309f4e72fd Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 11:30:32 +0000
Subject: [PATCH 011/233] feat: extract comment length validation and use new
 hook

---
 .../ReportActionCompose.tsx                   | 120 ++++++--------
 .../report/ReportActionCompose/SendButton.tsx |  33 +++-
 .../ReportActionCompose/useDeleteDraft.ts     |  64 --------
 .../ReportActionCompose/useEditMessage.ts     | 122 +++++++++++++++
 .../report/ReportActionItemMessageEdit.tsx    | 148 +++++-------------
 5 files changed, 231 insertions(+), 256 deletions(-)
 delete mode 100644 src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/useEditMessage.ts

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 7e0f87a8dd2e..d452dee8d98a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -1,4 +1,3 @@
-import lodashDebounce from 'lodash/debounce';
 import noop from 'lodash/noop';
 import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInputSelectionChangeEvent} from 'react-native';
@@ -20,8 +19,6 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider';
 import useAgentZeroStatusIndicator from '@hooks/useAgentZeroStatusIndicator';
 import useAncestors from '@hooks/useAncestors';
 import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength';
-import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitleLength';
 import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
@@ -36,7 +33,6 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
 import ComposerFocusManager from '@libs/ComposerFocusManager';
 import {canUseTouchScreen} from '@libs/DeviceCapabilities';
 import DomUtils from '@libs/DomUtils';
-import draftMessageVideoAttributeCache from '@libs/DraftMessageVideoAttributeCache';
 import FS from '@libs/Fullstory';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
 import Performance from '@libs/Performance';
@@ -66,7 +62,7 @@ import AgentZeroProcessingRequestIndicator from '@pages/inbox/report/AgentZeroPr
 import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime';
 import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator';
 import {hideEmojiPicker, isActive as isActiveEmojiPickerAction, isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
-import {addAttachmentWithComment, deleteReportActionDraft, editReportComment, setIsComposerFullSize} from '@userActions/Report';
+import {addAttachmentWithComment, setIsComposerFullSize} from '@userActions/Report';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -76,11 +72,12 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
-import type {ComposerRef, ComposerWithSuggestionsProps} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
 import MessageEditCancelButton from './MessageEditCancelButton';
 import SendButton from './SendButton';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
-import useDeleteDraft from './useDeleteDraft';
+import useDebouncedCommentMaxLengthValidation from './useDebouncedCommentMaxLengthValidation';
+import useEditMessage from './useEditMessage';
 
 type SuggestionsRef = {
     resetSuggestions: () => void;
@@ -150,7 +147,6 @@ function ReportActionCompose({
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
     const {isSmallScreenWidth, isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
     const {isOffline} = useNetwork();
-    const {email} = useCurrentUserPersonalDetails();
     const actionButtonRef = useRef<View | HTMLDivElement | null>(null);
     const currentUserPersonalDetails = useCurrentUserPersonalDetails();
     const personalDetails = usePersonalDetails();
@@ -214,18 +210,10 @@ function ReportActionCompose({
     const [isMenuVisible, setMenuVisibility] = useState(false);
     const [isAttachmentPreviewActive, setIsAttachmentPreviewActive] = useState(false);
 
-    /**
-     * Updates the composer when the comment length is exceeded
-     * Shows red borders and prevents the comment from being sent
-     */
-    const {hasExceededMaxCommentLength, validateCommentMaxLength, setHasExceededMaxCommentLength} = useHandleExceedMaxCommentLength();
-    const {hasExceededMaxTaskTitleLength, validateTaskTitleMaxLength, setHasExceededMaxTitleLength} = useHandleExceedMaxTaskTitleLength();
-    const [exceededMaxLength, setExceededMaxLength] = useState<number | null>(null);
-
     const icons = useMemoizedLazyExpensifyIcons(['MessageInABottle']);
 
     const suggestionsRef = useRef<SuggestionsRef>(null);
-    const composerRef = useRef<ComposerRef | null>(null);
+    const composerRef = useRef<ComposerWithSuggestionsRef | null>(null);
     const reportParticipantIDs = useMemo(
         () =>
             Object.keys(report?.participants ?? {})
@@ -259,14 +247,15 @@ function ReportActionCompose({
 
     const [reportActionDrafts] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, {canBeMissing: true});
 
-    const editingReportActionID = useMemo(() => {
-        if (!reportActionDrafts) {
-            return null;
+    const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
+    useEffect(() => {
+        if (!reportActionDrafts || editingReportActionID) {
+            return;
         }
 
         const entry = Object.entries(reportActionDrafts).find(([, draft]) => draft?.message);
-        return entry?.[0] ?? null;
-    }, [reportActionDrafts]);
+        setEditingReportActionID(entry?.[0] ?? null);
+    }, [editingReportActionID, reportActionDrafts]);
 
     const editingReportAction = useMemo(() => {
         if (!editingReportActionID || !reportActions) {
@@ -276,7 +265,7 @@ function ReportActionCompose({
         return reportActions[editingReportActionID] ?? null;
     }, [editingReportActionID, reportActions]);
 
-    const isEditingInline = shouldUseNarrowLayout && !!editingReportAction;
+    const isEditing = shouldUseNarrowLayout && !!editingReportAction;
 
     const personalDetail = useCurrentUserPersonalDetails();
 
@@ -379,6 +368,15 @@ function ReportActionCompose({
         ComposerFocusManager.setReadyToFocus();
     }, [updateShouldShowSuggestionMenuToFalse]);
 
+    const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({reportID});
+
+    const {publishDraft, deleteDraft} = useEditMessage({reportID, reportAction: editingReportAction, index: 0, isFocused, debouncedCommentMaxLengthValidation, composerRef});
+
+    const deleteDraftMessage = useCallback(() => {
+        deleteDraft();
+        setEditingReportActionID(null);
+    }, [deleteDraft]);
+
     /**
      * Add a new comment to this chat
      */
@@ -390,9 +388,10 @@ function ReportActionCompose({
                 return;
             }
 
-            if (isEditingInline && !attachmentFileRef.current) {
-                editReportComment(report, editingReportAction, ancestors, newCommentTrimmed, isReportArchived, false, email ?? '', Object.fromEntries(draftMessageVideoAttributeCache));
-                deleteReportActionDraft(reportID, editingReportAction);
+            if (isEditing && !attachmentFileRef.current) {
+                publishDraft(newCommentTrimmed);
+                deleteDraft();
+                setEditingReportActionID(null);
                 return;
             }
 
@@ -427,16 +426,15 @@ function ReportActionCompose({
             }
         },
         [
-            isEditingInline,
+            isEditing,
             isConciergeChat,
-            report,
-            editingReportAction,
-            ancestors,
-            isReportArchived,
-            email,
-            reportID,
+            publishDraft,
+            deleteDraft,
             kickoffWaitingIndicator,
             transactionThreadReport,
+            report,
+            reportID,
+            ancestors,
             currentUserPersonalDetails.accountID,
             personalDetail.timezone,
             isInSidePanel,
@@ -474,16 +472,6 @@ function ReportActionCompose({
         setIsCommentEmpty(!valueToCheck || !!valueToCheck.match(CONST.REGEX.EMPTY_COMMENT));
     }, [activeInlineDraft?.message, draftComment, shouldUseNarrowLayout]);
 
-    useEffect(() => {
-        if (hasExceededMaxTaskTitleLength) {
-            setExceededMaxLength(CONST.TITLE_CHARACTER_LIMIT);
-        } else if (hasExceededMaxCommentLength) {
-            setExceededMaxLength(CONST.MAX_COMMENT_LENGTH);
-        } else {
-            setExceededMaxLength(null);
-        }
-    }, [hasExceededMaxTaskTitleLength, hasExceededMaxCommentLength]);
-
     // We are returning a callback here as we want to invoke the method on unmount only
     useEffect(
         () => () => {
@@ -504,30 +492,15 @@ function ReportActionCompose({
 
     const hasReportRecipient = !isEmptyObject(reportRecipient);
 
-    const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!exceededMaxLength;
-
-    const validateMaxLength = useCallback(
-        (value: string) => {
-            const taskCommentMatch = value?.match(CONST.REGEX.TASK_TITLE_WITH_OPTIONAL_SHORT_MENTION);
-            if (taskCommentMatch) {
-                const title = taskCommentMatch?.[3] ? taskCommentMatch[3].trim().replaceAll('\n', ' ') : '';
-                setHasExceededMaxCommentLength(false);
-                return validateTaskTitleMaxLength(title);
-            }
-            setHasExceededMaxTitleLength(false);
-            return validateCommentMaxLength(value, {reportID});
-        },
-        [setHasExceededMaxCommentLength, setHasExceededMaxTitleLength, validateTaskTitleMaxLength, validateCommentMaxLength, reportID],
-    );
-
-    const debouncedValidate = useMemo(() => lodashDebounce(validateMaxLength, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME, {leading: true}), [validateMaxLength]);
+    const isNewCommentEmpty = isCommentEmpty && !isEditing;
+    const isSendDisabled = !isEditing && (isBlockedFromConcierge || isExceedingMaxLength || isNewCommentEmpty);
 
     // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value
     // useSharedValue on web doesn't support functions, so we need to wrap it in an object.
-    const composerRefShared = useSharedValue<Partial<ComposerRef>>({});
+    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
 
     const handleSendMessage = useCallback(() => {
-        if (isSendDisabled || !debouncedValidate.flush()) {
+        if (isSendDisabled || !debouncedCommentMaxLengthValidation.flush()) {
             return;
         }
 
@@ -545,9 +518,7 @@ function ReportActionCompose({
 
             clearWorklet?.();
         });
-    }, [isSendDisabled, debouncedValidate, isComposerFullSize, reportID, composerRefShared]);
-
-    const deleteDraft = useDeleteDraft({reportID, reportAction: editingReportAction, index: 0, isFocused});
+    }, [isSendDisabled, debouncedCommentMaxLengthValidation, isComposerFullSize, reportID, composerRefShared]);
     onSubmitAction = handleSendMessage;
 
     const emojiPositionValues = useMemo(
@@ -585,9 +556,9 @@ function ReportActionCompose({
             if (value.length === 0 && isComposerFullSize) {
                 setIsComposerFullSize(reportID, false);
             }
-            debouncedValidate(value);
+            debouncedCommentMaxLengthValidation(value);
         },
-        [isComposerFullSize, reportID, debouncedValidate],
+        [isComposerFullSize, reportID, debouncedCommentMaxLengthValidation],
     );
 
     const {validateAttachments, onReceiptDropped, PDFValidationComponent, ErrorModal} = useAttachmentUploadValidation({
@@ -627,12 +598,12 @@ function ReportActionCompose({
                             styles.flexRow,
                             styles.chatItemComposeBox,
                             isComposerFullSize && styles.chatItemFullComposeBox,
-                            !!exceededMaxLength && styles.borderColorDanger,
+                            isExceedingMaxLength && styles.borderColorDanger,
                         ]}
                     >
                         {PDFValidationComponent}
-                        {isEditingInline ? (
-                            <MessageEditCancelButton onCancel={deleteDraft} />
+                        {isEditing ? (
+                            <MessageEditCancelButton onCancel={deleteDraftMessage} />
                         ) : (
                             <AttachmentPickerWithMenuItems
                                 onAttachmentPicked={(files) => validateAttachments({files})}
@@ -656,7 +627,7 @@ function ReportActionCompose({
                                     focus();
                                 }}
                                 actionButtonRef={actionButtonRef}
-                                shouldDisableAttachmentItem={!!exceededMaxLength}
+                                shouldDisableAttachmentItem={isExceedingMaxLength}
                             />
                         )}
                         <ComposerWithSuggestions
@@ -730,8 +701,9 @@ function ReportActionCompose({
                             />
                         )}
                         <SendButton
+                            isEditing={isEditing}
                             isDisabled={isSendDisabled}
-                            handleSendMessage={handleSendMessage}
+                            onSend={handleSendMessage}
                         />
                     </View>
                     {ErrorModal}
@@ -752,7 +724,7 @@ function ReportActionCompose({
                         {!!exceededMaxLength && (
                             <ExceededCommentLength
                                 maxCommentLength={exceededMaxLength}
-                                isTaskTitle={hasExceededMaxTaskTitleLength}
+                                isTaskTitle={isTaskTitle}
                             />
                         )}
                     </View>
@@ -769,4 +741,4 @@ function ReportActionCompose({
 
 export default memo(ReportActionCompose);
 export {onSubmitAction};
-export type {SuggestionsRef, ComposerRef, ReportActionComposeProps};
+export type {SuggestionsRef, ReportActionComposeProps};
diff --git a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
index f283195e4e57..e8a3a48ce4a6 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
@@ -15,11 +15,14 @@ type SendButtonProps = {
     /** Whether the button is disabled */
     isDisabled: boolean;
 
+    /** Whether the button is in editing mode */
+    isEditing?: boolean;
+
     /** Handle clicking on send button */
-    handleSendMessage: () => void;
+    onSend: () => void;
 };
 
-function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonProps) {
+function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: SendButtonProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
     const {translate} = useLocalize();
@@ -28,10 +31,13 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP
     const {isSmallScreenWidth} = useResponsiveLayout();
     const Tap = Gesture.Tap()
         .onEnd(() => {
-            handleSendMessage();
+            onSend();
         })
         .runOnJS(true);
 
+    const label = isEditing ? translate('common.saveChanges') : translate('common.send');
+    const sentryLabel = isEditing ? CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON : CONST.SENTRY_LABEL.REPORT.SEND_BUTTON;
+
     return (
         <View
             style={styles.justifyContentEnd}
@@ -48,10 +54,10 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP
                     // In order to make buttons accessible, we have to wrap children in a View with accessible and accessibilityRole="button" props based on the docs: https://docs.swmansion.com/react-native-gesture-handler/docs/components/buttons/
                     accessible
                     role={CONST.ROLE.BUTTON}
-                    accessibilityLabel={translate('common.send')}
+                    accessibilityLabel={label}
                     collapsable={false}
                 >
-                    <Tooltip text={translate('common.send')}>
+                    <Tooltip text={label}>
                         <PressableWithFeedback
                             style={({pressed, isDisabled}) => [
                                 styles.chatItemSubmitButton,
@@ -62,7 +68,7 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP
                             // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.
                             accessible={false}
                             focusable={false}
-                            sentryLabel={CONST.SENTRY_LABEL.REPORT.SEND_BUTTON}
+                            sentryLabel={sentryLabel}
                         >
                             {({pressed}) => (
                                 <Icon
@@ -73,6 +79,21 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP
                         </PressableWithFeedback>
                     </Tooltip>
                 </View>
+
+                {/* <View style={styles.alignSelfEnd}>
+                            <PressableWithFeedback
+                                onPress={publishDraft}
+                                hoverDimmingValue={1}
+                                pressDimmingValue={0.2}
+                                // Keep focus on the composer when send button is clicked.
+                                onMouseDown={(e) => e.preventDefault()}
+                            >
+                                <Icon
+                                    src={icons.Checkmark}
+                                    fill={hasExceededMaxCommentLength ? theme.icon : theme.textLight}
+                                />
+                            </PressableWithFeedback>
+                    </View> */}
             </GestureDetector>
         </View>
     );
diff --git a/src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts b/src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts
deleted file mode 100644
index c14457aee8fa..000000000000
--- a/src/pages/inbox/report/ReportActionCompose/useDeleteDraft.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import {useEffect, useRef} from 'react';
-import {InteractionManager} from 'react-native';
-import useReportScrollManager from '@hooks/useReportScrollManager';
-import {isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
-import {deleteReportActionDraft} from '@libs/actions/Report';
-import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
-import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
-import type * as OnyxTypes from '@src/types/onyx';
-import KeyboardUtils from '@src/utils/keyboard';
-
-type UseDeleteDraftProps = {
-    reportID: string | undefined;
-    reportAction: OnyxTypes.ReportAction | null | undefined;
-    index: number;
-    isFocused: boolean;
-};
-
-/**
- * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
- */
-function useDeleteDraft({reportID, reportAction, index, isFocused}: UseDeleteDraftProps) {
-    const reportScrollManager = useReportScrollManager();
-    const isFocusedRef = useRef(isFocused);
-
-    useEffect(() => {
-        // required for keeping last state of isFocused variable
-        isFocusedRef.current = isFocused;
-    }, [isFocused]);
-
-    // We consider the report action active if it's focused, its emoji picker is open or its context menu is open
-    function isActive(): boolean {
-        if (!reportAction) {
-            return false;
-        }
-
-        return isFocusedRef.current || isEmojiPickerActive(reportAction.reportActionID) || ReportActionContextMenu.isActiveReportAction(reportAction.reportActionID);
-    }
-
-    function deleteDraft(): void {
-        if (!reportAction) {
-            return;
-        }
-
-        deleteReportActionDraft(reportID, reportAction);
-
-        if (isActive()) {
-            ReportActionComposeFocusManager.clear(true);
-            // Wait for report action compose re-mounting on mWeb
-            // eslint-disable-next-line @typescript-eslint/no-deprecated
-            InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus());
-        }
-
-        // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
-        if (index === 0) {
-            KeyboardUtils.dismiss().then(() => {
-                reportScrollManager.scrollToIndex(index, false);
-            });
-        }
-    }
-
-    return deleteDraft;
-}
-
-export default useDeleteDraft;
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
new file mode 100644
index 000000000000..124d7c125383
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -0,0 +1,122 @@
+// eslint-disable-next-line lodash/import-scope
+import type {DebouncedFuncLeading} from 'lodash';
+import type React from 'react';
+import {useEffect, useRef} from 'react';
+import {InteractionManager} from 'react-native';
+import type {ComposerRef} from '@components/Composer/types';
+import useAncestors from '@hooks/useAncestors';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useOnyx from '@hooks/useOnyx';
+import useReportIsArchived from '@hooks/useReportIsArchived';
+import useReportScrollManager from '@hooks/useReportScrollManager';
+import {isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
+import {deleteReportActionDraft, editReportComment} from '@libs/actions/Report';
+import draftMessageVideoAttributeCache from '@libs/DraftMessageVideoAttributeCache';
+import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
+import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
+import {getOriginalReportID} from '@libs/ReportUtils';
+import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxTypes from '@src/types/onyx';
+import KeyboardUtils from '@src/utils/keyboard';
+
+type UseEditMessageProps = {
+    reportID: string | undefined;
+    originalReportID?: string;
+    reportAction: OnyxTypes.ReportAction | null | undefined;
+    index: number;
+    isFocused: boolean;
+    debouncedCommentMaxLengthValidation: DebouncedFuncLeading<(value: string) => boolean>;
+    composerRef: React.RefObject<ComposerRef | null>;
+};
+
+/**
+ * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
+ */
+function useEditMessage({reportID, originalReportID, reportAction, index, isFocused, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) {
+    const reportScrollManager = useReportScrollManager();
+    const isFocusedRef = useRef(isFocused);
+
+    const {email} = useCurrentUserPersonalDetails();
+    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, {canBeMissing: true});
+    const originalParentReportID = getOriginalReportID(originalReportID, reportAction, reportActions);
+    const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {canBeMissing: true});
+    const isOriginalReportArchived = useReportIsArchived(originalReportID);
+    const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
+    const ancestors = useAncestors(originalReport);
+
+    useEffect(() => {
+        // required for keeping last state of isFocused variable
+        isFocusedRef.current = isFocused;
+    }, [isFocused]);
+
+    // We consider the report action active if it's focused, its emoji picker is open or its context menu is open
+    function isActive(): boolean {
+        if (!reportAction) {
+            return false;
+        }
+
+        return isFocusedRef.current || isEmojiPickerActive(reportAction.reportActionID) || ReportActionContextMenu.isActiveReportAction(reportAction.reportActionID);
+    }
+
+    function deleteDraft(): void {
+        if (!reportAction) {
+            return;
+        }
+
+        deleteReportActionDraft(reportID, reportAction);
+
+        if (isActive()) {
+            ReportActionComposeFocusManager.clear(true);
+            // Wait for report action compose re-mounting on mWeb
+            // eslint-disable-next-line @typescript-eslint/no-deprecated
+            InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus());
+        }
+
+        // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
+        if (index === 0) {
+            KeyboardUtils.dismiss().then(() => {
+                reportScrollManager.scrollToIndex(index, false);
+            });
+        }
+    }
+
+    /**
+     * Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with
+     * the new content.
+     */
+    function publishDraft(draftMessage: string) {
+        if (!reportAction) {
+            return;
+        }
+
+        // Do nothing if draft exceed the character limit
+        if (!debouncedCommentMaxLengthValidation.flush()) {
+            return;
+        }
+
+        const trimmedNewDraft = draftMessage.trim();
+
+        // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
+        if (!trimmedNewDraft) {
+            composerRef.current?.blur();
+            ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, reportAction, true, deleteDraft, () => focusEditAfterCancelDelete(composerRef.current));
+            return;
+        }
+        editReportComment(
+            originalReport,
+            reportAction,
+            ancestors,
+            trimmedNewDraft,
+            isOriginalReportArchived,
+            isOriginalParentReportArchived,
+            email ?? '',
+            Object.fromEntries(draftMessageVideoAttributeCache),
+        );
+        deleteDraft();
+    }
+
+    return {publishDraft, deleteDraft};
+}
+
+export default useEditMessage;
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 450155fdb5e8..26d2edef554a 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -1,39 +1,29 @@
 import lodashDebounce from 'lodash/debounce';
-import type {ForwardedRef} from 'react';
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {InteractionManager, View} from 'react-native';
-import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInput, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native';
+import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native';
 import {useFocusedInputHandler} from 'react-native-keyboard-controller';
 import {useSharedValue} from 'react-native-reanimated';
 import type {Emoji} from '@assets/emojis/types';
 import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types';
 import Composer from '@components/Composer';
-import type {TextSelection} from '@components/Composer/types';
+import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton';
 import ExceededCommentLength from '@components/ExceededCommentLength';
-import Icon from '@components/Icon';
-import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import Tooltip from '@components/Tooltip';
-import useAncestors from '@hooks/useAncestors';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength';
 import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered';
 import useKeyboardState from '@hooks/useKeyboardState';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import usePrevious from '@hooks/usePrevious';
-import useReportIsArchived from '@hooks/useReportIsArchived';
 import useReportScrollManager from '@hooks/useReportScrollManager';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useScrollBlocker from '@hooks/useScrollBlocker';
 import useStyleUtils from '@hooks/useStyleUtils';
-import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {setShouldShowComposeInput} from '@libs/actions/Composer';
 import {clearActive, isActive as isEmojiPickerActive, isEmojiPickerVisible} from '@libs/actions/EmojiPickerAction';
 import {composerFocusKeepFocusOn} from '@libs/actions/InputFocus';
-import {editReportComment, saveReportActionDraft} from '@libs/actions/Report';
+import {saveReportActionDraft} from '@libs/actions/Report';
 import {isMobileChrome} from '@libs/Browser';
 import {canSkipTriggerHotkeys, insertText} from '@libs/ComposerUtils';
 import DomUtils from '@libs/DomUtils';
@@ -41,12 +31,10 @@ import draftMessageVideoAttributeCache from '@libs/DraftMessageVideoAttributeCac
 import {extractEmojis, getZWNJCursorOffset, insertZWNJBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils';
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
 import type {Selection} from '@libs/focusComposerWithDelay/types';
-import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
 import Parser from '@libs/Parser';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import reportActionItemEventHandler from '@libs/ReportActionItemEventHandler';
 import {getReportActionHtml, isDeletedAction} from '@libs/ReportActionsUtils';
-import {getOriginalReportID} from '@libs/ReportUtils';
 import setShouldShowComposeInputKeyboardAware from '@libs/setShouldShowComposeInputKeyboardAware';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -58,8 +46,10 @@ import getCursorPosition from './ReportActionCompose/getCursorPosition';
 import getScrollPosition from './ReportActionCompose/getScrollPosition';
 import MessageEditCancelButton from './ReportActionCompose/MessageEditCancelButton';
 import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose';
+import SendButton from './ReportActionCompose/SendButton';
 import Suggestions from './ReportActionCompose/Suggestions';
-import useDeleteDraft from './ReportActionCompose/useDeleteDraft';
+import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation';
+import useEditMessage from './ReportActionCompose/useEditMessage';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
 
 type ReportActionItemMessageEditProps = {
@@ -88,7 +78,7 @@ type ReportActionItemMessageEditProps = {
     isGroupPolicyReport: boolean;
 
     /** Reference to the outer element */
-    ref?: ForwardedRef<TextInput | HTMLTextAreaElement | undefined>;
+    ref?: React.Ref<ComposerRef | undefined>;
 };
 
 const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection();
@@ -110,13 +100,11 @@ function ReportActionItemMessageEdit({
     ref,
 }: ReportActionItemMessageEditProps) {
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true});
-    const {email} = useCurrentUserPersonalDetails();
-    const theme = useTheme();
     const styles = useThemeStyles();
     const StyleUtils = useStyleUtils();
     const containerRef = useRef<View>(null);
     const reportScrollManager = useReportScrollManager();
-    const {translate, preferredLocale} = useLocalize();
+    const {preferredLocale} = useLocalize();
     const {isKeyboardShown} = useKeyboardState();
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const prevDraftMessage = usePrevious(draftMessage);
@@ -133,8 +121,8 @@ function ReportActionItemMessageEdit({
     });
     const [selection, setSelection] = useState<TextSelection>({start: draft.length, end: draft.length, positionX: 0, positionY: 0});
     const [isFocused, setIsFocused] = useState<boolean>(false);
-    const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength();
-    const debouncedValidateCommentMaxLength = useMemo(() => lodashDebounce(validateCommentMaxLength, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME), [validateCommentMaxLength]);
+
+    const {debouncedCommentMaxLengthValidation, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID});
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
@@ -143,19 +131,12 @@ function ReportActionItemMessageEdit({
 
     const {isScrolling, startScrollBlock, endScrollBlock} = useScrollBlocker();
 
-    const textInputRef = useRef<(HTMLTextAreaElement & TextInput) | null>(null);
+    const composerRef = useRef<ComposerRef | null>(null);
     const isFocusedRef = useRef<boolean>(false);
     const draftRef = useRef(draft);
     const emojiPickerSelectionRef = useRef<Selection | undefined>(undefined);
     // The ref to check whether the comment saving is in progress
     const isCommentPendingSaved = useRef(false);
-    const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {canBeMissing: true});
-    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, {canBeMissing: true});
-    const isOriginalReportArchived = useReportIsArchived(originalReportID);
-    const originalParentReportID = getOriginalReportID(originalReportID, action, reportActions);
-    const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
-    const ancestors = useAncestors(originalReport);
-    const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Close']);
 
     useEffect(() => {
         draftMessageVideoAttributeCache.clear();
@@ -170,17 +151,17 @@ function ReportActionItemMessageEdit({
     }, [draftMessage, action, prevDraftMessage]);
 
     useEffect(() => {
-        composerFocusKeepFocusOn(textInputRef.current as HTMLElement, isFocused, modal, onyxInputFocused);
+        composerFocusKeepFocusOn(composerRef.current as HTMLElement, isFocused, modal, onyxInputFocused);
     }, [isFocused, modal, onyxInputFocused]);
 
     useEffect(
         // Remove focus callback on unmount to avoid stale callbacks
         () => {
-            if (textInputRef.current) {
-                ReportActionComposeFocusManager.editComposerRef.current = textInputRef.current;
+            if (composerRef.current) {
+                ReportActionComposeFocusManager.editComposerRef.current = composerRef.current;
             }
             return () => {
-                if (ReportActionComposeFocusManager.editComposerRef.current !== textInputRef.current) {
+                if (ReportActionComposeFocusManager.editComposerRef.current !== composerRef.current) {
                     return;
                 }
                 ReportActionComposeFocusManager.clear(true);
@@ -194,7 +175,7 @@ function ReportActionItemMessageEdit({
      * @param shouldDelay - Impose delay before focusing the composer
      */
     const focus = useCallback((shouldDelay = false, forcedSelectionRange?: Selection) => {
-        focusComposerWithDelay(textInputRef.current)(shouldDelay, forcedSelectionRange);
+        focusComposerWithDelay(composerRef.current)(shouldDelay, forcedSelectionRange);
     }, []);
 
     // Take over focus priority
@@ -271,50 +252,9 @@ function ReportActionItemMessageEdit({
         // eslint-disable-next-line react-hooks/exhaustive-deps -- run this only when language is changed
     }, [action.reportActionID, preferredLocale]);
 
-    const deleteDraft = useDeleteDraft({reportID, reportAction: action, index, isFocused});
-
-    /**
-     * Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with
-     * the new content.
-     */
-    const publishDraft = useCallback(() => {
-        // Do nothing if draft exceed the character limit
-        if (!debouncedValidateCommentMaxLength.flush()) {
-            return;
-        }
+    const {publishDraft, deleteDraft} = useEditMessage({reportID, originalReportID, reportAction: action, index, isFocused, debouncedCommentMaxLengthValidation, composerRef});
 
-        const trimmedNewDraft = draft.trim();
-
-        // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
-        if (!trimmedNewDraft) {
-            textInputRef.current?.blur();
-            ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current));
-            return;
-        }
-        editReportComment(
-            originalReport,
-            action,
-            ancestors,
-            trimmedNewDraft,
-            isOriginalReportArchived,
-            isOriginalParentReportArchived,
-            email ?? '',
-            Object.fromEntries(draftMessageVideoAttributeCache),
-        );
-        deleteDraft();
-    }, [
-        reportID,
-        action,
-        ancestors,
-        deleteDraft,
-        draft,
-        originalReportID,
-        isOriginalReportArchived,
-        originalReport,
-        isOriginalParentReportArchived,
-        debouncedValidateCommentMaxLength,
-        email,
-    ]);
+    const publishDraftMessage = useCallback(() => publishDraft(draft), [publishDraft, draft]);
 
     /**
      * @param emoji
@@ -378,13 +318,13 @@ function ReportActionItemMessageEdit({
             }
             if (keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !keyEvent.shiftKey) {
                 e.preventDefault();
-                publishDraft();
+                publishDraftMessage();
             } else if (keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) {
                 e.preventDefault();
                 deleteDraft();
             }
         },
-        [deleteDraft, hideSuggestionMenu, isKeyboardShown, shouldUseNarrowLayout, publishDraft],
+        [deleteDraft, hideSuggestionMenu, isKeyboardShown, shouldUseNarrowLayout, publishDraftMessage],
     );
 
     const measureContainer = useCallback((callback: MeasureInWindowOnSuccessCallback) => {
@@ -397,7 +337,7 @@ function ReportActionItemMessageEdit({
     const measureParentContainerAndReportCursor = useCallback(
         (callback: MeasureParentContainerAndCursorCallback) => {
             const performMeasurement = () => {
-                const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef});
+                const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef: composerRef});
                 const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.get(), positionOnWeb: selection});
                 measureContainer((x, y, width, height) => {
                     callback({
@@ -422,7 +362,7 @@ function ReportActionItemMessageEdit({
 
     useEffect(() => {
         // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data.
-        tag.set(findNodeHandle(textInputRef.current) ?? -1);
+        tag.set(findNodeHandle(composerRef.current) ?? -1);
     }, [tag]);
     useFocusedInputHandler(
         {
@@ -441,8 +381,8 @@ function ReportActionItemMessageEdit({
     );
 
     useEffect(() => {
-        debouncedValidateCommentMaxLength(draft, {reportID});
-    }, [draft, reportID, debouncedValidateCommentMaxLength]);
+        debouncedCommentMaxLengthValidation(draft);
+    }, [draft, debouncedCommentMaxLengthValidation]);
 
     useEffect(() => {
         // required for keeping last state of isFocused variable
@@ -465,15 +405,15 @@ function ReportActionItemMessageEdit({
                         styles.flexRow,
                         styles.flex1,
                         styles.chatItemComposeBox,
-                        hasExceededMaxCommentLength && styles.borderColorDanger,
+                        isExceedingMaxLength && styles.borderColorDanger,
                     ]}
                 >
                     <MessageEditCancelButton onCancel={deleteDraft} />
                     <View style={[StyleUtils.getContainerComposeStyles(), styles.textInputComposeBorder]}>
                         <Composer
                             multiline
-                            ref={(el: TextInput & HTMLTextAreaElement) => {
-                                textInputRef.current = el;
+                            ref={(el) => {
+                                composerRef.current = el;
                                 if (typeof ref === 'function') {
                                     ref(el);
                                 } else if (ref) {
@@ -489,8 +429,8 @@ function ReportActionItemMessageEdit({
                             style={[styles.textInputCompose, styles.flex1, styles.bgTransparent, styles.textAlignLeft]}
                             onFocus={() => {
                                 setIsFocused(true);
-                                if (textInputRef.current) {
-                                    ReportActionComposeFocusManager.editComposerRef.current = textInputRef.current;
+                                if (composerRef.current) {
+                                    ReportActionComposeFocusManager.editComposerRef.current = composerRef.current;
                                 }
                                 startScrollBlock();
                                 // eslint-disable-next-line @typescript-eslint/no-deprecated
@@ -540,7 +480,7 @@ function ReportActionItemMessageEdit({
 
                     <Suggestions
                         ref={suggestionsRef}
-                        isComposerFocused={textInputRef.current?.isFocused()}
+                        isComposerFocused={composerRef.current?.isFocused()}
                         updateComment={updateDraft}
                         measureParentContainerAndReportCursor={measureParentContainerAndReportCursor}
                         isGroupPolicyReport={isGroupPolicyReport}
@@ -566,30 +506,14 @@ function ReportActionItemMessageEdit({
                         />
                     </View>
 
-                    <View style={styles.alignSelfEnd}>
-                        <Tooltip text={translate('common.saveChanges')}>
-                            <PressableWithFeedback
-                                style={[styles.chatItemSubmitButton, hasExceededMaxCommentLength ? {} : styles.buttonSuccess]}
-                                onPress={publishDraft}
-                                disabled={hasExceededMaxCommentLength}
-                                role={CONST.ROLE.BUTTON}
-                                accessibilityLabel={translate('common.saveChanges')}
-                                hoverDimmingValue={1}
-                                pressDimmingValue={0.2}
-                                // Keep focus on the composer when send button is clicked.
-                                onMouseDown={(e) => e.preventDefault()}
-                                sentryLabel={CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON}
-                            >
-                                <Icon
-                                    src={icons.Checkmark}
-                                    fill={hasExceededMaxCommentLength ? theme.icon : theme.textLight}
-                                />
-                            </PressableWithFeedback>
-                        </Tooltip>
-                    </View>
+                    <SendButton
+                        isDisabled={isExceedingMaxLength}
+                        onSend={publishDraftMessage}
+                        isEditing
+                    />
                 </View>
             </View>
-            {hasExceededMaxCommentLength && <ExceededCommentLength />}
+            {isExceedingMaxLength && <ExceededCommentLength />}
         </>
     );
 }

From ab3ee6e7bea307c3359489fdde8b19b38e64a4bb Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 11:31:36 +0000
Subject: [PATCH 012/233] reactor: update composer ref types in
 `ReportActionComposeFocusManager`

---
 src/libs/ReportActionComposeFocusManager.ts | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts
index 435468cb8a0e..4807c2554344 100644
--- a/src/libs/ReportActionComposeFocusManager.ts
+++ b/src/libs/ReportActionComposeFocusManager.ts
@@ -1,7 +1,6 @@
 import {findFocusedRoute} from '@react-navigation/native';
-import type {RefObject} from 'react';
 import React from 'react';
-import type {TextInput} from 'react-native';
+import type {ComposerRef} from '@components/Composer/types';
 import SCREENS from '@src/SCREENS';
 import isReportOpenInRHP from './Navigation/helpers/isReportOpenInRHP';
 import navigationRef from './Navigation/navigationRef';
@@ -11,11 +10,11 @@ type ComposerType = 'main' | 'edit';
 
 type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void;
 
-const composerRef: RefObject<TextInput | null> = React.createRef<TextInput>();
-
 // There are two types of composer: general composer (edit composer) and main composer.
 // The general composer callback will take priority if it exists.
-const editComposerRef: RefObject<TextInput | null> = React.createRef<TextInput>();
+const composerRef = React.createRef<ComposerRef>();
+const editComposerRef = React.createRef<ComposerRef>();
+
 // There are two types of focus callbacks: priority and general
 // Priority callback would take priority if it existed
 let priorityFocusCallback: FocusCallback | null = null;

From 1d2f1d9ccac91c4525722886b15b20fc0495d2e6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 15:50:23 +0000
Subject: [PATCH 013/233] fix: remove proxy for `useImperativeHandle`

---
 .../Composer/implementation/index.tsx         | 42 ++++++++-----------
 1 file changed, 18 insertions(+), 24 deletions(-)

diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 301b74f2cacb..f3a47b879700 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -285,30 +285,24 @@ function Composer({
         onClear(currentText);
     }, [onClear, onSelectionChange]);
 
-    useImperativeHandle(
-        ref,
-        () =>
-            new Proxy(
-                {},
-                {
-                    get: (_target, prop) => {
-                        if (prop === 'clear') {
-                            return clear;
-                        }
-                        if (prop === 'blur') {
-                            return () => textInputRef.current?.blur();
-                        }
-                        if (prop === 'focus') {
-                            return () => textInputRef.current?.focus();
-                        }
-                        if (prop === 'scrollTop') {
-                            return textInputRef.current?.scrollTop;
-                        }
-                        return textInputRef.current?.[prop as keyof AnimatedMarkdownTextInputRef];
-                    },
-                },
-            ) as ComposerRef,
-    );
+    useImperativeHandle(ref, () => {
+        const textInput = textInputRef.current;
+        if (!textInput) {
+            throw new Error('textInput is not available. This should never happen and indicates a developer error.');
+        }
+
+        return {
+            ...textInput,
+            // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works
+            clear,
+            // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly
+            blur: () => textInput.blur(),
+            focus: () => textInput.focus(),
+            get scrollTop() {
+                return textInput.scrollTop;
+            },
+        } as ComposerRef;
+    }, [clear]);
 
     const handleKeyPress = useCallback(
         (e: TextInputKeyPressEvent) => {

From 3ab79b7af8d8c46684be3f428a4554af503d32ae Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 16:18:23 +0000
Subject: [PATCH 014/233] refactor: update props on `useEditMessage` hook

---
 .../report/ReportActionCompose/ReportActionCompose.tsx |  9 ++++++++-
 .../inbox/report/ReportActionCompose/useEditMessage.ts |  8 ++++----
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx | 10 +++++++++-
 3 files changed, 21 insertions(+), 6 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index d452dee8d98a..309d55c90f9a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -370,7 +370,14 @@ function ReportActionCompose({
 
     const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({reportID});
 
-    const {publishDraft, deleteDraft} = useEditMessage({reportID, reportAction: editingReportAction, index: 0, isFocused, debouncedCommentMaxLengthValidation, composerRef});
+    const {publishDraft, deleteDraft} = useEditMessage({
+        reportID,
+        reportAction: activeEdit?.reportAction,
+        shouldScrollToLastMessage: isEditingLastReportAction,
+        isFocused,
+        debouncedCommentMaxLengthValidation,
+        composerRef,
+    });
 
     const deleteDraftMessage = useCallback(() => {
         deleteDraft();
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 124d7c125383..62847bfc78c2 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -24,7 +24,7 @@ type UseEditMessageProps = {
     reportID: string | undefined;
     originalReportID?: string;
     reportAction: OnyxTypes.ReportAction | null | undefined;
-    index: number;
+    shouldScrollToLastMessage?: boolean;
     isFocused: boolean;
     debouncedCommentMaxLengthValidation: DebouncedFuncLeading<(value: string) => boolean>;
     composerRef: React.RefObject<ComposerRef | null>;
@@ -33,7 +33,7 @@ type UseEditMessageProps = {
 /**
  * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
  */
-function useEditMessage({reportID, originalReportID, reportAction, index, isFocused, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) {
+function useEditMessage({reportID, originalReportID, reportAction, shouldScrollToLastMessage = false, isFocused, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) {
     const reportScrollManager = useReportScrollManager();
     const isFocusedRef = useRef(isFocused);
 
@@ -74,9 +74,9 @@ function useEditMessage({reportID, originalReportID, reportAction, index, isFocu
         }
 
         // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
-        if (index === 0) {
+        if (shouldScrollToLastMessage) {
             KeyboardUtils.dismiss().then(() => {
-                reportScrollManager.scrollToIndex(index, false);
+                reportScrollManager.scrollToIndex(0, false);
             });
         }
     }
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 26d2edef554a..48cc943a9239 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -252,7 +252,15 @@ function ReportActionItemMessageEdit({
         // eslint-disable-next-line react-hooks/exhaustive-deps -- run this only when language is changed
     }, [action.reportActionID, preferredLocale]);
 
-    const {publishDraft, deleteDraft} = useEditMessage({reportID, originalReportID, reportAction: action, index, isFocused, debouncedCommentMaxLengthValidation, composerRef});
+    const {publishDraft, deleteDraft} = useEditMessage({
+        reportID,
+        originalReportID,
+        reportAction: action,
+        shouldScrollToLastMessage: index === 0,
+        isFocused,
+        debouncedCommentMaxLengthValidation,
+        composerRef,
+    });
 
     const publishDraftMessage = useCallback(() => publishDraft(draft), [publishDraft, draft]);
 

From 5717b79107ac0fd5ea1b232293e5ac7c662455bf Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 16:18:57 +0000
Subject: [PATCH 015/233] refactor: editing logic

---
 src/libs/actions/Report/index.ts              |   6 +-
 .../inbox/report/PureReportActionItem.tsx     |  10 +-
 .../ComposerWithSuggestions.tsx               | 104 +++++++-------
 .../ReportActionCompose.tsx                   | 129 ++++++++++--------
 .../inbox/report/ReportActionCompose/types.ts |  12 ++
 5 files changed, 146 insertions(+), 115 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/types.ts

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index dd0ad4dd74c8..bfdbc437b88b 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2381,7 +2381,11 @@ function editReportComment(
 }
 
 /** Deletes the draft for a comment report action. */
-function deleteReportActionDraft(reportID: string | undefined, reportAction: ReportAction) {
+function deleteReportActionDraft(reportID: string | undefined, reportAction: ReportAction | null | undefined) {
+    if (!reportAction) {
+        return;
+    }
+
     const originalReportID = getOriginalReportID(reportID, reportAction, undefined);
     Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`, {[reportAction.reportActionID]: null});
 }
diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 80f71c63b2f6..2cb4f95ddc82 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -59,7 +59,6 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
 import type {OnyxDataWithErrors} from '@libs/ErrorUtils';
 import {getLatestErrorMessageField, isReceiptError} from '@libs/ErrorUtils';
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
-import getIsNarrowLayout from '@libs/getIsNarrowLayout';
 import {isReportMessageAttachment} from '@libs/isReportMessageAttachment';
 import Navigation from '@libs/Navigation/Navigation';
 import Parser from '@libs/Parser';
@@ -578,7 +577,10 @@ function PureReportActionItem({
         isActionableMentionWhisper(action) || isActionableMentionInviteToSubmitExpenseConfirmWhisper(action) || isActionableTrackExpense(action) || isActionableReportMentionWhisper(action);
     const isReportArchived = useReportIsArchived(reportID);
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
-    const isEditingInline = !isNarrowLayout && draftMessage !== undefined;
+    const isEditingInline = !shouldUseNarrowLayout && draftMessage !== undefined;
+
+    console.log({isEditingInline, draftMessage, shouldUseNarrowLayout});
+
     const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID);
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye'] as const);
 
@@ -1932,7 +1934,7 @@ function PureReportActionItem({
             return emptyHTML;
         }
 
-        if (!isNarrowLayout && draftMessage !== undefined) {
+        if (!shouldUseNarrowLayout && draftMessage !== undefined) {
             return <ReportActionItemDraft>{content}</ReportActionItemDraft>;
         }
 
@@ -1940,7 +1942,7 @@ function PureReportActionItem({
             return (
                 <ReportActionItemSingle
                     action={action}
-                    showHeader={draftMessage === undefined || isNarrowLayout}
+                    showHeader={draftMessage === undefined || shouldUseNarrowLayout}
                     wrapperStyle={{
                         ...(isOnSearch && styles.p0),
                         ...(isWhisper && styles.pt1),
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 34824fd4acf9..6eb0e3dd2d06 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -43,6 +43,7 @@ import getScrollPosition from '@pages/inbox/report/ReportActionCompose/getScroll
 import type {SuggestionsRef} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import SilentCommentUpdater from '@pages/inbox/report/ReportActionCompose/SilentCommentUpdater';
 import Suggestions from '@pages/inbox/report/ReportActionCompose/Suggestions';
+import type {ActiveEdit} from '@pages/inbox/report/ReportActionCompose/types';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
 import {inputFocusChange} from '@userActions/InputFocus';
@@ -161,6 +162,15 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
         /** Whether the main composer was hidden */
         didHideComposerInput?: boolean;
 
+        /** Whether the composer is editing in composer */
+        isEditingInComposer: boolean;
+
+        /** The active edit */
+        activeEdit?: ActiveEdit | null;
+
+        /** Function to set the active edit */
+        setActiveEdit: (activeEdit: ActiveEdit | null) => void;
+
         /** Reference to the outer element */
         ref?: Ref<ComposerWithSuggestionsRef | null>;
     };
@@ -207,6 +217,9 @@ function ComposerWithSuggestions({
     lastReportAction,
     isGroupPolicyReport,
     policyID,
+    isEditingInComposer,
+    activeEdit,
+    setActiveEdit,
 
     // Focus
     onFocus,
@@ -257,55 +270,55 @@ function ComposerWithSuggestions({
     const [draftComment = ''] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true});
     const {shouldUseNarrowLayout} = useResponsiveLayout();
 
-    const [allActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
-    const activeInlineEdit = useMemo(() => {
-        if (!shouldUseNarrowLayout) {
-            return null;
-        }
-
-        const reportDrafts = allActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
-
-        if (!reportDrafts) {
-            return null;
-        }
-
-        const entry = Object.entries(reportDrafts).find(([, d]) => d?.message);
-
-        if (!entry) {
-            return null;
-        }
-
-        const [reportActionID, draft] = entry;
-
-        return {
-            reportActionID,
-            message: draft?.message ?? '',
-        };
-    }, [allActionDrafts, reportID, shouldUseNarrowLayout]);
-
-    const initialValue = shouldUseNarrowLayout ? (activeInlineEdit?.message ?? draftComment) : draftComment;
+    const composerRef = useRef<ComposerRef | null>(null);
 
     const [value, setValue] = useState(() => {
+        const initialValue = shouldUseNarrowLayout ? (activeEdit?.message ?? draftComment) : draftComment;
+
         if (initialValue) {
             emojisPresentBefore.current = extractEmojis(initialValue);
         }
         return initialValue;
     });
+    const [selection, setSelection] = useState<TextSelection>(() => ({start: value.length, end: value.length, positionX: 0, positionY: 0}));
+
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
 
     const commentRef = useRef(value);
 
+    // Focus the composer when editing in composer
+    useEffect(() => {
+        if (!isEditingInComposer) {
+            return;
+        }
+
+        composerRef.current?.focus();
+        if (activeEdit?.currentSelection) {
+            setSelection(activeEdit?.currentSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
+        }
+    }, [activeEdit?.currentSelection, isEditingInComposer, value.length]);
+
+    // Reset the composer value when the app extends to wide layout,
+    // because the inline composer is showing up
     useEffect(() => {
-        if (!shouldUseNarrowLayout) {
+        if (!activeEdit || shouldUseNarrowLayout) {
             return;
         }
 
-        const nextValue = activeInlineEdit?.message ?? draftComment ?? '';
+        setValue('');
+    }, [activeEdit, shouldUseNarrowLayout]);
+
+    useEffect(() => {
+        if (!shouldUseNarrowLayout || !activeEdit) {
+            return;
+        }
+
+        const nextValue = activeEdit.message ?? draftComment ?? '';
 
         emojisPresentBefore.current = extractEmojis(nextValue);
         setValue(nextValue);
         commentRef.current = nextValue;
-    }, [activeInlineEdit?.message, activeInlineEdit?.reportActionID, draftComment, shouldUseNarrowLayout]);
+    }, [activeEdit, draftComment, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -329,13 +342,9 @@ function ComposerWithSuggestions({
     const valueRef = useRef(value);
     valueRef.current = value;
 
-    const [selection, setSelection] = useState<TextSelection>(() => ({start: value.length, end: value.length, positionX: 0, positionY: 0}));
-
     const [composerHeightAfterClear, setDefaultComposerHeight] = useState<number | null>(null);
     const emptyComposerHeightRef = useRef<number | null>(null);
 
-    const composerRef = useRef<ComposerRef | null>(null);
-
     const syncSelectionWithOnChangeTextRef = useRef<SyncSelection | null>(null);
 
     // The ref to check whether the comment saving is in progress
@@ -514,10 +523,8 @@ function ComposerWithSuggestions({
 
             commentRef.current = newCommentConverted;
             if (shouldUseNarrowLayout) {
-                const editingReportActionID = activeInlineEdit?.reportActionID;
-
-                if (editingReportActionID) {
-                    saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
+                if (activeEdit?.reportActionID) {
+                    saveReportActionDraft(reportID, {reportActionID: activeEdit.reportActionID} as OnyxTypes.ReportAction, newCommentConverted);
                 }
 
                 if (newCommentConverted) {
@@ -542,7 +549,7 @@ function ComposerWithSuggestions({
             suggestionsRef,
             raiseIsScrollLikelyLayoutTriggered,
             debouncedSaveReportComment,
-            activeInlineEdit?.reportActionID,
+            activeEdit?.reportActionID,
             shouldUseNarrowLayout,
             selection?.end,
             selection?.start,
@@ -665,8 +672,15 @@ function ComposerWithSuggestions({
                 return;
             }
             suggestionsRef.current?.onSelectionChange?.(e);
+
+            if (activeEdit) {
+                setActiveEdit({
+                    ...activeEdit,
+                    currentSelection: e.nativeEvent.selection,
+                });
+            }
         },
-        [suggestionsRef],
+        [activeEdit, setActiveEdit, suggestionsRef],
     );
 
     const hideSuggestionMenu = useCallback(
@@ -781,13 +795,6 @@ function ComposerWithSuggestions({
         [checkComposerVisibility, focus, isSidePanelHiddenOrLargeScreen],
     );
 
-    const blur = useCallback(() => {
-        if (!composerRef.current) {
-            return;
-        }
-        composerRef.current.blur();
-    }, []);
-
     const clearWorklet = useCallback(() => {
         'worklet';
 
@@ -874,9 +881,6 @@ function ComposerWithSuggestions({
                 {},
                 {
                     get: (_target, prop) => {
-                        if (prop === 'blur') {
-                            return blur;
-                        }
                         if (prop === 'focus') {
                             return focus;
                         }
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 309d55c90f9a..45f71d837d86 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -62,7 +62,7 @@ import AgentZeroProcessingRequestIndicator from '@pages/inbox/report/AgentZeroPr
 import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime';
 import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator';
 import {hideEmojiPicker, isActive as isActiveEmojiPickerAction, isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
-import {addAttachmentWithComment, setIsComposerFullSize} from '@userActions/Report';
+import {addAttachmentWithComment, deleteReportActionDraft, setIsComposerFullSize} from '@userActions/Report';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -75,6 +75,7 @@ import ComposerWithSuggestions from './ComposerWithSuggestions';
 import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
 import MessageEditCancelButton from './MessageEditCancelButton';
 import SendButton from './SendButton';
+import type {ActiveEdit} from './types';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
 import useDebouncedCommentMaxLengthValidation from './useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './useEditMessage';
@@ -158,28 +159,60 @@ function ReportActionCompose({
     const [initialModalState] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});
     const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, {canBeMissing: true});
     const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true});
-    const [allActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
+    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
 
-    const activeInlineDraft = useMemo(() => {
-        const reportDrafts = allActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
+    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
+        canEvict: false,
+        canBeMissing: true,
+    });
+
+    const [activeEdit, setActiveEdit] = useState<ActiveEdit | null>(null);
+    const previousActiveEditRef = useRef<ActiveEdit | null>(null);
+
+    // Set the active edit when the report actions or draft comments change
+    useEffect(() => {
+        const previousActiveEdit = previousActiveEditRef.current;
+
+        if (activeEdit && previousActiveEdit && activeEdit !== previousActiveEdit) {
+            deleteReportActionDraft(reportID, activeEdit.reportAction);
+            return;
+        }
+
+        const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
         if (!reportDrafts) {
-            return null;
+            setActiveEdit(null);
+            return;
         }
 
-        const entry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
+        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
 
-        if (!entry) {
-            return null;
+        if (!reportDraftEntry) {
+            setActiveEdit(null);
+            return;
         }
 
-        const [reportActionID, draft] = entry;
+        const [reportActionID, draft] = reportDraftEntry;
 
-        return {
+        previousActiveEditRef.current = activeEdit;
+        setActiveEdit({
             reportActionID,
+            reportAction: reportActions?.[reportActionID] ?? null,
             message: draft.message,
-        };
-    }, [allActionDrafts, reportID]);
+        });
+    }, [activeEdit, reportActionDrafts, reportActions, reportID]);
+
+    const isEditingInComposer = shouldUseNarrowLayout && !!activeEdit;
+
+    const isEditingLastReportAction = useMemo(() => {
+        if (!reportActions) {
+            return false;
+        }
+
+        const lastIndex = Object.keys(reportActions).length - 1;
+
+        return activeEdit?.reportActionID === reportActions[lastIndex]?.reportActionID;
+    }, [activeEdit?.reportActionID, reportActions]);
 
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
 
@@ -198,7 +231,7 @@ function ReportActionCompose({
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
-    const effectiveDraft = shouldUseNarrowLayout ? activeInlineDraft?.message : draftComment;
+    const effectiveDraft = shouldUseNarrowLayout ? activeEdit?.message : draftComment;
 
     const [isCommentEmpty, setIsCommentEmpty] = useState(() => {
         return !effectiveDraft || !!effectiveDraft.match(CONST.REGEX.EMPTY_COMMENT);
@@ -240,33 +273,6 @@ function ReportActionCompose({
     const isTransactionThreadView = useMemo(() => isReportTransactionThread(report), [report]);
     const isExpensesReport = useMemo(() => reportTransactions && reportTransactions.length > 1, [reportTransactions]);
 
-    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
-        canEvict: false,
-        canBeMissing: true,
-    });
-
-    const [reportActionDrafts] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, {canBeMissing: true});
-
-    const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
-    useEffect(() => {
-        if (!reportActionDrafts || editingReportActionID) {
-            return;
-        }
-
-        const entry = Object.entries(reportActionDrafts).find(([, draft]) => draft?.message);
-        setEditingReportActionID(entry?.[0] ?? null);
-    }, [editingReportActionID, reportActionDrafts]);
-
-    const editingReportAction = useMemo(() => {
-        if (!editingReportActionID || !reportActions) {
-            return null;
-        }
-
-        return reportActions[editingReportActionID] ?? null;
-    }, [editingReportActionID, reportActions]);
-
-    const isEditing = shouldUseNarrowLayout && !!editingReportAction;
-
     const personalDetail = useCurrentUserPersonalDetails();
 
     const iouAction = reportActions ? Object.values(reportActions).find((action) => isMoneyRequestAction(action)) : null;
@@ -381,24 +387,24 @@ function ReportActionCompose({
 
     const deleteDraftMessage = useCallback(() => {
         deleteDraft();
-        setEditingReportActionID(null);
+        setActiveEdit(null);
     }, [deleteDraft]);
 
     /**
-     * Add a new comment to this chat
+     * Add or edit a comment in the composer
      */
     const submitForm = useCallback(
-        (newComment: string) => {
-            const newCommentTrimmed = newComment.trim();
+        (draftMessage: string) => {
+            const draftMessageTrimmed = draftMessage.trim();
 
-            if (!newCommentTrimmed && !attachmentFileRef.current) {
+            if (isEditingInComposer && !attachmentFileRef.current) {
+                publishDraft(draftMessageTrimmed);
+                deleteDraft();
+                setActiveEdit(null);
                 return;
             }
 
-            if (isEditing && !attachmentFileRef.current) {
-                publishDraft(newCommentTrimmed);
-                deleteDraft();
-                setEditingReportActionID(null);
+            if (!draftMessageTrimmed && !attachmentFileRef.current) {
                 return;
             }
 
@@ -413,27 +419,27 @@ function ReportActionCompose({
                     ancestors,
                     attachments: attachmentFileRef.current,
                     currentUserAccountID: currentUserPersonalDetails.accountID,
-                    text: newCommentTrimmed,
+                    text: draftMessageTrimmed,
                     timezone: personalDetail.timezone,
                     shouldPlaySound: true,
                     isInSidePanel,
                 });
                 attachmentFileRef.current = null;
             } else {
-                Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
+                Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: draftMessageTrimmed});
                 startSpan(CONST.TELEMETRY.SPAN_SEND_MESSAGE, {
                     name: 'send-message',
                     op: CONST.TELEMETRY.SPAN_SEND_MESSAGE,
                     attributes: {
                         [CONST.TELEMETRY.ATTRIBUTE_REPORT_ID]: reportID,
-                        [CONST.TELEMETRY.ATTRIBUTE_MESSAGE_LENGTH]: newCommentTrimmed.length,
+                        [CONST.TELEMETRY.ATTRIBUTE_MESSAGE_LENGTH]: draftMessageTrimmed.length,
                     },
                 });
-                onSubmit(newCommentTrimmed);
+                onSubmit(draftMessageTrimmed);
             }
         },
         [
-            isEditing,
+            isEditingInComposer,
             isConciergeChat,
             publishDraft,
             deleteDraft,
@@ -475,9 +481,9 @@ function ReportActionCompose({
     }, [onComposerFocus]);
 
     useEffect(() => {
-        const valueToCheck = shouldUseNarrowLayout ? activeInlineDraft?.message : draftComment;
+        const valueToCheck = shouldUseNarrowLayout ? activeEdit?.message : draftComment;
         setIsCommentEmpty(!valueToCheck || !!valueToCheck.match(CONST.REGEX.EMPTY_COMMENT));
-    }, [activeInlineDraft?.message, draftComment, shouldUseNarrowLayout]);
+    }, [activeEdit?.message, draftComment, shouldUseNarrowLayout]);
 
     // We are returning a callback here as we want to invoke the method on unmount only
     useEffect(
@@ -499,8 +505,8 @@ function ReportActionCompose({
 
     const hasReportRecipient = !isEmptyObject(reportRecipient);
 
-    const isNewCommentEmpty = isCommentEmpty && !isEditing;
-    const isSendDisabled = !isEditing && (isBlockedFromConcierge || isExceedingMaxLength || isNewCommentEmpty);
+    const isNewCommentEmpty = isCommentEmpty && !isEditingInComposer;
+    const isSendDisabled = !isEditingInComposer && (isBlockedFromConcierge || isExceedingMaxLength || isNewCommentEmpty);
 
     // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value
     // useSharedValue on web doesn't support functions, so we need to wrap it in an object.
@@ -609,7 +615,7 @@ function ReportActionCompose({
                         ]}
                     >
                         {PDFValidationComponent}
-                        {isEditing ? (
+                        {isEditingInComposer ? (
                             <MessageEditCancelButton onCancel={deleteDraftMessage} />
                         ) : (
                             <AttachmentPickerWithMenuItems
@@ -667,6 +673,9 @@ function ReportActionCompose({
                             onBlur={onBlur}
                             measureParentContainer={measureContainer}
                             onValueChange={onValueChange}
+                            isEditingInComposer={isEditingInComposer}
+                            activeEdit={activeEdit}
+                            setActiveEdit={setActiveEdit}
                             didHideComposerInput={didHideComposerInput}
                             forwardedFSClass={fsClass}
                         />
@@ -708,7 +717,7 @@ function ReportActionCompose({
                             />
                         )}
                         <SendButton
-                            isEditing={isEditing}
+                            isEditing={isEditingInComposer}
                             isDisabled={isSendDisabled}
                             onSend={handleSendMessage}
                         />
diff --git a/src/pages/inbox/report/ReportActionCompose/types.ts b/src/pages/inbox/report/ReportActionCompose/types.ts
new file mode 100644
index 000000000000..817a28880b62
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/types.ts
@@ -0,0 +1,12 @@
+import type {TextSelection} from '@components/Composer/types';
+import type * as OnyxTypes from '@src/types/onyx';
+
+type ActiveEdit = {
+    reportActionID: string;
+    reportAction: OnyxTypes.ReportAction | null;
+    message: string;
+    currentSelection?: TextSelection;
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export type {ActiveEdit};

From d45f5d10438e5fe45ce258339132a6282984f2c6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 16:19:12 +0000
Subject: [PATCH 016/233] refactor: composer ref refactorings

---
 src/pages/inbox/report/PureReportActionItem.tsx | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 2cb4f95ddc82..7d27738b35c6 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -10,6 +10,7 @@ import type {Emoji} from '@assets/emojis/types';
 import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView';
 import {AttachmentContext} from '@components/AttachmentContext';
 import Button from '@components/Button';
+import type {ComposerRef} from '@components/Composer/types';
 import DisplayNames from '@components/DisplayNames';
 import Hoverable from '@components/Hoverable';
 import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
@@ -566,13 +567,12 @@ function PureReportActionItem({
     const reactionListRef = useContext(ReactionListContext);
     const {updateHiddenAttachments} = useContext(AttachmentModalContext);
     const kycWallRef = useContext(KYCWallContext);
-    const composerTextInputRef = useRef<TextInput | HTMLTextAreaElement>(null);
+    const composerRef = useRef<ComposerRef | null>(null);
     const popoverAnchorRef = useRef<Exclude<ContextMenuAnchor, TextInput>>(null);
     const downloadedPreviews = useRef<string[]>([]);
     const prevDraftMessage = usePrevious(draftMessage);
     const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID;
     const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked);
-    const isNarrowLayout = getIsNarrowLayout();
     const isActionableWhisper =
         isActionableMentionWhisper(action) || isActionableMentionInviteToSubmitExpenseConfirmWhisper(action) || isActionableTrackExpense(action) || isActionableReportMentionWhisper(action);
     const isReportArchived = useReportIsArchived(reportID);
@@ -689,7 +689,7 @@ function PureReportActionItem({
             return;
         }
 
-        focusComposerWithDelay(composerTextInputRef.current)(true);
+        focusComposerWithDelay(composerRef.current)(true);
     }, [prevDraftMessage, draftMessage]);
 
     useEffect(() => {
@@ -1793,7 +1793,7 @@ function PureReportActionItem({
                                     originalReportID={originalReportID}
                                     policyID={report?.policyID}
                                     index={index}
-                                    ref={composerTextInputRef}
+                                    ref={composerRef}
                                     shouldDisableEmojiPicker={
                                         (chatIncludesConcierge(report) && isBlockedFromConcierge(blockedFromConcierge)) || isArchivedNonExpenseReport(report, isArchivedRoom)
                                     }

From e7615d138e00debc8dacb863b2fa4192b97d49a9 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 19:50:42 +0000
Subject: [PATCH 017/233] feat: only allow editing one comment

---
 src/libs/actions/Report/index.ts              |  5 +++++
 .../BaseReportActionContextMenu.tsx           |  4 ----
 .../report/ContextMenu/ContextMenuActions.tsx | 21 ++++++-------------
 .../ReportActionCompose.tsx                   |  3 +--
 4 files changed, 12 insertions(+), 21 deletions(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index bfdbc437b88b..0a260d54314f 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2380,6 +2380,10 @@ function editReportComment(
     );
 }
 
+function clearReportActionDrafts(reportID: string | undefined) {
+    Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, null);
+}
+
 /** Deletes the draft for a comment report action. */
 function deleteReportActionDraft(reportID: string | undefined, reportAction: ReportAction | null | undefined) {
     if (!reportAction) {
@@ -6645,6 +6649,7 @@ export {
     completeOnboarding,
     createNewReport,
     deleteReport,
+    clearReportActionDrafts,
     deleteReportActionDraft,
     deleteReportComment,
     deleteReportField,
diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
index c88169aa353a..2fd8696a016b 100755
--- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -240,8 +240,6 @@ function BaseReportActionContextMenu({
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
 
     const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;
-    const [allDraftMessages] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
-    const reportDrafts = originalReportID ? allDraftMessages?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`] : undefined;
 
     const isMoneyRequest = useMemo(() => ReportUtilsIsMoneyRequest(childReport), [childReport]);
     const isTrackExpenseReport = ReportUtilsIsTrackExpenseReport(childReport);
@@ -378,8 +376,6 @@ function BaseReportActionContextMenu({
                             originalReportID,
                             report,
                             draftMessage,
-                            allDraftMessages: reportDrafts,
-                            shouldUseNarrowLayout,
                             selection,
                             close: () => setShouldKeepOpen(false),
                             transitionActionSheetState,
diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index c3a982f80634..176e63bd8da4 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -167,6 +167,7 @@ import {
 import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils';
 import {setDownload} from '@userActions/Download';
 import {
+    clearReportActionDrafts,
     deleteReportActionDraft,
     explain,
     markCommentAsUnread,
@@ -192,7 +193,6 @@ import type {
     ReportAction,
     ReportActionReactions,
     ReportActions,
-    ReportActionsDrafts,
     Report as ReportType,
     Transaction,
 } from '@src/types/onyx';
@@ -258,8 +258,6 @@ type ContextMenuActionPayload = {
     report: OnyxEntry<ReportType>;
     policy?: OnyxEntry<Policy>;
     draftMessage: string;
-    allDraftMessages?: ReportActionsDrafts;
-    shouldUseNarrowLayout: boolean;
     selection: string;
     close: () => void;
     transitionActionSheetState: (params: {type: string; payload?: Record<string, unknown>}) => void;
@@ -518,7 +516,7 @@ const ContextMenuActions: ContextMenuAction[] = [
         icon: 'Pencil',
         shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}) =>
             type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport,
-        onPress: (closePopover, {reportID, reportAction, allDraftMessages, shouldUseNarrowLayout, draftMessage, moneyRequestAction, introSelected}) => {
+        onPress: (closePopover, {reportID, reportAction, draftMessage, moneyRequestAction, introSelected}) => {
             if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) {
                 const editExpense = () => {
                     const childReportID = reportAction?.childReportID;
@@ -533,19 +531,12 @@ const ContextMenuActions: ContextMenuAction[] = [
                 return;
             }
             const editAction = () => {
-                if (!draftMessage) {
-                    if (shouldUseNarrowLayout && allDraftMessages) {
-                        for (const actionID of Object.keys(allDraftMessages)) {
-                            if (actionID === reportAction.reportActionID) {
-                                continue;
-                            }
-                            deleteReportActionDraft(reportID, reportAction);
-                        }
-                    }
+                clearReportActionDrafts(reportID);
 
-                    saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction)));
-                } else {
+                if (draftMessage) {
                     deleteReportActionDraft(reportID, reportAction);
+                } else {
+                    saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction)));
                 }
             };
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 45f71d837d86..0cda52c7893d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -173,8 +173,7 @@ function ReportActionCompose({
     useEffect(() => {
         const previousActiveEdit = previousActiveEditRef.current;
 
-        if (activeEdit && previousActiveEdit && activeEdit !== previousActiveEdit) {
-            deleteReportActionDraft(reportID, activeEdit.reportAction);
+        if (activeEdit) {
             return;
         }
 

From 5494c9c6f6303419c4210c7ef2987b6a54e76f4f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 19:53:49 +0000
Subject: [PATCH 018/233] remove unused code

---
 .../report/ReportActionCompose/ReportActionCompose.tsx      | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 0cda52c7893d..7f9c5d92ec28 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -62,7 +62,7 @@ import AgentZeroProcessingRequestIndicator from '@pages/inbox/report/AgentZeroPr
 import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime';
 import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator';
 import {hideEmojiPicker, isActive as isActiveEmojiPickerAction, isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
-import {addAttachmentWithComment, deleteReportActionDraft, setIsComposerFullSize} from '@userActions/Report';
+import {addAttachmentWithComment, setIsComposerFullSize} from '@userActions/Report';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -167,12 +167,9 @@ function ReportActionCompose({
     });
 
     const [activeEdit, setActiveEdit] = useState<ActiveEdit | null>(null);
-    const previousActiveEditRef = useRef<ActiveEdit | null>(null);
 
     // Set the active edit when the report actions or draft comments change
     useEffect(() => {
-        const previousActiveEdit = previousActiveEditRef.current;
-
         if (activeEdit) {
             return;
         }
@@ -193,7 +190,6 @@ function ReportActionCompose({
 
         const [reportActionID, draft] = reportDraftEntry;
 
-        previousActiveEditRef.current = activeEdit;
         setActiveEdit({
             reportActionID,
             reportAction: reportActions?.[reportActionID] ?? null,

From 7b25160dc1c3ec274164e61da6ce6008a91b5c84 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 20:18:11 +0000
Subject: [PATCH 019/233] refactor: move editing state up into context

---
 src/pages/inbox/ReportScreen.tsx              | 209 +++++++++---------
 .../ComposerWithSuggestions.tsx               |  64 +++---
 .../ReportActionCompose.tsx                   |  53 +----
 .../inbox/report/ReportActionCompose/types.ts |  12 -
 .../report/ReportActionEditMessageContext.tsx | 106 +++++++++
 .../report/ReportActionItemMessageEdit.tsx    |  17 +-
 6 files changed, 268 insertions(+), 193 deletions(-)
 delete mode 100644 src/pages/inbox/report/ReportActionCompose/types.ts
 create mode 100644 src/pages/inbox/report/ReportActionEditMessageContext.tsx

diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index cbf87cb8335a..a38fecae215f 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -111,6 +111,7 @@ import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject';
 import HeaderView from './HeaderView';
 import useReportWasDeleted from './hooks/useReportWasDeleted';
 import ReactionListWrapper from './ReactionListWrapper';
+import {ReportActionEditMessageContextProvider} from './report/ReportActionEditMessageContext';
 import ReportActionsView from './report/ReportActionsView';
 import ReportFooter from './report/ReportFooter';
 import type {ActionListContextType, ScrollPosition} from './ReportScreenContext';
@@ -1010,112 +1011,114 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
         // Wide RHP overlays should be rendered only for the report screen displayed in RHP
         <WideRHPOverlayWrapper shouldWrap={route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT}>
             <ActionListContext.Provider value={actionListValue}>
-                <ReactionListWrapper>
-                    <ScreenWrapper
-                        navigation={navigation}
-                        style={screenWrapperStyle}
-                        shouldEnableKeyboardAvoidingView={isTopMostReportId || isInNarrowPaneModal}
-                        testID={`report-screen-${reportID}`}
-                    >
-                        <FullPageNotFoundView
-                            shouldShow={shouldShowNotFoundPage}
-                            subtitleKey={shouldShowNotFoundLinkedAction ? 'notFound.commentYouLookingForCannotBeFound' : 'notFound.noAccess'}
-                            subtitleStyle={[styles.textSupporting]}
-                            shouldShowBackButton={shouldUseNarrowLayout}
-                            onBackButtonPress={shouldShowNotFoundLinkedAction ? navigateToEndOfReport : Navigation.goBack}
-                            shouldShowLink={shouldShowNotFoundLinkedAction}
-                            linkTranslationKey="notFound.goToChatInstead"
-                            subtitleKeyBelowLink={shouldShowNotFoundLinkedAction ? 'notFound.contactConcierge' : ''}
-                            onLinkPress={navigateToEndOfReport}
-                            shouldDisplaySearchRouter
+                <ReportActionEditMessageContextProvider reportID={reportID}>
+                    <ReactionListWrapper>
+                        <ScreenWrapper
+                            navigation={navigation}
+                            style={screenWrapperStyle}
+                            shouldEnableKeyboardAvoidingView={isTopMostReportId || isInNarrowPaneModal}
+                            testID={`report-screen-${reportID}`}
                         >
-                            <DragAndDropProvider isDisabled={isEditingDisabled}>
-                                <OfflineWithFeedback
-                                    pendingAction={reportPendingAction ?? report?.pendingFields?.reimbursed}
-                                    errors={reportErrors}
-                                    shouldShowErrorMessages={false}
-                                    needsOffscreenAlphaCompositing
-                                >
-                                    {headerView}
-                                </OfflineWithFeedback>
-                                {!!accountManagerReportID && isConciergeChatReport(report) && isBannerVisible && (
-                                    <Banner
-                                        containerStyles={[styles.mh4, styles.mt4, styles.p4, styles.br2]}
-                                        text={chatWithAccountManagerText}
-                                        onClose={dismissBanner}
-                                        onButtonPress={chatWithAccountManager}
-                                        shouldShowCloseButton
-                                        icon={expensifyIcons.Lightbulb}
-                                        shouldShowIcon
-                                        shouldShowButton
-                                    />
-                                )}
-                                <View style={[styles.flex1, styles.flexRow]}>
-                                    {shouldShowWideRHP && (
-                                        <Animated.View style={styles.wideRHPMoneyRequestReceiptViewContainer}>
-                                            <ScrollView contentContainerStyle={styles.wideRHPMoneyRequestReceiptViewScrollViewContainer}>
-                                                <MoneyRequestReceiptView
-                                                    allReports={allReports}
-                                                    report={transactionThreadReport ?? report}
-                                                    fillSpace
-                                                    isDisplayedInWideRHP
-                                                />
-                                            </ScrollView>
-                                        </Animated.View>
-                                    )}
-                                    <View
-                                        style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
-                                        testID="report-actions-view-wrapper"
+                            <FullPageNotFoundView
+                                shouldShow={shouldShowNotFoundPage}
+                                subtitleKey={shouldShowNotFoundLinkedAction ? 'notFound.commentYouLookingForCannotBeFound' : 'notFound.noAccess'}
+                                subtitleStyle={[styles.textSupporting]}
+                                shouldShowBackButton={shouldUseNarrowLayout}
+                                onBackButtonPress={shouldShowNotFoundLinkedAction ? navigateToEndOfReport : Navigation.goBack}
+                                shouldShowLink={shouldShowNotFoundLinkedAction}
+                                linkTranslationKey="notFound.goToChatInstead"
+                                subtitleKeyBelowLink={shouldShowNotFoundLinkedAction ? 'notFound.contactConcierge' : ''}
+                                onLinkPress={navigateToEndOfReport}
+                                shouldDisplaySearchRouter
+                            >
+                                <DragAndDropProvider isDisabled={isEditingDisabled}>
+                                    <OfflineWithFeedback
+                                        pendingAction={reportPendingAction ?? report?.pendingFields?.reimbursed}
+                                        errors={reportErrors}
+                                        shouldShowErrorMessages={false}
+                                        needsOffscreenAlphaCompositing
                                     >
-                                        {(!report || shouldWaitForTransactions) && <ReportActionsSkeletonView />}
-                                        {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? (
-                                            <ReportActionsView
-                                                report={report}
-                                                reportActions={reportActions}
-                                                isLoadingInitialReportActions={reportMetadata?.isLoadingInitialReportActions}
-                                                hasNewerActions={hasNewerActions}
-                                                hasOlderActions={hasOlderActions}
-                                                parentReportAction={parentReportAction}
-                                                transactionThreadReportID={transactionThreadReportID}
-                                                isReportTransactionThread={isTransactionThreadView}
-                                            />
-                                        ) : null}
-                                        {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? (
-                                            <MoneyRequestReportActionsList
-                                                report={report}
-                                                hasPendingDeletionTransaction={hasPendingDeletionTransaction}
-                                                policy={policy}
-                                                reportActions={reportActions}
-                                                transactions={visibleTransactions}
-                                                newTransactions={newTransactions}
-                                                violations={allReportViolations}
-                                                hasOlderActions={hasOlderActions}
-                                                hasNewerActions={hasNewerActions}
-                                                showReportActionsLoadingState={showReportActionsLoadingState}
-                                                reportPendingAction={reportPendingAction}
-                                            />
-                                        ) : null}
-                                        {isCurrentReportLoadedFromOnyx ? (
-                                            <ReportFooter
-                                                report={report}
-                                                reportMetadata={reportMetadata}
-                                                policy={policy}
-                                                pendingAction={reportPendingAction}
-                                                isComposerFullSize={!!isComposerFullSize}
-                                                lastReportAction={lastReportAction}
-                                                reportTransactions={reportTransactions}
-                                                // If the report is from the 'Send Money' flow, we add the comment to the `iou` report because for these we don't combine reportActions even if there is a single transaction (they always have a single transaction)
-                                                transactionThreadReportID={isSentMoneyReport ? undefined : transactionThreadReportID}
-                                                isInSidePanel={isInSidePanel}
-                                            />
-                                        ) : null}
+                                        {headerView}
+                                    </OfflineWithFeedback>
+                                    {!!accountManagerReportID && isConciergeChatReport(report) && isBannerVisible && (
+                                        <Banner
+                                            containerStyles={[styles.mh4, styles.mt4, styles.p4, styles.br2]}
+                                            text={chatWithAccountManagerText}
+                                            onClose={dismissBanner}
+                                            onButtonPress={chatWithAccountManager}
+                                            shouldShowCloseButton
+                                            icon={expensifyIcons.Lightbulb}
+                                            shouldShowIcon
+                                            shouldShowButton
+                                        />
+                                    )}
+                                    <View style={[styles.flex1, styles.flexRow]}>
+                                        {shouldShowWideRHP && (
+                                            <Animated.View style={styles.wideRHPMoneyRequestReceiptViewContainer}>
+                                                <ScrollView contentContainerStyle={styles.wideRHPMoneyRequestReceiptViewScrollViewContainer}>
+                                                    <MoneyRequestReceiptView
+                                                        allReports={allReports}
+                                                        report={transactionThreadReport ?? report}
+                                                        fillSpace
+                                                        isDisplayedInWideRHP
+                                                    />
+                                                </ScrollView>
+                                            </Animated.View>
+                                        )}
+                                        <View
+                                            style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
+                                            testID="report-actions-view-wrapper"
+                                        >
+                                            {(!report || shouldWaitForTransactions) && <ReportActionsSkeletonView />}
+                                            {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? (
+                                                <ReportActionsView
+                                                    report={report}
+                                                    reportActions={reportActions}
+                                                    isLoadingInitialReportActions={reportMetadata?.isLoadingInitialReportActions}
+                                                    hasNewerActions={hasNewerActions}
+                                                    hasOlderActions={hasOlderActions}
+                                                    parentReportAction={parentReportAction}
+                                                    transactionThreadReportID={transactionThreadReportID}
+                                                    isReportTransactionThread={isTransactionThreadView}
+                                                />
+                                            ) : null}
+                                            {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? (
+                                                <MoneyRequestReportActionsList
+                                                    report={report}
+                                                    hasPendingDeletionTransaction={hasPendingDeletionTransaction}
+                                                    policy={policy}
+                                                    reportActions={reportActions}
+                                                    transactions={visibleTransactions}
+                                                    newTransactions={newTransactions}
+                                                    violations={allReportViolations}
+                                                    hasOlderActions={hasOlderActions}
+                                                    hasNewerActions={hasNewerActions}
+                                                    showReportActionsLoadingState={showReportActionsLoadingState}
+                                                    reportPendingAction={reportPendingAction}
+                                                />
+                                            ) : null}
+                                            {isCurrentReportLoadedFromOnyx ? (
+                                                <ReportFooter
+                                                    report={report}
+                                                    reportMetadata={reportMetadata}
+                                                    policy={policy}
+                                                    pendingAction={reportPendingAction}
+                                                    isComposerFullSize={!!isComposerFullSize}
+                                                    lastReportAction={lastReportAction}
+                                                    reportTransactions={reportTransactions}
+                                                    // If the report is from the 'Send Money' flow, we add the comment to the `iou` report because for these we don't combine reportActions even if there is a single transaction (they always have a single transaction)
+                                                    transactionThreadReportID={isSentMoneyReport ? undefined : transactionThreadReportID}
+                                                    isInSidePanel={isInSidePanel}
+                                                />
+                                            ) : null}
+                                        </View>
                                     </View>
-                                </View>
-                                <PortalHost name="suggestions" />
-                            </DragAndDropProvider>
-                        </FullPageNotFoundView>
-                    </ScreenWrapper>
-                </ReactionListWrapper>
+                                    <PortalHost name="suggestions" />
+                                </DragAndDropProvider>
+                            </FullPageNotFoundView>
+                        </ScreenWrapper>
+                    </ReactionListWrapper>
+                </ReportActionEditMessageContextProvider>
             </ActionListContext.Provider>
         </WideRHPOverlayWrapper>
     );
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 6eb0e3dd2d06..4b3d868150d2 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -43,7 +43,7 @@ import getScrollPosition from '@pages/inbox/report/ReportActionCompose/getScroll
 import type {SuggestionsRef} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import SilentCommentUpdater from '@pages/inbox/report/ReportActionCompose/SilentCommentUpdater';
 import Suggestions from '@pages/inbox/report/ReportActionCompose/Suggestions';
-import type {ActiveEdit} from '@pages/inbox/report/ReportActionCompose/types';
+import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
 import {inputFocusChange} from '@userActions/InputFocus';
@@ -165,12 +165,6 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
         /** Whether the composer is editing in composer */
         isEditingInComposer: boolean;
 
-        /** The active edit */
-        activeEdit?: ActiveEdit | null;
-
-        /** Function to set the active edit */
-        setActiveEdit: (activeEdit: ActiveEdit | null) => void;
-
         /** Reference to the outer element */
         ref?: Ref<ComposerWithSuggestionsRef | null>;
     };
@@ -218,8 +212,6 @@ function ComposerWithSuggestions({
     isGroupPolicyReport,
     policyID,
     isEditingInComposer,
-    activeEdit,
-    setActiveEdit,
 
     // Focus
     onFocus,
@@ -272,8 +264,10 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
+    const {editingReportActionID, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection} = useReportActionActiveEdit();
+
     const [value, setValue] = useState(() => {
-        const initialValue = shouldUseNarrowLayout ? (activeEdit?.message ?? draftComment) : draftComment;
+        const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
 
         if (initialValue) {
             emojisPresentBefore.current = extractEmojis(initialValue);
@@ -293,32 +287,32 @@ function ComposerWithSuggestions({
         }
 
         composerRef.current?.focus();
-        if (activeEdit?.currentSelection) {
-            setSelection(activeEdit?.currentSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
+        if (currentEditMessageSelection) {
+            setSelection(currentEditMessageSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
         }
-    }, [activeEdit?.currentSelection, isEditingInComposer, value.length]);
+    }, [currentEditMessageSelection, isEditingInComposer, value.length]);
 
     // Reset the composer value when the app extends to wide layout,
     // because the inline composer is showing up
     useEffect(() => {
-        if (!activeEdit || shouldUseNarrowLayout) {
+        if (!editingReportActionID || shouldUseNarrowLayout) {
             return;
         }
 
         setValue('');
-    }, [activeEdit, shouldUseNarrowLayout]);
+    }, [editingReportActionID, shouldUseNarrowLayout]);
 
     useEffect(() => {
-        if (!shouldUseNarrowLayout || !activeEdit) {
+        if (!shouldUseNarrowLayout || !editingReportActionID) {
             return;
         }
 
-        const nextValue = activeEdit.message ?? draftComment ?? '';
+        const nextValue = editingMessage ?? draftComment ?? '';
 
         emojisPresentBefore.current = extractEmojis(nextValue);
         setValue(nextValue);
         commentRef.current = nextValue;
-    }, [activeEdit, draftComment, shouldUseNarrowLayout]);
+    }, [editingMessage, draftComment, shouldUseNarrowLayout, editingReportActionID]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -523,8 +517,8 @@ function ComposerWithSuggestions({
 
             commentRef.current = newCommentConverted;
             if (shouldUseNarrowLayout) {
-                if (activeEdit?.reportActionID) {
-                    saveReportActionDraft(reportID, {reportActionID: activeEdit.reportActionID} as OnyxTypes.ReportAction, newCommentConverted);
+                if (editingReportActionID) {
+                    saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
                 }
 
                 if (newCommentConverted) {
@@ -541,19 +535,19 @@ function ComposerWithSuggestions({
             }
         },
         [
+            raiseIsScrollLikelyLayoutTriggered,
+            selection?.start,
+            selection.end,
             findNewlyAddedChars,
-            preferredLocale,
             preferredSkinTone,
-            reportID,
-            setIsCommentEmpty,
-            suggestionsRef,
-            raiseIsScrollLikelyLayoutTriggered,
-            debouncedSaveReportComment,
-            activeEdit?.reportActionID,
+            preferredLocale,
             shouldUseNarrowLayout,
-            selection?.end,
-            selection?.start,
+            suggestionsRef,
+            setIsCommentEmpty,
+            editingReportActionID,
+            reportID,
             currentUserAccountID,
+            debouncedSaveReportComment,
         ],
     );
 
@@ -673,14 +667,16 @@ function ComposerWithSuggestions({
             }
             suggestionsRef.current?.onSelectionChange?.(e);
 
-            if (activeEdit) {
-                setActiveEdit({
-                    ...activeEdit,
-                    currentSelection: e.nativeEvent.selection,
+            if (editingReportActionID) {
+                setCurrentEditMessageSelection({
+                    start: e.nativeEvent.selection.start,
+                    end: e.nativeEvent.selection.end,
+                    positionX: 0,
+                    positionY: 0,
                 });
             }
         },
-        [activeEdit, setActiveEdit, suggestionsRef],
+        [editingReportActionID, setCurrentEditMessageSelection, suggestionsRef],
     );
 
     const hideSuggestionMenu = useCallback(
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 7f9c5d92ec28..135cc534f968 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -60,6 +60,7 @@ import {getTransactionID, hasReceipt as hasReceiptTransactionUtils} from '@libs/
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
 import AgentZeroProcessingRequestIndicator from '@pages/inbox/report/AgentZeroProcessingRequestIndicator';
 import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime';
+import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator';
 import {hideEmojiPicker, isActive as isActiveEmojiPickerAction, isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import {addAttachmentWithComment, setIsComposerFullSize} from '@userActions/Report';
@@ -75,7 +76,6 @@ import ComposerWithSuggestions from './ComposerWithSuggestions';
 import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
 import MessageEditCancelButton from './MessageEditCancelButton';
 import SendButton from './SendButton';
-import type {ActiveEdit} from './types';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
 import useDebouncedCommentMaxLengthValidation from './useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './useEditMessage';
@@ -159,45 +159,15 @@ function ReportActionCompose({
     const [initialModalState] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});
     const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, {canBeMissing: true});
     const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true});
-    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
 
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
         canEvict: false,
         canBeMissing: true,
     });
 
-    const [activeEdit, setActiveEdit] = useState<ActiveEdit | null>(null);
+    const {editingReportActionID, editingReportAction, editingMessage, setActiveEdit} = useReportActionActiveEdit();
 
-    // Set the active edit when the report actions or draft comments change
-    useEffect(() => {
-        if (activeEdit) {
-            return;
-        }
-
-        const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
-
-        if (!reportDrafts) {
-            setActiveEdit(null);
-            return;
-        }
-
-        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
-
-        if (!reportDraftEntry) {
-            setActiveEdit(null);
-            return;
-        }
-
-        const [reportActionID, draft] = reportDraftEntry;
-
-        setActiveEdit({
-            reportActionID,
-            reportAction: reportActions?.[reportActionID] ?? null,
-            message: draft.message,
-        });
-    }, [activeEdit, reportActionDrafts, reportActions, reportID]);
-
-    const isEditingInComposer = shouldUseNarrowLayout && !!activeEdit;
+    const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID;
 
     const isEditingLastReportAction = useMemo(() => {
         if (!reportActions) {
@@ -206,8 +176,8 @@ function ReportActionCompose({
 
         const lastIndex = Object.keys(reportActions).length - 1;
 
-        return activeEdit?.reportActionID === reportActions[lastIndex]?.reportActionID;
-    }, [activeEdit?.reportActionID, reportActions]);
+        return editingReportActionID === reportActions[lastIndex]?.reportActionID;
+    }, [editingReportActionID, reportActions]);
 
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
 
@@ -226,7 +196,7 @@ function ReportActionCompose({
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
-    const effectiveDraft = shouldUseNarrowLayout ? activeEdit?.message : draftComment;
+    const effectiveDraft = shouldUseNarrowLayout ? editingMessage : draftComment;
 
     const [isCommentEmpty, setIsCommentEmpty] = useState(() => {
         return !effectiveDraft || !!effectiveDraft.match(CONST.REGEX.EMPTY_COMMENT);
@@ -373,7 +343,7 @@ function ReportActionCompose({
 
     const {publishDraft, deleteDraft} = useEditMessage({
         reportID,
-        reportAction: activeEdit?.reportAction,
+        reportAction: editingReportAction,
         shouldScrollToLastMessage: isEditingLastReportAction,
         isFocused,
         debouncedCommentMaxLengthValidation,
@@ -383,7 +353,7 @@ function ReportActionCompose({
     const deleteDraftMessage = useCallback(() => {
         deleteDraft();
         setActiveEdit(null);
-    }, [deleteDraft]);
+    }, [deleteDraft, setActiveEdit]);
 
     /**
      * Add or edit a comment in the composer
@@ -438,6 +408,7 @@ function ReportActionCompose({
             isConciergeChat,
             publishDraft,
             deleteDraft,
+            setActiveEdit,
             kickoffWaitingIndicator,
             transactionThreadReport,
             report,
@@ -476,9 +447,9 @@ function ReportActionCompose({
     }, [onComposerFocus]);
 
     useEffect(() => {
-        const valueToCheck = shouldUseNarrowLayout ? activeEdit?.message : draftComment;
+        const valueToCheck = shouldUseNarrowLayout ? editingMessage : draftComment;
         setIsCommentEmpty(!valueToCheck || !!valueToCheck.match(CONST.REGEX.EMPTY_COMMENT));
-    }, [activeEdit?.message, draftComment, shouldUseNarrowLayout]);
+    }, [editingMessage, draftComment, shouldUseNarrowLayout]);
 
     // We are returning a callback here as we want to invoke the method on unmount only
     useEffect(
@@ -669,8 +640,6 @@ function ReportActionCompose({
                             measureParentContainer={measureContainer}
                             onValueChange={onValueChange}
                             isEditingInComposer={isEditingInComposer}
-                            activeEdit={activeEdit}
-                            setActiveEdit={setActiveEdit}
                             didHideComposerInput={didHideComposerInput}
                             forwardedFSClass={fsClass}
                         />
diff --git a/src/pages/inbox/report/ReportActionCompose/types.ts b/src/pages/inbox/report/ReportActionCompose/types.ts
deleted file mode 100644
index 817a28880b62..000000000000
--- a/src/pages/inbox/report/ReportActionCompose/types.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type {TextSelection} from '@components/Composer/types';
-import type * as OnyxTypes from '@src/types/onyx';
-
-type ActiveEdit = {
-    reportActionID: string;
-    reportAction: OnyxTypes.ReportAction | null;
-    message: string;
-    currentSelection?: TextSelection;
-};
-
-// eslint-disable-next-line import/prefer-default-export
-export type {ActiveEdit};
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
new file mode 100644
index 000000000000..60c0655ec882
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -0,0 +1,106 @@
+import {createContext, useContext, useEffect, useState} from 'react';
+import type {TextSelection} from '@components/Composer/types';
+import useOnyx from '@hooks/useOnyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxTypes from '@src/types/onyx';
+
+const NOOP = () => {};
+
+type ReportActionActiveEdit = {
+    editingReportActionID: string | null;
+    editingReportAction: OnyxTypes.ReportAction | null;
+    editingMessage: string | null;
+};
+
+type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
+    setActiveEdit: (activeEdit: ReportActionActiveEdit | null) => void;
+
+    currentEditMessageSelection: TextSelection | null;
+    setCurrentEditMessageSelection: (selection: TextSelection) => void;
+};
+
+const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
+    editingReportActionID: null,
+    editingReportAction: null,
+    editingMessage: null,
+    setActiveEdit: NOOP,
+
+    currentEditMessageSelection: null,
+    setCurrentEditMessageSelection: NOOP,
+});
+
+type ReportActionEditMessageContextProviderProps = {
+    reportID: string | undefined;
+    children: React.ReactNode;
+};
+
+function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
+    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID ?? ''}`, {
+        canEvict: false,
+        canBeMissing: true,
+    });
+    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
+
+    const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
+    const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
+    const [editingMessage, setEditingMessage] = useState<string | null>(null);
+    const [currentEditMessageSelection, setCurrentEditMessageSelection] = useState<TextSelection | null>(null);
+
+    function setActiveEdit(activeEdit: ReportActionActiveEdit | null) {
+        setEditingReportActionID(activeEdit?.editingReportActionID ?? null);
+        setEditingReportAction(activeEdit?.editingReportAction ?? null);
+        setEditingMessage(activeEdit?.editingMessage ?? null);
+    }
+
+    // Set the active edit when the report actions or draft comments change
+    useEffect(() => {
+        if (editingReportActionID) {
+            return;
+        }
+
+        const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
+
+        if (!reportDrafts) {
+            setActiveEdit(null);
+            return;
+        }
+
+        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
+
+        if (!reportDraftEntry) {
+            setActiveEdit(null);
+            return;
+        }
+
+        const [reportActionID, draft] = reportDraftEntry;
+
+        setActiveEdit({
+            editingReportActionID: reportActionID,
+            editingReportAction: reportActions?.[reportActionID] ?? null,
+            editingMessage: draft.message,
+        });
+    }, [editingReportActionID, reportActionDrafts, reportActions, reportID]);
+
+    return (
+        <ReportActionEditMessageContext.Provider
+            // eslint-disable-next-line react/jsx-no-constructed-context-values
+            value={{
+                editingReportActionID,
+                editingReportAction,
+                editingMessage,
+                setActiveEdit,
+                currentEditMessageSelection,
+                setCurrentEditMessageSelection,
+            }}
+        >
+            {children}
+        </ReportActionEditMessageContext.Provider>
+    );
+}
+
+function useReportActionActiveEdit() {
+    return useContext(ReportActionEditMessageContext);
+}
+
+export {ReportActionEditMessageContextProvider, useReportActionActiveEdit, ReportActionEditMessageContext};
+export type {ReportActionActiveEdit, ReportActionEditMessageContextValue};
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 48cc943a9239..e30e17ff4dc2 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -50,6 +50,7 @@ import SendButton from './ReportActionCompose/SendButton';
 import Suggestions from './ReportActionCompose/Suggestions';
 import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './ReportActionCompose/useEditMessage';
+import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
 
 type ReportActionItemMessageEditProps = {
@@ -119,7 +120,19 @@ function ReportActionItemMessageEdit({
         }
         return draftMessage;
     });
-    const [selection, setSelection] = useState<TextSelection>({start: draft.length, end: draft.length, positionX: 0, positionY: 0});
+
+    const {setCurrentEditMessageSelection} = useReportActionActiveEdit();
+
+    const [selection, setSelectionState] = useState<TextSelection>({start: draft.length, end: draft.length, positionX: 0, positionY: 0});
+
+    const setSelection = useCallback(
+        (newSelection: TextSelection) => {
+            setSelectionState(newSelection);
+            setCurrentEditMessageSelection({...newSelection, positionX: 0, positionY: 0});
+        },
+        [setSelectionState, setCurrentEditMessageSelection],
+    );
+
     const [isFocused, setIsFocused] = useState<boolean>(false);
 
     const {debouncedCommentMaxLengthValidation, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID});
@@ -244,7 +257,7 @@ function ReportActionItemMessageEdit({
             debouncedSaveDraft(newDraft);
             isCommentPendingSaved.current = true;
         },
-        [raiseIsScrollLayoutTriggered, debouncedSaveDraft, preferredSkinTone, preferredLocale, selection.end],
+        [raiseIsScrollLayoutTriggered, preferredSkinTone, preferredLocale, debouncedSaveDraft, selection?.end, setSelection],
     );
 
     useEffect(() => {

From ade943eb85778b4df7d54d81696944bcac5fe87f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 20:45:21 +0000
Subject: [PATCH 020/233] remove: console.log

---
 src/pages/inbox/report/PureReportActionItem.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 7d27738b35c6..d30291bfaa6e 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -579,8 +579,6 @@ function PureReportActionItem({
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
     const isEditingInline = !shouldUseNarrowLayout && draftMessage !== undefined;
 
-    console.log({isEditingInline, draftMessage, shouldUseNarrowLayout});
-
     const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID);
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye'] as const);
 

From 722699e4c0d4619c50492bedfe4be29d7020b414 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 20:45:42 +0000
Subject: [PATCH 021/233] feat: implement persisting text selection

---
 .../ComposerWithSuggestions.tsx               | 80 ++++++++++++-------
 .../report/ReportActionEditMessageContext.tsx | 10 ++-
 .../report/ReportActionItemMessageEdit.tsx    |  9 ++-
 3 files changed, 69 insertions(+), 30 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 4b3d868150d2..f3eec1407537 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -274,7 +274,7 @@ function ComposerWithSuggestions({
         }
         return initialValue;
     });
-    const [selection, setSelection] = useState<TextSelection>(() => ({start: value.length, end: value.length, positionX: 0, positionY: 0}));
+    const [selection, setSelection] = useState<TextSelection>(() => currentEditMessageSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
 
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
 
@@ -287,9 +287,6 @@ function ComposerWithSuggestions({
         }
 
         composerRef.current?.focus();
-        if (currentEditMessageSelection) {
-            setSelection(currentEditMessageSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
-        }
     }, [currentEditMessageSelection, isEditingInComposer, value.length]);
 
     // Reset the composer value when the app extends to wide layout,
@@ -513,6 +510,8 @@ function ComposerWithSuggestions({
                     positionX: prevSelection.positionX,
                     positionY: prevSelection.positionY,
                 }));
+
+                setCurrentEditMessageSelection({...currentEditMessageSelection, start: position, end: position});
             }
 
             commentRef.current = newCommentConverted;
@@ -544,6 +543,8 @@ function ComposerWithSuggestions({
             shouldUseNarrowLayout,
             suggestionsRef,
             setIsCommentEmpty,
+            setCurrentEditMessageSelection,
+            currentEditMessageSelection,
             editingReportActionID,
             reportID,
             currentUserAccountID,
@@ -610,17 +611,35 @@ function ComposerWithSuggestions({
                 if (lastGraphemeLength > 1) {
                     event.preventDefault();
                     const newText = lastTextRef.current.slice(0, selection.start - lastGraphemeLength) + lastTextRef.current.slice(selection.start);
+                    const newStart = selection.start - lastGraphemeLength;
+                    const newEnd = selection.start - lastGraphemeLength;
+
                     setSelection((prevSelection) => ({
-                        start: selection.start - lastGraphemeLength,
-                        end: selection.start - lastGraphemeLength,
+                        start: newStart,
+                        end: newEnd,
                         positionX: prevSelection.positionX,
                         positionY: prevSelection.positionY,
                     }));
+
+                    setCurrentEditMessageSelection({...currentEditMessageSelection, start: newStart, end: newEnd});
                     updateComment(newText, true);
                 }
             }
         },
-        [shouldUseNarrowLayout, isKeyboardShown, suggestionsRef, selection.start, includeChronos, onEnterKeyPress, lastReportAction, reportID, updateComment, selection.end],
+        [
+            shouldUseNarrowLayout,
+            isKeyboardShown,
+            suggestionsRef,
+            selection.start,
+            selection.end,
+            includeChronos,
+            onEnterKeyPress,
+            lastReportAction,
+            reportID,
+            setCurrentEditMessageSelection,
+            currentEditMessageSelection,
+            updateComment,
+        ],
     );
 
     /**
@@ -662,21 +681,19 @@ function ComposerWithSuggestions({
         (e: CustomSelectionChangeEvent) => {
             setSelection(e.nativeEvent.selection);
 
+            setCurrentEditMessageSelection({
+                start: e.nativeEvent.selection.start,
+                end: e.nativeEvent.selection.end,
+                positionX: 0,
+                positionY: 0,
+            });
+
             if (!composerRef.current?.isFocused()) {
                 return;
             }
             suggestionsRef.current?.onSelectionChange?.(e);
-
-            if (editingReportActionID) {
-                setCurrentEditMessageSelection({
-                    start: e.nativeEvent.selection.start,
-                    end: e.nativeEvent.selection.end,
-                    positionX: 0,
-                    positionY: 0,
-                });
-            }
         },
-        [editingReportActionID, setCurrentEditMessageSelection, suggestionsRef],
+        [setCurrentEditMessageSelection, suggestionsRef],
     );
 
     const hideSuggestionMenu = useCallback(
@@ -983,18 +1000,27 @@ function ComposerWithSuggestions({
 
     // When using the suggestions box (Suggestions) we need to imperatively
     // set the cursor to the end of the suggestion/mention after it's selected.
-    const onSuggestionSelected = useCallback((suggestionSelection: TextSelection) => {
-        const endOfSuggestionSelection = suggestionSelection.end;
-        setSelection(suggestionSelection);
+    const onSuggestionSelected = useCallback(
+        (suggestionSelection: TextSelection) => {
+            const endOfSuggestionSelection = suggestionSelection.end;
+            setSelection(suggestionSelection);
+            setCurrentEditMessageSelection({
+                start: suggestionSelection.start,
+                end: suggestionSelection.end,
+                positionX: 0,
+                positionY: 0,
+            });
 
-        if (endOfSuggestionSelection === undefined) {
-            return;
-        }
+            if (endOfSuggestionSelection === undefined) {
+                return;
+            }
 
-        queueMicrotask(() => {
-            composerRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection);
-        });
-    }, []);
+            queueMicrotask(() => {
+                composerRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection);
+            });
+        },
+        [setCurrentEditMessageSelection],
+    );
 
     return (
         <>
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 60c0655ec882..7664d18d891b 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -44,7 +44,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
     const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
-    const [currentEditMessageSelection, setCurrentEditMessageSelection] = useState<TextSelection | null>(null);
+    const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
 
     function setActiveEdit(activeEdit: ReportActionActiveEdit | null) {
         setEditingReportActionID(activeEdit?.editingReportActionID ?? null);
@@ -52,6 +52,14 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         setEditingMessage(activeEdit?.editingMessage ?? null);
     }
 
+    function setCurrentEditMessageSelection(selection: TextSelection) {
+        if (!editingReportActionID) {
+            return;
+        }
+
+        setCurrentEditMessageSelectionState(selection);
+    }
+
     // Set the active edit when the report actions or draft comments change
     useEffect(() => {
         if (editingReportActionID) {
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index e30e17ff4dc2..d03bce1aa96e 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -121,9 +121,10 @@ function ReportActionItemMessageEdit({
         return draftMessage;
     });
 
-    const {setCurrentEditMessageSelection} = useReportActionActiveEdit();
+    const {currentEditMessageSelection, setCurrentEditMessageSelection} = useReportActionActiveEdit();
 
-    const [selection, setSelectionState] = useState<TextSelection>({start: draft.length, end: draft.length, positionX: 0, positionY: 0});
+    const defaultSelection = useMemo(() => ({start: draft.length, end: draft.length, positionX: 0, positionY: 0}), [draft.length]);
+    const [selection, setSelectionState] = useState<TextSelection>(() => currentEditMessageSelection ?? defaultSelection);
 
     const setSelection = useCallback(
         (newSelection: TextSelection) => {
@@ -133,6 +134,10 @@ function ReportActionItemMessageEdit({
         [setSelectionState, setCurrentEditMessageSelection],
     );
 
+    useEffect(() => {
+        setSelectionState(currentEditMessageSelection ?? defaultSelection);
+    }, [currentEditMessageSelection, defaultSelection, draft.length, setSelection]);
+
     const [isFocused, setIsFocused] = useState<boolean>(false);
 
     const {debouncedCommentMaxLengthValidation, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID});

From f450f44b736d3180adae36f2c247d4a29513996b Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 20:45:49 +0000
Subject: [PATCH 022/233] fix: missing React import

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 7664d18d891b..eb030b33398f 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,4 @@
-import {createContext, useContext, useEffect, useState} from 'react';
+import React, {createContext, useContext, useEffect, useState} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
 import ONYXKEYS from '@src/ONYXKEYS';

From c363712edb934cbb9f0ac247bcdaefa6921d5ad1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 21:42:30 +0000
Subject: [PATCH 023/233] fix: message editing

---
 .../ComposerWithSuggestions.tsx               | 32 ++++---------------
 .../ReportActionCompose.tsx                   |  9 ++----
 .../report/ReportActionEditMessageContext.tsx | 30 ++++++++---------
 3 files changed, 22 insertions(+), 49 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index f3eec1407537..4c5bfa4e1d13 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -162,9 +162,6 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
         /** Whether the main composer was hidden */
         didHideComposerInput?: boolean;
 
-        /** Whether the composer is editing in composer */
-        isEditingInComposer: boolean;
-
         /** Reference to the outer element */
         ref?: Ref<ComposerWithSuggestionsRef | null>;
     };
@@ -211,7 +208,6 @@ function ComposerWithSuggestions({
     lastReportAction,
     isGroupPolicyReport,
     policyID,
-    isEditingInComposer,
 
     // Focus
     onFocus,
@@ -274,42 +270,26 @@ function ComposerWithSuggestions({
         }
         return initialValue;
     });
+
     const [selection, setSelection] = useState<TextSelection>(() => currentEditMessageSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
 
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
 
     const commentRef = useRef(value);
 
-    // Focus the composer when editing in composer
-    useEffect(() => {
-        if (!isEditingInComposer) {
-            return;
-        }
-
-        composerRef.current?.focus();
-    }, [currentEditMessageSelection, isEditingInComposer, value.length]);
-
-    // Reset the composer value when the app extends to wide layout,
-    // because the inline composer is showing up
-    useEffect(() => {
-        if (!editingReportActionID || shouldUseNarrowLayout) {
-            return;
-        }
-
-        setValue('');
-    }, [editingReportActionID, shouldUseNarrowLayout]);
-
     useEffect(() => {
         if (!shouldUseNarrowLayout || !editingReportActionID) {
+            setValue('');
             return;
         }
 
         const nextValue = editingMessage ?? draftComment ?? '';
-
+        commentRef.current = nextValue;
         emojisPresentBefore.current = extractEmojis(nextValue);
+
         setValue(nextValue);
-        commentRef.current = nextValue;
-    }, [editingMessage, draftComment, shouldUseNarrowLayout, editingReportActionID]);
+        composerRef.current?.focus();
+    }, [draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 135cc534f968..916a14ea600b 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -165,7 +165,7 @@ function ReportActionCompose({
         canBeMissing: true,
     });
 
-    const {editingReportActionID, editingReportAction, editingMessage, setActiveEdit} = useReportActionActiveEdit();
+    const {editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID;
 
@@ -352,8 +352,7 @@ function ReportActionCompose({
 
     const deleteDraftMessage = useCallback(() => {
         deleteDraft();
-        setActiveEdit(null);
-    }, [deleteDraft, setActiveEdit]);
+    }, [deleteDraft]);
 
     /**
      * Add or edit a comment in the composer
@@ -365,7 +364,6 @@ function ReportActionCompose({
             if (isEditingInComposer && !attachmentFileRef.current) {
                 publishDraft(draftMessageTrimmed);
                 deleteDraft();
-                setActiveEdit(null);
                 return;
             }
 
@@ -408,7 +406,6 @@ function ReportActionCompose({
             isConciergeChat,
             publishDraft,
             deleteDraft,
-            setActiveEdit,
             kickoffWaitingIndicator,
             transactionThreadReport,
             report,
@@ -639,9 +636,9 @@ function ReportActionCompose({
                             onBlur={onBlur}
                             measureParentContainer={measureContainer}
                             onValueChange={onValueChange}
-                            isEditingInComposer={isEditingInComposer}
                             didHideComposerInput={didHideComposerInput}
                             forwardedFSClass={fsClass}
+                            key={editingReportActionID}
                         />
                         {shouldDisplayDualDropZone && (
                             <DualDropZone
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index eb030b33398f..4a62d2eba269 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,4 @@
-import React, {createContext, useContext, useEffect, useState} from 'react';
+import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -13,8 +13,6 @@ type ReportActionActiveEdit = {
 };
 
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
-    setActiveEdit: (activeEdit: ReportActionActiveEdit | null) => void;
-
     currentEditMessageSelection: TextSelection | null;
     setCurrentEditMessageSelection: (selection: TextSelection) => void;
 };
@@ -23,8 +21,6 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingReportActionID: null,
     editingReportAction: null,
     editingMessage: null,
-    setActiveEdit: NOOP,
-
     currentEditMessageSelection: null,
     setCurrentEditMessageSelection: NOOP,
 });
@@ -52,24 +48,24 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         setEditingMessage(activeEdit?.editingMessage ?? null);
     }
 
-    function setCurrentEditMessageSelection(selection: TextSelection) {
-        if (!editingReportActionID) {
-            return;
-        }
+    const setCurrentEditMessageSelection = useCallback(
+        (selection: TextSelection | null) => {
+            if (!editingReportActionID) {
+                return;
+            }
 
-        setCurrentEditMessageSelectionState(selection);
-    }
+            setCurrentEditMessageSelectionState(selection);
+        },
+        [editingReportActionID],
+    );
 
     // Set the active edit when the report actions or draft comments change
     useEffect(() => {
-        if (editingReportActionID) {
-            return;
-        }
-
         const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
         if (!reportDrafts) {
             setActiveEdit(null);
+            setCurrentEditMessageSelection(null);
             return;
         }
 
@@ -77,6 +73,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
         if (!reportDraftEntry) {
             setActiveEdit(null);
+            setCurrentEditMessageSelection(null);
             return;
         }
 
@@ -87,7 +84,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
             editingReportAction: reportActions?.[reportActionID] ?? null,
             editingMessage: draft.message,
         });
-    }, [editingReportActionID, reportActionDrafts, reportActions, reportID]);
+    }, [editingReportActionID, reportActionDrafts, reportActions, reportID, setCurrentEditMessageSelection]);
 
     return (
         <ReportActionEditMessageContext.Provider
@@ -96,7 +93,6 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
                 editingReportActionID,
                 editingReportAction,
                 editingMessage,
-                setActiveEdit,
                 currentEditMessageSelection,
                 setCurrentEditMessageSelection,
             }}

From 3672eedbf966105e041330ce85844ec97d597c24 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 22:10:50 +0000
Subject: [PATCH 024/233] fix: draft not unset

---
 .../ComposerWithSuggestions.tsx               |  5 ++-
 .../ReportActionCompose.tsx                   |  6 +--
 .../ReportActionCompose/useEditMessage.ts     |  5 +++
 .../report/ReportActionEditMessageContext.tsx | 45 ++++++++++++++-----
 4 files changed, 42 insertions(+), 19 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 4c5bfa4e1d13..000b023baf06 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -260,7 +260,7 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingReportActionID, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection} = useReportActionActiveEdit();
+    const {editingReportActionID, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, didSubmitEditRef} = useReportActionActiveEdit();
 
     const [value, setValue] = useState(() => {
         const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
@@ -496,7 +496,7 @@ function ComposerWithSuggestions({
 
             commentRef.current = newCommentConverted;
             if (shouldUseNarrowLayout) {
-                if (editingReportActionID) {
+                if (editingReportActionID && !didSubmitEditRef.current) {
                     saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
                 }
 
@@ -526,6 +526,7 @@ function ComposerWithSuggestions({
             setCurrentEditMessageSelection,
             currentEditMessageSelection,
             editingReportActionID,
+            didSubmitEditRef,
             reportID,
             currentUserAccountID,
             debouncedSaveReportComment,
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 916a14ea600b..27ee73d8b915 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -350,10 +350,6 @@ function ReportActionCompose({
         composerRef,
     });
 
-    const deleteDraftMessage = useCallback(() => {
-        deleteDraft();
-    }, [deleteDraft]);
-
     /**
      * Add or edit a comment in the composer
      */
@@ -579,7 +575,7 @@ function ReportActionCompose({
                     >
                         {PDFValidationComponent}
                         {isEditingInComposer ? (
-                            <MessageEditCancelButton onCancel={deleteDraftMessage} />
+                            <MessageEditCancelButton onCancel={deleteDraft} />
                         ) : (
                             <AttachmentPickerWithMenuItems
                                 onAttachmentPicked={(files) => validateAttachments({files})}
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 62847bfc78c2..71ca082949df 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -16,6 +16,7 @@ import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import {getOriginalReportID} from '@libs/ReportUtils';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
+import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 import KeyboardUtils from '@src/utils/keyboard';
@@ -45,6 +46,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
     const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
     const ancestors = useAncestors(originalReport);
 
+    const {didSubmitEditRef} = useReportActionActiveEdit();
+
     useEffect(() => {
         // required for keeping last state of isFocused variable
         isFocusedRef.current = isFocused;
@@ -103,6 +106,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, reportAction, true, deleteDraft, () => focusEditAfterCancelDelete(composerRef.current));
             return;
         }
+
+        didSubmitEditRef.current = true;
         editReportComment(
             originalReport,
             reportAction,
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 4a62d2eba269..4d8cc6300928 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,4 @@
-import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
+import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -15,6 +15,7 @@ type ReportActionActiveEdit = {
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
     currentEditMessageSelection: TextSelection | null;
     setCurrentEditMessageSelection: (selection: TextSelection) => void;
+    didSubmitEditRef: React.RefObject<boolean>;
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
@@ -23,6 +24,7 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingMessage: null,
     currentEditMessageSelection: null,
     setCurrentEditMessageSelection: NOOP,
+    didSubmitEditRef: {current: false},
 });
 
 type ReportActionEditMessageContextProviderProps = {
@@ -41,12 +43,26 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
     const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
+    const didSubmitEditRef = useRef<boolean>(false);
 
-    function setActiveEdit(activeEdit: ReportActionActiveEdit | null) {
-        setEditingReportActionID(activeEdit?.editingReportActionID ?? null);
-        setEditingReportAction(activeEdit?.editingReportAction ?? null);
-        setEditingMessage(activeEdit?.editingMessage ?? null);
-    }
+    const updateActiveEditState = useCallback(
+        (activeEdit: ReportActionActiveEdit | null) => {
+            const newEditingReportActionID = activeEdit?.editingReportActionID ?? null;
+            const newEditingReportAction = activeEdit?.editingReportAction ?? null;
+            const newEditingMessage = activeEdit?.editingMessage ?? null;
+
+            if (newEditingReportActionID !== editingReportActionID) {
+                setEditingReportActionID(newEditingReportActionID);
+            }
+            if (newEditingReportAction !== editingReportAction) {
+                setEditingReportAction(newEditingReportAction);
+            }
+            if (newEditingMessage !== editingMessage) {
+                setEditingMessage(newEditingMessage);
+            }
+        },
+        [editingReportActionID, editingReportAction, editingMessage],
+    );
 
     const setCurrentEditMessageSelection = useCallback(
         (selection: TextSelection | null) => {
@@ -59,32 +75,36 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         [editingReportActionID],
     );
 
+    const reset = useCallback(() => {
+        didSubmitEditRef.current = false;
+        updateActiveEditState(null);
+        setCurrentEditMessageSelection(null);
+    }, [updateActiveEditState, setCurrentEditMessageSelection]);
+
     // Set the active edit when the report actions or draft comments change
     useEffect(() => {
         const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
         if (!reportDrafts) {
-            setActiveEdit(null);
-            setCurrentEditMessageSelection(null);
+            reset();
             return;
         }
 
         const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
 
         if (!reportDraftEntry) {
-            setActiveEdit(null);
-            setCurrentEditMessageSelection(null);
+            reset();
             return;
         }
 
         const [reportActionID, draft] = reportDraftEntry;
 
-        setActiveEdit({
+        updateActiveEditState({
             editingReportActionID: reportActionID,
             editingReportAction: reportActions?.[reportActionID] ?? null,
             editingMessage: draft.message,
         });
-    }, [editingReportActionID, reportActionDrafts, reportActions, reportID, setCurrentEditMessageSelection]);
+    }, [reportActionDrafts, reportActions, reportID, reset, updateActiveEditState]);
 
     return (
         <ReportActionEditMessageContext.Provider
@@ -95,6 +115,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
                 editingMessage,
                 currentEditMessageSelection,
                 setCurrentEditMessageSelection,
+                didSubmitEditRef,
             }}
         >
             {children}

From c48635a23b856535f61d308da5962a07d84f0df1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 22:16:09 +0000
Subject: [PATCH 025/233] fix: pass `originalReportID`

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx   | 3 +++
 src/pages/inbox/report/ReportActionCompose/useEditMessage.ts   | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 27ee73d8b915..6667a751b4b3 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -24,6 +24,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import useOnyx from '@hooks/useOnyx';
+import useOriginalReportID from '@hooks/useOriginalReportID';
 import usePreferredPolicy from '@hooks/usePreferredPolicy';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -341,8 +342,10 @@ function ReportActionCompose({
 
     const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({reportID});
 
+    const originalReportID = useOriginalReportID(reportID, editingReportAction);
     const {publishDraft, deleteDraft} = useEditMessage({
         reportID,
+        originalReportID,
         reportAction: editingReportAction,
         shouldScrollToLastMessage: isEditingLastReportAction,
         isFocused,
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 71ca082949df..40357b507e8a 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -23,7 +23,7 @@ import KeyboardUtils from '@src/utils/keyboard';
 
 type UseEditMessageProps = {
     reportID: string | undefined;
-    originalReportID?: string;
+    originalReportID: string | undefined;
     reportAction: OnyxTypes.ReportAction | null | undefined;
     shouldScrollToLastMessage?: boolean;
     isFocused: boolean;

From e5207452412341740923dfda34fe83c897084301 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 22:31:20 +0000
Subject: [PATCH 026/233] fix: allow passing unset reportAction

---
 src/libs/actions/Report/index.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index 0a260d54314f..114fb675b41e 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2395,7 +2395,11 @@ function deleteReportActionDraft(reportID: string | undefined, reportAction: Rep
 }
 
 /** Saves the draft for a comment report action. This will put the comment into "edit mode" */
-function saveReportActionDraft(reportID: string | undefined, reportAction: ReportAction, draftMessage: string) {
+function saveReportActionDraft(reportID: string | undefined, reportAction: ReportAction | null, draftMessage: string) {
+    if (!reportAction) {
+        return;
+    }
+
     const originalReportID = getOriginalReportID(reportID, reportAction, undefined);
     Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`, {[reportAction.reportActionID]: {message: draftMessage}});
 }

From 4cbc1ba46fbc71097b9b2875687d72e8dd97d40c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 22:31:34 +0000
Subject: [PATCH 027/233] fix: allow passing empty draft to delete message

---
 .../inbox/report/ReportActionCompose/useEditMessage.ts   | 3 ++-
 .../inbox/report/ReportActionEditMessageContext.tsx      | 9 +++++++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 40357b507e8a..53fad0d46fdf 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -67,6 +67,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
+        didSubmitEditRef.current = true;
+
         deleteReportActionDraft(reportID, reportAction);
 
         if (isActive()) {
@@ -107,7 +109,6 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        didSubmitEditRef.current = true;
         editReportComment(
             originalReport,
             reportAction,
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 4d8cc6300928..9b961f547949 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -43,7 +43,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
     const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
-    const didSubmitEditRef = useRef<boolean>(false);
+    const didSubmitEditRef = useRef<boolean | null>(null);
 
     const updateActiveEditState = useCallback(
         (activeEdit: ReportActionActiveEdit | null) => {
@@ -76,7 +76,11 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     );
 
     const reset = useCallback(() => {
-        didSubmitEditRef.current = false;
+        if (didSubmitEditRef.current === false) {
+            return;
+        }
+
+        didSubmitEditRef.current = null;
         updateActiveEditState(null);
         setCurrentEditMessageSelection(null);
     }, [updateActiveEditState, setCurrentEditMessageSelection]);
@@ -99,6 +103,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
         const [reportActionID, draft] = reportDraftEntry;
 
+        didSubmitEditRef.current = false;
         updateActiveEditState({
             editingReportActionID: reportActionID,
             editingReportAction: reportActions?.[reportActionID] ?? null,

From e7f098a9f6f94c0ae7466afb596e0d9461a27f51 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 22:31:44 +0000
Subject: [PATCH 028/233] fix: add debounce to draft message save

---
 .../ComposerWithSuggestions.tsx               | 41 ++++++++++++++++---
 1 file changed, 36 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 000b023baf06..9be11cd0fe54 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -260,7 +260,7 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingReportActionID, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, didSubmitEditRef} = useReportActionActiveEdit();
+    const {editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, didSubmitEditRef} = useReportActionActiveEdit();
 
     const [value, setValue] = useState(() => {
         const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
@@ -318,9 +318,6 @@ function ComposerWithSuggestions({
 
     const syncSelectionWithOnChangeTextRef = useRef<SyncSelection | null>(null);
 
-    // The ref to check whether the comment saving is in progress
-    const isCommentPendingSaved = useRef(false);
-
     // Tracks transition state to prevent SilentCommentUpdater from overwriting the just-saved draft during report ID changes
     const isTransitioningToPreExistingReport = useRef(false);
 
@@ -351,6 +348,34 @@ function ComposerWithSuggestions({
         RNTextInputReset.resetKeyboardInput(CONST.COMPOSER.NATIVE_ID);
     }, []);
 
+    // The ref to check whether the comment saving is in progress
+    const isDraftPendingSaved = useRef(false);
+
+    /**
+     * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
+     * allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
+     * @param {String} newDraft
+     */
+    const debouncedSaveDraft = useMemo(
+        () =>
+            lodashDebounce((newDraft: string) => {
+                saveReportActionDraft(reportID, editingReportAction, newDraft);
+                isDraftPendingSaved.current = false;
+            }, 1000),
+        [reportID, editingReportAction],
+    );
+
+    useEffect(
+        () => () => {
+            debouncedSaveDraft.cancel();
+            isDraftPendingSaved.current = false;
+        },
+        [debouncedSaveDraft],
+    );
+
+    // The ref to check whether the comment saving is in progress
+    const isCommentPendingSaved = useRef(false);
+
     const debouncedSaveReportComment = useMemo(
         () =>
             lodashDebounce((selectedReportID: string, newComment: string | null) => {
@@ -497,7 +522,12 @@ function ComposerWithSuggestions({
             commentRef.current = newCommentConverted;
             if (shouldUseNarrowLayout) {
                 if (editingReportActionID && !didSubmitEditRef.current) {
-                    saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
+                    if (shouldDebounceSaveComment) {
+                        isDraftPendingSaved.current = true;
+                        debouncedSaveDraft(newCommentConverted);
+                    } else {
+                        saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
+                    }
                 }
 
                 if (newCommentConverted) {
@@ -527,6 +557,7 @@ function ComposerWithSuggestions({
             currentEditMessageSelection,
             editingReportActionID,
             didSubmitEditRef,
+            debouncedSaveDraft,
             reportID,
             currentUserAccountID,
             debouncedSaveReportComment,

From cb8d96966cecb0d3c823bcbaf25c34015451d00a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 13 Feb 2026 22:35:38 +0000
Subject: [PATCH 029/233] fix: TS error

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 9b961f547949..0c0845ac8ae8 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -15,7 +15,7 @@ type ReportActionActiveEdit = {
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
     currentEditMessageSelection: TextSelection | null;
     setCurrentEditMessageSelection: (selection: TextSelection) => void;
-    didSubmitEditRef: React.RefObject<boolean>;
+    didSubmitEditRef: React.RefObject<boolean | null>;
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
@@ -24,7 +24,7 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingMessage: null,
     currentEditMessageSelection: null,
     setCurrentEditMessageSelection: NOOP,
-    didSubmitEditRef: {current: false},
+    didSubmitEditRef: {current: null},
 });
 
 type ReportActionEditMessageContextProviderProps = {

From a86daae2ed4585d8e97a4d2f5c6345df16d17058 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 18:33:52 +0000
Subject: [PATCH 030/233] fix: default string in `useOnyx` hook

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 0c0845ac8ae8..de3439a91b3e 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,6 +1,7 @@
 import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
+import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 
@@ -33,7 +34,7 @@ type ReportActionEditMessageContextProviderProps = {
 };
 
 function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
-    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID ?? ''}`, {
+    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
         canBeMissing: true,
     });

From 280368576377d39d44d5ed3a2b819de3b4aefb7c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 18:34:12 +0000
Subject: [PATCH 031/233] fix: remove unused import

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index de3439a91b3e..e11c34a86b9d 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,7 +1,6 @@
 import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
-import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 

From 5b4103c7245299fa351d808824ed8884c7a4a660 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 18:47:11 +0000
Subject: [PATCH 032/233] fix: pass missing `isEditing` flag

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx | 5 ++++-
 .../useDebouncedCommentMaxLengthValidation.ts                | 2 ++
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx       | 2 +-
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index c2b986139de0..c156d05192e3 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -341,7 +341,10 @@ function ReportActionCompose({
         ComposerFocusManager.setReadyToFocus();
     }, [updateShouldShowSuggestionMenuToFalse]);
 
-    const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({reportID});
+    const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({
+        reportID,
+        isEditing: !!editingReportAction,
+    });
 
     const originalReportID = useOriginalReportID(reportID, editingReportAction);
     const {publishDraft, deleteDraft} = useEditMessage({
diff --git a/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts b/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
index e83d9a678f82..15baf8e62669 100644
--- a/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
@@ -15,10 +15,12 @@ function useDebouncedCommentMaxLengthValidation({reportID, isEditing = false}: U
     /**
      * Updates the composer when the comment length is exceeded
      * Shows red borders and prevents the comment from being sent
+     * When editing, we only validate comment length; task title rules do not apply.
      */
     function validateMaxLength(value: string) {
         const taskCommentMatch = value?.match(CONST.REGEX.TASK_TITLE_WITH_OPTIONAL_SHORT_MENTION);
 
+        // Only apply task-title validation when composing (not when editing an existing message)
         if (!isEditing && taskCommentMatch) {
             const title = taskCommentMatch?.[3] ? taskCommentMatch[3].trim().replaceAll('\n', ' ') : '';
             const exceeded = title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false;
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index d03bce1aa96e..5ebe6025656f 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -140,7 +140,7 @@ function ReportActionItemMessageEdit({
 
     const [isFocused, setIsFocused] = useState<boolean>(false);
 
-    const {debouncedCommentMaxLengthValidation, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID});
+    const {debouncedCommentMaxLengthValidation, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID, isEditing: true});
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 

From 05b972d0340a5fd9cc10ff244a33d16a754b17c1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 19:23:11 +0000
Subject: [PATCH 033/233] test: extract logic into test utils

---
 tests/ui/ReportActionComposeTest.tsx         | 32 +------
 tests/ui/ReportActionItemMessageEditTest.tsx | 33 +-------
 tests/utils/ReportActionComposeUtils.tsx     | 87 ++++++++++++++++++++
 3 files changed, 92 insertions(+), 60 deletions(-)
 create mode 100644 tests/utils/ReportActionComposeUtils.tsx

diff --git a/tests/ui/ReportActionComposeTest.tsx b/tests/ui/ReportActionComposeTest.tsx
index a9eecd42f649..27c6b26db3bf 100644
--- a/tests/ui/ReportActionComposeTest.tsx
+++ b/tests/ui/ReportActionComposeTest.tsx
@@ -1,16 +1,11 @@
 import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
-import React from 'react';
+import {act, fireEvent, screen, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
-import ComposeProviders from '@components/ComposeProviders';
-import {LocaleContextProvider} from '@components/LocaleContextProvider';
-import OnyxListItemProvider from '@components/OnyxListItemProvider';
 import {forceClearInput} from '@libs/ComponentUtils';
-import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import ReportActionCompose, {onSubmitAction} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import {onSubmitAction} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import * as LHNTestUtils from '../utils/LHNTestUtils';
+import {renderReportActionCompose} from '../utils/ReportActionComposeUtils';
 import * as TestHelper from '../utils/TestHelper';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
 
@@ -41,27 +36,6 @@ jest.mock('@react-navigation/native', () => ({
 
 TestHelper.setupGlobalFetchMock();
 
-const defaultReport = LHNTestUtils.getFakeReport();
-const defaultProps: ReportActionComposeProps = {
-    onSubmit: jest.fn(),
-    isComposerFullSize: false,
-    reportID: defaultReport.reportID,
-    report: defaultReport,
-};
-
-const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
-    return render(
-        <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
-            <ReportActionCompose
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-            />
-        </ComposeProviders>,
-    );
-};
-
 // Helper function to simulate text selection
 const simulateSelection = (composer: ReturnType<typeof screen.getByTestId>, start: number, end: number) => {
     fireEvent(composer, 'selectionChange', {
diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index 95a9c42d2ded..84212a6afec6 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -1,16 +1,10 @@
 import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, render, screen} from '@testing-library/react-native';
-import React from 'react';
+import {act, fireEvent, screen} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
-import ComposeProviders from '@components/ComposeProviders';
-import {LocaleContextProvider} from '@components/LocaleContextProvider';
-import OnyxListItemProvider from '@components/OnyxListItemProvider';
 import {editReportComment} from '@libs/actions/Report';
-import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
-import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import * as LHNTestUtils from '../utils/LHNTestUtils';
+import {renderReportActionItemMessageEdit} from '../utils/ReportActionComposeUtils';
 import * as TestHelper from '../utils/TestHelper';
 
 const mockEditReportComment = jest.mocked(editReportComment);
@@ -46,29 +40,6 @@ jest.mock('@react-navigation/native', () => ({
 
 TestHelper.setupGlobalFetchMock();
 
-const defaultReport = LHNTestUtils.getFakeReport();
-const defaultProps: ReportActionItemMessageEditProps = {
-    action: LHNTestUtils.getFakeReportAction(),
-    draftMessage: '',
-    reportID: defaultReport.reportID,
-    originalReportID: defaultReport.reportID,
-    index: 0,
-    isGroupPolicyReport: false,
-};
-
-const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
-    return render(
-        <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
-            <ReportActionItemMessageEdit
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-            />
-        </ComposeProviders>,
-    );
-};
-
 describe('ReportActionCompose Integration Tests', () => {
     beforeAll(() => {
         Onyx.init({
diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
new file mode 100644
index 000000000000..d8ef98e2a04c
--- /dev/null
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -0,0 +1,87 @@
+import {render} from '@testing-library/react-native';
+import type {PropsWithChildren} from 'react';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
+import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
+import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
+import * as LHNTestUtils from './LHNTestUtils';
+
+const defaultReport = LHNTestUtils.getFakeReport();
+
+function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
+    return <ReportActionEditMessageContextProvider reportID={defaultReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
+}
+
+function ReportScreenProviders({children}: PropsWithChildren) {
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+}
+
+const defaultReportActionComposeProps: ReportActionComposeProps = {
+    onSubmit: jest.fn(),
+    isComposerFullSize: false,
+    reportID: defaultReport.reportID,
+    report: defaultReport,
+};
+
+const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
+    return render(
+        <ReportScreenProviders>
+            <ReportActionCompose
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultReportActionComposeProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...props}
+            />
+        </ReportScreenProviders>,
+    );
+};
+
+const defaultReportActionItemMessageEditProps: ReportActionItemMessageEditProps = {
+    action: LHNTestUtils.getFakeReportAction(),
+    draftMessage: '',
+    reportID: defaultReport.reportID,
+    originalReportID: defaultReport.reportID,
+    index: 0,
+    isGroupPolicyReport: false,
+};
+
+const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
+    return render(
+        <ReportScreenProviders>
+            <ReportActionItemMessageEdit
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultReportActionItemMessageEditProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...props}
+            />
+        </ReportScreenProviders>,
+    );
+};
+
+const renderReportActionMessageEditComponents = (
+    reportActionComposeProps?: Partial<ReportActionComposeProps>,
+    reportActionItemMessageEditProps?: Partial<ReportActionItemMessageEditProps>,
+) => {
+    return render(
+        <ReportScreenProviders>
+            <ReportActionCompose
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultReportActionComposeProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...reportActionComposeProps}
+            />
+            <ReportActionItemMessageEdit
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultReportActionItemMessageEditProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...reportActionItemMessageEditProps}
+            />
+        </ReportScreenProviders>,
+    );
+};
+
+export {renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents};

From ce4fecc9b2488ac9da9a623c8ef2cf891958ca49 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 19:23:27 +0000
Subject: [PATCH 034/233] fix: pass `maxCommentLength` prop

---
 src/components/ExceededCommentLength.tsx               | 2 +-
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/ExceededCommentLength.tsx b/src/components/ExceededCommentLength.tsx
index 8996a69c0ba8..69bf7c96d8f4 100644
--- a/src/components/ExceededCommentLength.tsx
+++ b/src/components/ExceededCommentLength.tsx
@@ -9,7 +9,7 @@ type ExceededCommentLengthProps = {
     isTaskTitle?: boolean;
 };
 
-function ExceededCommentLength({maxCommentLength = CONST.MAX_COMMENT_LENGTH, isTaskTitle}: ExceededCommentLengthProps) {
+function ExceededCommentLength({maxCommentLength = CONST.MAX_COMMENT_LENGTH, isTaskTitle = false}: ExceededCommentLengthProps) {
     const styles = useThemeStyles();
     const {numberFormat, translate} = useLocalize();
 
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 5ebe6025656f..e317c21c60cb 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -140,7 +140,7 @@ function ReportActionItemMessageEdit({
 
     const [isFocused, setIsFocused] = useState<boolean>(false);
 
-    const {debouncedCommentMaxLengthValidation, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID, isEditing: true});
+    const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID, isEditing: true});
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
@@ -539,7 +539,7 @@ function ReportActionItemMessageEdit({
                     />
                 </View>
             </View>
-            {isExceedingMaxLength && <ExceededCommentLength />}
+            {isExceedingMaxLength && !!exceededMaxLength && <ExceededCommentLength maxCommentLength={exceededMaxLength} />}
         </>
     );
 }

From ba5f1c06f14e671ff46d8cb77ea8b9eb22e4aa31 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 19:35:07 +0000
Subject: [PATCH 035/233] remove unused code

---
 .../report/ReportActionCompose/SendButton.tsx     | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
index e8a3a48ce4a6..6cf4607b6de3 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
@@ -79,21 +79,6 @@ function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: Sen
                         </PressableWithFeedback>
                     </Tooltip>
                 </View>
-
-                {/* <View style={styles.alignSelfEnd}>
-                            <PressableWithFeedback
-                                onPress={publishDraft}
-                                hoverDimmingValue={1}
-                                pressDimmingValue={0.2}
-                                // Keep focus on the composer when send button is clicked.
-                                onMouseDown={(e) => e.preventDefault()}
-                            >
-                                <Icon
-                                    src={icons.Checkmark}
-                                    fill={hasExceededMaxCommentLength ? theme.icon : theme.textLight}
-                                />
-                            </PressableWithFeedback>
-                    </View> */}
             </GestureDetector>
         </View>
     );

From 8c7e1d388083349bd589e528b40f8c0876301009 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 21:10:23 +0000
Subject: [PATCH 036/233] fix: use checkmark icon for editing in send button

---
 src/pages/inbox/report/ReportActionCompose/SendButton.tsx | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
index 6cf4607b6de3..43546461ca90 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
@@ -10,6 +10,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 
 type SendButtonProps = {
     /** Whether the button is disabled */
@@ -26,6 +27,8 @@ function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: Sen
     const theme = useTheme();
     const styles = useThemeStyles();
     const {translate} = useLocalize();
+    const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
+
     // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to manage GestureDetector correctly
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
     const {isSmallScreenWidth} = useResponsiveLayout();
@@ -33,7 +36,8 @@ function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: Sen
         .onEnd(() => {
             onSend();
         })
-        .runOnJS(true);
+        .runOnJS(true)
+        .withTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
 
     const label = isEditing ? translate('common.saveChanges') : translate('common.send');
     const sentryLabel = isEditing ? CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON : CONST.SENTRY_LABEL.REPORT.SEND_BUTTON;
@@ -72,7 +76,7 @@ function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: Sen
                         >
                             {({pressed}) => (
                                 <Icon
-                                    src={Expensicons.Send}
+                                    src={isEditing ? icons.Checkmark : Expensicons.Send}
                                     fill={isDisabledProp || pressed ? theme.icon : theme.textLight}
                                 />
                             )}

From a9c5406a767c38f8921c9818f4ee0fdbc5d3ba28 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 21:52:18 +0000
Subject: [PATCH 037/233] fix: tests with new `SendButton`

---
 tests/ui/ReportActionItemMessageEditTest.tsx | 17 ++++++++++-------
 tests/utils/ReportActionComposeUtils.tsx     | 16 +++++++++++++++-
 2 files changed, 25 insertions(+), 8 deletions(-)

diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index 84212a6afec6..204755c12d34 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -4,7 +4,8 @@ import Onyx from 'react-native-onyx';
 import {editReportComment} from '@libs/actions/Report';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import {renderReportActionItemMessageEdit} from '../utils/ReportActionComposeUtils';
+import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
+import {pressReportActionComposeSendButton, renderReportActionItemMessageEdit} from '../utils/ReportActionComposeUtils';
 import * as TestHelper from '../utils/TestHelper';
 
 const mockEditReportComment = jest.mocked(editReportComment);
@@ -58,15 +59,16 @@ describe('ReportActionCompose Integration Tests', () => {
     describe('Message validation', () => {
         it('should edit when length is within the limit', async () => {
             renderReportActionItemMessageEdit();
-            const composer = screen.getByTestId('composer');
-            const saveChangesButton = screen.getByLabelText('common.saveChanges');
+            const composer = screen.getByTestId(CONST.COMPOSER.NATIVE_ID);
 
             // Given a message that is within the length limit
             const validMessage = 'x'.repeat(CONST.MAX_COMMENT_LENGTH);
             fireEvent.changeText(composer, validMessage);
 
+            await waitForBatchedUpdatesWithAct();
+
             // When the message is saved
-            fireEvent.press(saveChangesButton);
+            pressReportActionComposeSendButton();
 
             // Then the message should be edited
             expect(mockEditReportComment).toHaveBeenCalledTimes(1);
@@ -74,15 +76,16 @@ describe('ReportActionCompose Integration Tests', () => {
 
         it('should not edit when length exceeds the limit', async () => {
             renderReportActionItemMessageEdit();
-            const composer = screen.getByTestId('composer');
-            const saveChangesButton = screen.getByLabelText('common.saveChanges');
+            const composer = screen.getByTestId(CONST.COMPOSER.NATIVE_ID);
 
             // Given a message that is over the length limit
             const invalidMessage = 'x'.repeat(CONST.MAX_COMMENT_LENGTH + 1);
             fireEvent.changeText(composer, invalidMessage);
 
+            await waitForBatchedUpdatesWithAct();
+
             // When the message is saved
-            fireEvent.press(saveChangesButton);
+            pressReportActionComposeSendButton();
 
             // Then the message should NOT be edited
             expect(mockEditReportComment).toHaveBeenCalledTimes(0);
diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index d8ef98e2a04c..b0cb8fe24be3 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -8,6 +8,9 @@ import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportA
 import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
 import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
 import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
+import {fireGestureHandler, getByGestureTestId} from 'react-native-gesture-handler/jest-utils';
+import CONST from '@src/CONST';
+import {State} from 'react-native-gesture-handler';
 import * as LHNTestUtils from './LHNTestUtils';
 
 const defaultReport = LHNTestUtils.getFakeReport();
@@ -84,4 +87,15 @@ const renderReportActionMessageEditComponents = (
     );
 };
 
-export {renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents};
+function pressReportActionComposeSendButton() {
+    const gesture = getByGestureTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
+
+    fireGestureHandler(gesture, [
+        {oldState: State.UNDETERMINED, state: State.BEGAN},
+        {oldState: State.BEGAN, state: State.ACTIVE},
+        {oldState: State.ACTIVE, state: State.ACTIVE},
+        {oldState: State.ACTIVE, state: State.END},
+    ]);
+}
+
+export {renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents, pressReportActionComposeSendButton};

From 682ea8ab7c49f3e56bcb95c9050a1b63a38e4561 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 21:53:22 +0000
Subject: [PATCH 038/233] fix: extract COMPOSER test id

---
 .../ComposerWithSuggestions/ComposerWithSuggestions.tsx         | 2 +-
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx          | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 9be11cd0fe54..a448d9d7a0af 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -1074,7 +1074,7 @@ function ComposerWithSuggestions({
                     isComposerFullSize={isComposerFullSize}
                     onContentSizeChange={handleContentSizeChange}
                     value={value}
-                    testID="composer"
+                    testID={CONST.COMPOSER.NATIVE_ID}
                     shouldCalculateCaretPosition
                     onLayout={onLayout}
                     onScroll={hideSuggestionMenu}
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index e317c21c60cb..6882e82ed695 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -500,7 +500,7 @@ function ReportActionItemMessageEdit({
                             isGroupPolicyReport={isGroupPolicyReport}
                             shouldCalculateCaretPosition
                             onScroll={onSaveScrollAndHideSuggestionMenu}
-                            testID="composer"
+                            testID={CONST.COMPOSER.NATIVE_ID}
                         />
                     </View>
 

From 106bc0e6c13d12fea8af689b2f8c9ea347ea653e Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 21:53:33 +0000
Subject: [PATCH 039/233] refactor: SendButton props

---
 .../inbox/report/ReportActionCompose/SendButton.tsx    | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
index 43546461ca90..ad726e3b5a18 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
@@ -23,7 +23,7 @@ type SendButtonProps = {
     onSend: () => void;
 };
 
-function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: SendButtonProps) {
+function SendButton({isDisabled: isDisabledProp = false, isEditing = false, onSend}: SendButtonProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
     const {translate} = useLocalize();
@@ -33,13 +33,14 @@ function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: Sen
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
     const {isSmallScreenWidth} = useResponsiveLayout();
     const Tap = Gesture.Tap()
+        .enabled(!isDisabledProp)
         .onEnd(() => {
             onSend();
         })
         .runOnJS(true)
         .withTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
 
-    const label = isEditing ? translate('common.saveChanges') : translate('common.send');
+    const label = translate(isEditing ? 'common.saveChanges' : 'common.send');
     const sentryLabel = isEditing ? CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON : CONST.SENTRY_LABEL.REPORT.SEND_BUTTON;
 
     return (
@@ -65,14 +66,15 @@ function SendButton({isDisabled: isDisabledProp, onSend, isEditing = false}: Sen
                         <PressableWithFeedback
                             style={({pressed, isDisabled}) => [
                                 styles.chatItemSubmitButton,
-                                isDisabledProp || pressed || isDisabled ? undefined : styles.buttonSuccess,
-                                isDisabledProp ? styles.cursorDisabled : undefined,
+                                pressed || isDisabled ? undefined : styles.buttonSuccess,
+                                isDisabled ? styles.cursorDisabled : undefined,
                             ]}
                             // Since the parent View has accessible, we need to set accessible to false here to avoid duplicate accessibility elements.
                             // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.
                             accessible={false}
                             focusable={false}
                             sentryLabel={sentryLabel}
+                            disabled={isDisabledProp}
                         >
                             {({pressed}) => (
                                 <Icon

From 42c9dc69d91000939ba8d854bad50b232155c042 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 21:53:48 +0000
Subject: [PATCH 040/233] fix: ignore react-hooks rule specifically

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index e11c34a86b9d..9d6c07c67656 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -90,6 +90,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
         if (!reportDrafts) {
+            // eslint-disable-next-line react-hooks/set-state-in-effect
             reset();
             return;
         }

From e6451cc89bb57817cb3b17a4bb867018cb6a2e7f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 21:53:50 +0000
Subject: [PATCH 041/233] Update index.ts

---
 src/CONST/index.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index da294e4abb72..a78e2ef74f15 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1671,6 +1671,7 @@ const CONST = {
         MAX_LINES_FULL: -1,
         // The minimum height needed to enable the full screen composer
         FULL_COMPOSER_MIN_HEIGHT: 60,
+        SEND_BUTTON_TEST_ID: 'send-button',
     },
     MODAL: {
         MODAL_TYPE: {

From c588aac1dca0ace4ff2b6b74e7160aedeeab79b0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 22:52:35 +0000
Subject: [PATCH 042/233] fix: simplify `useImperativeHandle` for
 `ComposerWithSuggestions`

---
 src/components/Composer/types.ts              |  4 +--
 .../ComposerWithSuggestions.tsx               | 33 +++++--------------
 2 files changed, 11 insertions(+), 26 deletions(-)

diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts
index 7fc8708eb9aa..1cdd50f4db61 100644
--- a/src/components/Composer/types.ts
+++ b/src/components/Composer/types.ts
@@ -1,6 +1,6 @@
+import type {MarkdownTextInput} from '@expensify/react-native-live-markdown';
 import type {Ref} from 'react';
 import type {StyleProp, TextInputProps, TextInputSelectionChangeEvent, TextStyle} from 'react-native';
-import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
 import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
 import type {FileObject} from '@src/types/utils/Attachment';
 
@@ -15,7 +15,7 @@ type CustomSelectionChangeEvent = TextInputSelectionChangeEvent & {
     positionY?: number;
 };
 
-type ComposerRef = AnimatedMarkdownTextInputRef & HTMLInputElement & HTMLTextAreaElement;
+type ComposerRef = MarkdownTextInput & HTMLInputElement & HTMLTextAreaElement;
 
 type ComposerProps = Omit<TextInputProps, 'onClear'> &
     ForwardedFSClassProps & {
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index a448d9d7a0af..d763e7be1e32 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -902,30 +902,15 @@ function ComposerWithSuggestions({
     useImperativeHandle(
         ref,
         () =>
-            new Proxy(
-                {},
-                {
-                    get: (_target, prop) => {
-                        if (prop === 'focus') {
-                            return focus;
-                        }
-                        if (prop === 'replaceSelectionWithText') {
-                            return replaceSelectionWithText;
-                        }
-                        if (prop === 'getCurrentText') {
-                            return getCurrentText;
-                        }
-                        if (prop === 'clearWorklet') {
-                            return clearWorklet;
-                        }
-                        if (prop === 'resetHeight') {
-                            return resetHeight;
-                        }
-
-                        return composerRef.current?.[prop as keyof ComposerRef];
-                    },
-                },
-            ) as ComposerWithSuggestionsRef,
+            ({
+                ...composerRef.current,
+                focus,
+                replaceSelectionWithText,
+                getCurrentText,
+                clearWorklet,
+                resetHeight,
+            }) as unknown as ComposerWithSuggestionsRef,
+        [focus, replaceSelectionWithText, clearWorklet, resetHeight, getCurrentText],
     );
 
     useEffect(() => {

From 7b87df45f8730cc02c093942762253871900a90a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 22:53:08 +0000
Subject: [PATCH 043/233] feat: also add `DraftMessageVideoAttributeCache` for
 `ReportActionCompose`

---
 src/libs/DraftMessageVideoAttributeCache.ts   |  4 --
 .../ComposerWithSuggestions.tsx               | 15 ++++--
 .../ReportActionCompose/useEditMessage.ts     |  2 +-
 .../useDraftMessageVideoAttributeCache.ts     | 53 +++++++++++++++++++
 4 files changed, 66 insertions(+), 8 deletions(-)
 delete mode 100644 src/libs/DraftMessageVideoAttributeCache.ts
 create mode 100644 src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts

diff --git a/src/libs/DraftMessageVideoAttributeCache.ts b/src/libs/DraftMessageVideoAttributeCache.ts
deleted file mode 100644
index 6c50113d7282..000000000000
--- a/src/libs/DraftMessageVideoAttributeCache.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// video source -> video attributes
-const draftMessageVideoAttributeCache = new Map<string, string>();
-
-export default draftMessageVideoAttributeCache;
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index d763e7be1e32..5eef48c4fa51 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -57,6 +57,7 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
+import useDraftMessageVideoAttributeCache from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 
 type SyncSelection = {
     position: number;
@@ -271,6 +272,17 @@ function ComposerWithSuggestions({
         return initialValue;
     });
 
+    // The ref to check whether the comment saving is in progress
+    const isDraftPendingSaved = useRef(false);
+
+    useDraftMessageVideoAttributeCache({
+        draftMessage: value,
+        isEditing: !!editingReportActionID,
+        editingReportAction,
+        updateDraftMessage: setValue,
+        isEditInProgressRef: isDraftPendingSaved,
+    });
+
     const [selection, setSelection] = useState<TextSelection>(() => currentEditMessageSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
 
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
@@ -348,9 +360,6 @@ function ComposerWithSuggestions({
         RNTextInputReset.resetKeyboardInput(CONST.COMPOSER.NATIVE_ID);
     }, []);
 
-    // The ref to check whether the comment saving is in progress
-    const isDraftPendingSaved = useRef(false);
-
     /**
      * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
      * allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 53fad0d46fdf..4b3ea4bc59ca 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -11,7 +11,6 @@ import useReportIsArchived from '@hooks/useReportIsArchived';
 import useReportScrollManager from '@hooks/useReportScrollManager';
 import {isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
 import {deleteReportActionDraft, editReportComment} from '@libs/actions/Report';
-import draftMessageVideoAttributeCache from '@libs/DraftMessageVideoAttributeCache';
 import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import {getOriginalReportID} from '@libs/ReportUtils';
@@ -20,6 +19,7 @@ import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMes
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 import KeyboardUtils from '@src/utils/keyboard';
+import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 
 type UseEditMessageProps = {
     reportID: string | undefined;
diff --git a/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts b/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts
new file mode 100644
index 000000000000..95c19ee15530
--- /dev/null
+++ b/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts
@@ -0,0 +1,53 @@
+import Parser from '@libs/Parser';
+import {getReportActionHtml, isDeletedAction} from '@libs/ReportActionsUtils';
+import type React from 'react';
+import {useEffect} from 'react';
+import type * as OnyxTypes from '@src/types/onyx';
+import usePrevious from '@hooks/usePrevious';
+
+type DraftMessageVideoAttributeCache = Map<string, string>;
+
+const draftMessageVideoAttributeCache: DraftMessageVideoAttributeCache = new Map();
+type UseDraftMessageVideoAttributeCacheProps = {
+    draftMessage: string;
+    isEditing: boolean;
+    editingReportAction: OnyxTypes.ReportAction | null;
+    updateDraftMessage: (draftMessage: string) => void;
+    isEditInProgressRef: React.RefObject<boolean>;
+};
+
+function useDraftMessageVideoAttributeCache({
+    draftMessage,
+    isEditing = false,
+    editingReportAction = null,
+    updateDraftMessage: updateDraftMessageProp = () => {},
+    isEditInProgressRef,
+}: UseDraftMessageVideoAttributeCacheProps): DraftMessageVideoAttributeCache {
+    const prevDraftMessage = usePrevious(draftMessage);
+
+    useEffect(() => {
+        if (!isEditing) {
+            return;
+        }
+
+        draftMessageVideoAttributeCache.clear();
+
+        const originalMessage = Parser.htmlToMarkdown(getReportActionHtml(editingReportAction), {
+            cacheVideoAttributes: (videoSource, attrs) => draftMessageVideoAttributeCache.set(videoSource, attrs),
+        });
+        if (
+            isDeletedAction(editingReportAction) ||
+            !!(editingReportAction?.message && draftMessage === originalMessage) ||
+            !!(prevDraftMessage === draftMessage || isEditInProgressRef.current)
+        ) {
+            return;
+        }
+        updateDraftMessageProp(draftMessage);
+    }, [draftMessage, editingReportAction, isEditInProgressRef, isEditing, prevDraftMessage, updateDraftMessageProp]);
+
+    return draftMessageVideoAttributeCache;
+}
+
+export default useDraftMessageVideoAttributeCache;
+export {draftMessageVideoAttributeCache};
+export type {DraftMessageVideoAttributeCache};

From cba88e8ca94ac1d2d31f876655490db765cffff4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 22:53:28 +0000
Subject: [PATCH 044/233] Update ReportActionItemMessageEdit.tsx

---
 .../report/ReportActionItemMessageEdit.tsx    | 34 +++++++------------
 1 file changed, 12 insertions(+), 22 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 6882e82ed695..f6dfdfbd308d 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -14,7 +14,6 @@ import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTrig
 import useKeyboardState from '@hooks/useKeyboardState';
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
-import usePrevious from '@hooks/usePrevious';
 import useReportScrollManager from '@hooks/useReportScrollManager';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useScrollBlocker from '@hooks/useScrollBlocker';
@@ -27,14 +26,11 @@ import {saveReportActionDraft} from '@libs/actions/Report';
 import {isMobileChrome} from '@libs/Browser';
 import {canSkipTriggerHotkeys, insertText} from '@libs/ComposerUtils';
 import DomUtils from '@libs/DomUtils';
-import draftMessageVideoAttributeCache from '@libs/DraftMessageVideoAttributeCache';
 import {extractEmojis, getZWNJCursorOffset, insertZWNJBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils';
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
 import type {Selection} from '@libs/focusComposerWithDelay/types';
-import Parser from '@libs/Parser';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import reportActionItemEventHandler from '@libs/ReportActionItemEventHandler';
-import {getReportActionHtml, isDeletedAction} from '@libs/ReportActionsUtils';
 import setShouldShowComposeInputKeyboardAware from '@libs/setShouldShowComposeInputKeyboardAware';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -52,6 +48,7 @@ import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDeb
 import useEditMessage from './ReportActionCompose/useEditMessage';
 import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
+import useDraftMessageVideoAttributeCache from './useDraftMessageVideoAttributeCache';
 
 type ReportActionItemMessageEditProps = {
     /** All the data of the action */
@@ -108,7 +105,6 @@ function ReportActionItemMessageEdit({
     const {preferredLocale} = useLocalize();
     const {isKeyboardShown} = useKeyboardState();
     const {shouldUseNarrowLayout} = useResponsiveLayout();
-    const prevDraftMessage = usePrevious(draftMessage);
     const suggestionsRef = useRef<SuggestionsRef>(null);
     const mobileInputScrollPosition = useRef(0);
     const cursorPositionValue = useSharedValue({x: 0, y: 0});
@@ -150,23 +146,18 @@ function ReportActionItemMessageEdit({
     const {isScrolling, startScrollBlock, endScrollBlock} = useScrollBlocker();
 
     const composerRef = useRef<ComposerRef | null>(null);
-    const isFocusedRef = useRef<boolean>(false);
     const draftRef = useRef(draft);
     const emojiPickerSelectionRef = useRef<Selection | undefined>(undefined);
     // The ref to check whether the comment saving is in progress
     const isCommentPendingSaved = useRef(false);
 
-    useEffect(() => {
-        draftMessageVideoAttributeCache.clear();
-
-        const originalMessage = Parser.htmlToMarkdown(getReportActionHtml(action), {
-            cacheVideoAttributes: (videoSource, attrs) => draftMessageVideoAttributeCache.set(videoSource, attrs),
-        });
-        if (isDeletedAction(action) || !!(action.message && draftMessage === originalMessage) || !!(prevDraftMessage === draftMessage || isCommentPendingSaved.current)) {
-            return;
-        }
-        setDraft(draftMessage);
-    }, [draftMessage, action, prevDraftMessage]);
+    useDraftMessageVideoAttributeCache({
+        draftMessage,
+        isEditing: true,
+        editingReportAction: action,
+        updateDraftMessage: setDraft,
+        isEditInProgressRef: isCommentPendingSaved,
+    });
 
     useEffect(() => {
         composerFocusKeepFocusOn(composerRef.current as HTMLElement, isFocused, modal, onyxInputFocused);
@@ -411,12 +402,11 @@ function ReportActionItemMessageEdit({
     }, [draft, debouncedCommentMaxLengthValidation]);
 
     useEffect(() => {
-        // required for keeping last state of isFocused variable
-        isFocusedRef.current = isFocused;
-
-        if (!isFocused) {
-            hideSuggestionMenu();
+        if (isFocused) {
+            return;
         }
+
+        hideSuggestionMenu();
     }, [isFocused, hideSuggestionMenu]);
 
     return (

From 225d2b9951c7b1471ef2e2c997dad0a6be0f1aa3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 16 Feb 2026 23:08:10 +0000
Subject: [PATCH 045/233] fix: remove unknown cast

---
 .../ComposerWithSuggestions/ComposerWithSuggestions.tsx         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 5eef48c4fa51..4c963f378f14 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -918,7 +918,7 @@ function ComposerWithSuggestions({
                 getCurrentText,
                 clearWorklet,
                 resetHeight,
-            }) as unknown as ComposerWithSuggestionsRef,
+            }) as ComposerWithSuggestionsRef,
         [focus, replaceSelectionWithText, clearWorklet, resetHeight, getCurrentText],
     );
 

From 58e986a1265e079529fb751a6d0f62d5b0f2fe43 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 09:14:47 +0000
Subject: [PATCH 046/233] refactor: remove unnecessary callback

---
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index f6dfdfbd308d..b52a4ab24714 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -271,8 +271,6 @@ function ReportActionItemMessageEdit({
         composerRef,
     });
 
-    const publishDraftMessage = useCallback(() => publishDraft(draft), [publishDraft, draft]);
-
     /**
      * @param emoji
      */
@@ -335,13 +333,13 @@ function ReportActionItemMessageEdit({
             }
             if (keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !keyEvent.shiftKey) {
                 e.preventDefault();
-                publishDraftMessage();
+                publishDraft(draft);
             } else if (keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) {
                 e.preventDefault();
                 deleteDraft();
             }
         },
-        [deleteDraft, hideSuggestionMenu, isKeyboardShown, shouldUseNarrowLayout, publishDraftMessage],
+        [shouldUseNarrowLayout, isKeyboardShown, hideSuggestionMenu, publishDraft, draft, deleteDraft],
     );
 
     const measureContainer = useCallback((callback: MeasureInWindowOnSuccessCallback) => {
@@ -524,7 +522,7 @@ function ReportActionItemMessageEdit({
 
                     <SendButton
                         isDisabled={isExceedingMaxLength}
-                        onSend={publishDraftMessage}
+                        onSend={() => publishDraft(draft)}
                         isEditing
                     />
                 </View>

From 0aa900a62665bdc11efa9bd46408704ad6d3b382 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 09:23:07 +0000
Subject: [PATCH 047/233] refactor: simplify `ReportActionsList` draftMessage
 retrieval

---
 src/pages/inbox/report/ReportActionsList.tsx | 47 +++++---------------
 1 file changed, 11 insertions(+), 36 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index e92ed7f56a8b..ba7b7db10049 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -48,7 +48,6 @@ import {
     canShowReportRecipientLocalTime,
     canUserPerformWriteAction,
     chatIncludesChronosWithID,
-    getOriginalReportID,
     getReportLastVisibleActionCreated,
     isArchivedNonExpenseReport,
     isCanceledTaskReport,
@@ -74,6 +73,7 @@ import ListBoundaryLoader from './ListBoundaryLoader';
 import ReportActionsListItemRenderer from './ReportActionsListItemRenderer';
 import shouldDisplayNewMarkerOnReportAction from './shouldDisplayNewMarkerOnReportAction';
 import useReportUnreadMessageScrollTracking from './useReportUnreadMessageScrollTracking';
+import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 
 type ReportActionsListProps = {
     /** The report currently being looked at */
@@ -183,9 +183,8 @@ function ReportActionsList({
     const isReportArchived = useReportIsArchived(report?.reportID);
     const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: tierNameSelector, canBeMissing: false});
     const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true});
-    const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}`, {canBeMissing: true});
+    const [allDraftMessages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}`, {canBeMissing: true});
     const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}`, {canBeMissing: true});
-    const [reportActionsFromOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canBeMissing: true});
     const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true});
     const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true});
     const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;
@@ -679,33 +678,10 @@ function ReportActionsList({
         return isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report);
     }, [parentReportAction, report, sortedVisibleReportActions]);
 
-    const activeMobileEditActionID = useMemo(() => {
-        if (!shouldUseNarrowLayout || !draftMessage) {
-            return null;
-        }
-
-        for (const reportDrafts of Object.values(draftMessage)) {
-            if (!reportDrafts) {
-                continue;
-            }
-
-            for (const [actionID, draft] of Object.entries(reportDrafts)) {
-                if (draft?.message) {
-                    return actionID;
-                }
-            }
-        }
-
-        return null;
-    }, [shouldUseNarrowLayout, draftMessage]);
-
+    const {editingReportActionID, editingMessage} = useReportActionActiveEdit();
     const renderItem = useCallback(
         ({item: reportAction, index}: ListRenderItemInfo<OnyxTypes.ReportAction>) => {
-            const originalReportID = getOriginalReportID(report.reportID, reportAction, reportActionsFromOnyx);
-            const reportDraftMessages = draftMessage?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`];
-            const matchingDraftMessage = reportDraftMessages?.[reportAction.reportActionID];
-            const matchingDraftMessageString =
-                shouldUseNarrowLayout && activeMobileEditActionID && activeMobileEditActionID !== reportAction.reportActionID ? undefined : matchingDraftMessage?.message;
+            const draftMessage = !!editingReportActionID && editingReportActionID === reportAction.reportActionID ? (editingMessage ?? undefined) : undefined;
 
             const actionEmojiReactions = emojiReactions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportAction.reportActionID}`];
             const transactionID = isMoneyRequestAction(reportAction) && getOriginalMessage(reportAction)?.IOUTransactionID;
@@ -737,9 +713,9 @@ function ReportActionsList({
                     userWalletTierName={userWalletTierName}
                     isUserValidated={isUserValidated}
                     personalDetails={personalDetailsList}
-                    draftMessage={matchingDraftMessageString}
+                    allDraftMessages={allDraftMessages}
                     emojiReactions={actionEmojiReactions}
-                    allDraftMessages={draftMessage}
+                    draftMessage={draftMessage}
                     allEmojiReactions={emojiReactions}
                     isReportArchived={isReportArchived}
                     linkedTransactionRouteError={actionLinkedTransactionRouteError}
@@ -751,8 +727,10 @@ function ReportActionsList({
             );
         },
         [
-            draftMessage,
+            editingReportActionID,
+            editingMessage,
             emojiReactions,
+            transactions,
             allReports,
             policies,
             parentReportAction,
@@ -766,18 +744,15 @@ function ReportActionsList({
             unreadMarkerReportActionID,
             firstVisibleReportActionID,
             shouldUseThreadDividerLine,
-            transactions,
             userWalletTierName,
             isUserValidated,
             personalDetailsList,
+            allDraftMessages,
+            isReportArchived,
             userBillingFundID,
             isTryNewDotNVPDismissed,
-            isReportArchived,
-            activeMobileEditActionID,
-            shouldUseNarrowLayout,
             reportNameValuePairs?.origin,
             reportNameValuePairs?.originalID,
-            reportActionsFromOnyx,
         ],
     );
 

From 1bd7e175bedd73c14fc5a9213b59e3536e890366 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 10:06:10 +0000
Subject: [PATCH 048/233] refactor: rename `handleSendMessage` callback

---
 .../report/ReportActionCompose/ReportActionCompose.tsx    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index c156d05192e3..98ddb1e11445 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -478,7 +478,7 @@ function ReportActionCompose({
     // useSharedValue on web doesn't support functions, so we need to wrap it in an object.
     const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
 
-    const handleSendMessage = useCallback(() => {
+    const sendMessage = useCallback(() => {
         if (isSendDisabled || !debouncedCommentMaxLengthValidation.flush()) {
             return;
         }
@@ -498,7 +498,7 @@ function ReportActionCompose({
             clearWorklet?.();
         });
     }, [isSendDisabled, debouncedCommentMaxLengthValidation, isComposerFullSize, reportID, composerRefShared]);
-    onSubmitAction = handleSendMessage;
+    onSubmitAction = sendMessage;
 
     const emojiPositionValues = useMemo(
         () => ({
@@ -633,7 +633,7 @@ function ReportActionCompose({
                             onClear={submitForm}
                             disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
                             setIsCommentEmpty={setIsCommentEmpty}
-                            onEnterKeyPress={handleSendMessage}
+                            onEnterKeyPress={sendMessage}
                             shouldShowComposeInput={shouldShowComposeInput}
                             onFocus={onFocus}
                             onBlur={onBlur}
@@ -683,7 +683,7 @@ function ReportActionCompose({
                         <SendButton
                             isEditing={isEditingInComposer}
                             isDisabled={isSendDisabled}
-                            onSend={handleSendMessage}
+                            onSend={sendMessage}
                         />
                     </View>
                     {ErrorModal}

From 89395b7ae955d7308c69e628cd94942fb9a49021 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 10:11:58 +0000
Subject: [PATCH 049/233] fix: `SendButton` disabled state

---
 src/pages/inbox/report/ReportActionCompose/SendButton.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
index ad726e3b5a18..10027addd4e3 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
@@ -66,8 +66,8 @@ function SendButton({isDisabled: isDisabledProp = false, isEditing = false, onSe
                         <PressableWithFeedback
                             style={({pressed, isDisabled}) => [
                                 styles.chatItemSubmitButton,
-                                pressed || isDisabled ? undefined : styles.buttonSuccess,
-                                isDisabled ? styles.cursorDisabled : undefined,
+                                isDisabledProp || pressed || isDisabled ? undefined : styles.buttonSuccess,
+                                isDisabledProp ? styles.cursorDisabled : undefined,
                             ]}
                             // Since the parent View has accessible, we need to set accessible to false here to avoid duplicate accessibility elements.
                             // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.

From 64e4b883299bdf98ec4df5a5bc7b1abfb6023fb3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 10:12:43 +0000
Subject: [PATCH 050/233] fix: value resetting immediately

---
 .../ComposerWithSuggestions.tsx               | 31 ++++++++++---------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 4c963f378f14..5d860df1aa2b 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -290,7 +290,11 @@ function ComposerWithSuggestions({
     const commentRef = useRef(value);
 
     useEffect(() => {
-        if (!shouldUseNarrowLayout || !editingReportActionID) {
+        if (!editingReportActionID) {
+            return;
+        }
+
+        if (!shouldUseNarrowLayout) {
             setValue('');
             return;
         }
@@ -388,13 +392,13 @@ function ComposerWithSuggestions({
     const debouncedSaveReportComment = useMemo(
         () =>
             lodashDebounce((selectedReportID: string, newComment: string | null) => {
-                if (shouldUseNarrowLayout) {
+                if (editingReportActionID) {
                     return;
                 }
                 saveReportDraftComment(selectedReportID, newComment);
                 isCommentPendingSaved.current = false;
             }, 1000),
-        [shouldUseNarrowLayout],
+        [editingReportActionID],
     );
 
     useEffect(() => {
@@ -529,19 +533,14 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            if (shouldUseNarrowLayout) {
-                if (editingReportActionID && !didSubmitEditRef.current) {
-                    if (shouldDebounceSaveComment) {
-                        isDraftPendingSaved.current = true;
-                        debouncedSaveDraft(newCommentConverted);
-                    } else {
-                        saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
-                    }
+            if (!!editingReportActionID && shouldUseNarrowLayout && !didSubmitEditRef.current) {
+                if (shouldDebounceSaveComment) {
+                    isDraftPendingSaved.current = true;
+                    debouncedSaveDraft(newCommentConverted);
+                    return;
                 }
 
-                if (newCommentConverted) {
-                    debouncedBroadcastUserIsTyping(reportID, currentUserAccountID);
-                }
+                saveReportActionDraft(reportID, {reportActionID: editingReportActionID} as OnyxTypes.ReportAction, newCommentConverted);
                 return;
             }
 
@@ -551,6 +550,10 @@ function ComposerWithSuggestions({
             } else {
                 saveReportDraftComment(reportID, newCommentConverted);
             }
+
+            if (newCommentConverted) {
+                debouncedBroadcastUserIsTyping(reportID, currentUserAccountID);
+            }
         },
         [
             raiseIsScrollLikelyLayoutTriggered,

From fa8a9f299cfd2fbcd12c84344fbcee96765d314e Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 10:24:28 +0000
Subject: [PATCH 051/233] fix: only clear composer value when we switch from
 narrow to wide layout

---
 .../ComposerWithSuggestions.tsx                        | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 5d860df1aa2b..186aa4bcc683 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -289,12 +289,20 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
+    const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     useEffect(() => {
         if (!editingReportActionID) {
+            wasEditingInComposerRef.current = shouldUseNarrowLayout;
             return;
         }
 
-        if (!shouldUseNarrowLayout) {
+        if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
+            wasEditingInComposerRef.current = true;
+        }
+
+        if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
+            wasEditingInComposerRef.current = false;
+            // When we switch from narrow to wide layout, we need to clear the composer value
             setValue('');
             return;
         }

From fa328e6769f9870868ea1c1439a5673c27421102 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 10:59:57 +0000
Subject: [PATCH 052/233] fix: clearing and resetting of composer and edit
 message composers

---
 .../ComposerWithSuggestions.tsx               | 29 +++++++++++++++----
 .../ReportActionCompose.tsx                   |  2 +-
 2 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 186aa4bcc683..eca30b4eb1eb 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -289,21 +289,39 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
+    const wasEditing = useRef(!!editingReportActionID);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     useEffect(() => {
         if (!editingReportActionID) {
+            if (wasEditing.current && shouldUseNarrowLayout) {
+                setValue('');
+            }
+
+            wasEditing.current = false;
             wasEditingInComposerRef.current = shouldUseNarrowLayout;
             return;
         }
 
+        if (wasEditing.current && !shouldUseNarrowLayout) {
+            wasEditing.current = true;
+            // When we switch from narrow to wide layout, we need to clear the composer value
+            setValue('');
+            return;
+        }
+
+        wasEditing.current = true;
+
         if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = true;
         }
 
-        if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
-            wasEditingInComposerRef.current = false;
-            // When we switch from narrow to wide layout, we need to clear the composer value
-            setValue('');
+        if (!shouldUseNarrowLayout) {
+            if (wasEditingInComposerRef.current) {
+                wasEditingInComposerRef.current = false;
+                // When we switch from narrow to wide layout, we need to clear the composer value
+                setValue('');
+            }
+
             return;
         }
 
@@ -312,8 +330,9 @@ function ComposerWithSuggestions({
         emojisPresentBefore.current = extractEmojis(nextValue);
 
         setValue(nextValue);
+        setSelection(currentEditMessageSelection ?? {start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0});
         composerRef.current?.focus();
-    }, [draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
+    }, [currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 98ddb1e11445..418a4da9e891 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -641,7 +641,7 @@ function ReportActionCompose({
                             onValueChange={onValueChange}
                             didHideComposerInput={didHideComposerInput}
                             forwardedFSClass={fsClass}
-                            key={editingReportActionID}
+                            // key={editingReportActionID}
                         />
                         {shouldDisplayDualDropZone && (
                             <DualDropZone

From d8f03f8698d7590f8dcba3e2a6f36ef56abbaaa2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 11:11:28 +0000
Subject: [PATCH 053/233] fix: composer value states

---
 .../ComposerWithSuggestions.tsx               | 91 ++++++++++++++-----
 1 file changed, 67 insertions(+), 24 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index eca30b4eb1eb..bb00129b7cc0 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -291,48 +291,91 @@ function ComposerWithSuggestions({
 
     const wasEditing = useRef(!!editingReportActionID);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
+
+    const applyComposerValue = useCallback(
+        (nextValue: string, selectionOverride?: TextSelection | null, shouldFocusComposer?: boolean) => {
+            const selectionToApply =
+                selectionOverride ?? ({start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0} satisfies TextSelection);
+
+            commentRef.current = nextValue;
+            emojisPresentBefore.current = extractEmojis(nextValue);
+
+            setValue(nextValue);
+            setSelection(selectionToApply);
+
+            if (shouldFocusComposer) {
+                composerRef.current?.focus();
+            }
+        },
+        [],
+    );
+
     useEffect(() => {
-        if (!editingReportActionID) {
-            if (wasEditing.current && shouldUseNarrowLayout) {
-                setValue('');
+        const isEditing = !!editingReportActionID;
+        const isNarrowLayout = shouldUseNarrowLayout;
+
+        if (!isEditing) {
+            if (wasEditing.current && wasEditingInComposerRef.current) {
+                // Editing just ended in the composer – restore the draft comment.
+                const nextValue = draftComment ?? '';
+                applyComposerValue(nextValue, null, false);
             }
 
             wasEditing.current = false;
-            wasEditingInComposerRef.current = shouldUseNarrowLayout;
+            wasEditingInComposerRef.current = isNarrowLayout;
             return;
         }
 
-        if (wasEditing.current && !shouldUseNarrowLayout) {
+        const shouldEditInComposer = isNarrowLayout;
+        const wasEditingBefore = wasEditing.current;
+        const wasEditingInComposerBefore = wasEditingInComposerRef.current;
+
+        // Editing just started.
+        if (!wasEditingBefore) {
             wasEditing.current = true;
-            // When we switch from narrow to wide layout, we need to clear the composer value
-            setValue('');
+            wasEditingInComposerRef.current = shouldEditInComposer;
+
+            if (!shouldEditInComposer) {
+                // Wide layout – another editor handles the edit, keep composer draft as-is.
+                return;
+            }
+
+            const nextValue = editingMessage ?? draftComment ?? '';
+            applyComposerValue(nextValue, currentEditMessageSelection, true);
             return;
         }
 
-        wasEditing.current = true;
-
-        if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
+        // Editing is ongoing and layout toggled from wide to narrow.
+        if (shouldEditInComposer && !wasEditingInComposerBefore) {
             wasEditingInComposerRef.current = true;
+            const nextValue = editingMessage ?? draftComment ?? '';
+            applyComposerValue(nextValue, currentEditMessageSelection, true);
+            return;
         }
 
-        if (!shouldUseNarrowLayout) {
-            if (wasEditingInComposerRef.current) {
-                wasEditingInComposerRef.current = false;
-                // When we switch from narrow to wide layout, we need to clear the composer value
-                setValue('');
-            }
-
+        // Editing is ongoing and layout toggled from narrow to wide.
+        if (!shouldEditInComposer && wasEditingInComposerBefore) {
+            wasEditingInComposerRef.current = false;
+            const nextValue = draftComment ?? '';
+            applyComposerValue(nextValue, null, false);
             return;
         }
 
-        const nextValue = editingMessage ?? draftComment ?? '';
-        commentRef.current = nextValue;
-        emojisPresentBefore.current = extractEmojis(nextValue);
+        // Editing is ongoing and layout did not change.
+        if (shouldEditInComposer && wasEditingInComposerBefore) {
+            const nextValue = editingMessage ?? draftComment ?? '';
 
-        setValue(nextValue);
-        setSelection(currentEditMessageSelection ?? {start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0});
-        composerRef.current?.focus();
-    }, [currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
+            if (nextValue !== commentRef.current) {
+                // We switched to editing a different message while staying in narrow layout.
+                applyComposerValue(nextValue, currentEditMessageSelection, true);
+                return;
+            }
+
+            if (currentEditMessageSelection) {
+                setSelection(currentEditMessageSelection);
+            }
+        }
+    }, [applyComposerValue, currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank

From 3e133b17bd23805e540027eb4612561271078c95 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 12:22:00 +0000
Subject: [PATCH 054/233] fix: focus composer after setting selection

---
 .../ComposerWithSuggestions/ComposerWithSuggestions.tsx          | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index bb00129b7cc0..2783c3765d37 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -373,6 +373,7 @@ function ComposerWithSuggestions({
 
             if (currentEditMessageSelection) {
                 setSelection(currentEditMessageSelection);
+                composerRef.current?.focus();
             }
         }
     }, [applyComposerValue, currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);

From 485c0f7564c07ee904d09feba0b11161c6662317 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 13:24:54 +0000
Subject: [PATCH 055/233] fix: composer value states

---
 .../ComposerWithSuggestions.tsx               | 74 +++++++++----------
 1 file changed, 34 insertions(+), 40 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 2783c3765d37..54925af370fa 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -290,12 +290,14 @@ function ComposerWithSuggestions({
     const commentRef = useRef(value);
 
     const wasEditing = useRef(!!editingReportActionID);
+
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
+    const previousEditingReportActionIDRef = useRef<string | null>(editingReportActionID ?? null);
 
     const applyComposerValue = useCallback(
-        (nextValue: string, selectionOverride?: TextSelection | null, shouldFocusComposer?: boolean) => {
-            const selectionToApply =
-                selectionOverride ?? ({start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0} satisfies TextSelection);
+        (nextValue: string, isEditingInComposer?: boolean) => {
+            const defaultSelection = {start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0} satisfies TextSelection;
+            const selectionToApply = isEditingInComposer ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection;
 
             commentRef.current = nextValue;
             emojisPresentBefore.current = extractEmojis(nextValue);
@@ -303,78 +305,70 @@ function ComposerWithSuggestions({
             setValue(nextValue);
             setSelection(selectionToApply);
 
-            if (shouldFocusComposer) {
+            if (isEditingInComposer) {
                 composerRef.current?.focus();
             }
         },
-        [],
+        [currentEditMessageSelection],
     );
 
     useEffect(() => {
         const isEditing = !!editingReportActionID;
-        const isNarrowLayout = shouldUseNarrowLayout;
+        const previousEditingReportActionID = previousEditingReportActionIDRef.current;
+        const currentEditingReportActionID = editingReportActionID ?? null;
+        const didChangeEditedAction = isEditing && previousEditingReportActionID && currentEditingReportActionID && previousEditingReportActionID !== currentEditingReportActionID;
+
+        previousEditingReportActionIDRef.current = currentEditingReportActionID;
 
         if (!isEditing) {
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment.
                 const nextValue = draftComment ?? '';
-                applyComposerValue(nextValue, null, false);
+                applyComposerValue(nextValue, false);
             }
 
             wasEditing.current = false;
-            wasEditingInComposerRef.current = isNarrowLayout;
+            wasEditingInComposerRef.current = shouldUseNarrowLayout;
             return;
         }
 
-        const shouldEditInComposer = isNarrowLayout;
-        const wasEditingBefore = wasEditing.current;
-        const wasEditingInComposerBefore = wasEditingInComposerRef.current;
-
         // Editing just started.
-        if (!wasEditingBefore) {
+        if (!wasEditing.current) {
             wasEditing.current = true;
-            wasEditingInComposerRef.current = shouldEditInComposer;
+            wasEditingInComposerRef.current = shouldUseNarrowLayout;
 
-            if (!shouldEditInComposer) {
+            if (!shouldUseNarrowLayout) {
                 // Wide layout – another editor handles the edit, keep composer draft as-is.
                 return;
             }
 
-            const nextValue = editingMessage ?? draftComment ?? '';
-            applyComposerValue(nextValue, currentEditMessageSelection, true);
+            // In narrow layout we always show the message being edited.
+            const nextValue = editingMessage ?? '';
+            applyComposerValue(nextValue, true);
+            return;
+        }
+
+        // We are already in editing mode, but the target message changed.
+        if (didChangeEditedAction && shouldUseNarrowLayout) {
+            const nextValue = editingMessage ?? '';
+            applyComposerValue(nextValue, true);
             return;
         }
 
         // Editing is ongoing and layout toggled from wide to narrow.
-        if (shouldEditInComposer && !wasEditingInComposerBefore) {
+        if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = true;
-            const nextValue = editingMessage ?? draftComment ?? '';
-            applyComposerValue(nextValue, currentEditMessageSelection, true);
+            // We just moved from wide to narrow while editing – start editing in the composer.
+            const nextValue = editingMessage ?? '';
+            applyComposerValue(nextValue, true);
             return;
         }
 
         // Editing is ongoing and layout toggled from narrow to wide.
-        if (!shouldEditInComposer && wasEditingInComposerBefore) {
+        if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = false;
-            const nextValue = draftComment ?? '';
-            applyComposerValue(nextValue, null, false);
-            return;
-        }
-
-        // Editing is ongoing and layout did not change.
-        if (shouldEditInComposer && wasEditingInComposerBefore) {
-            const nextValue = editingMessage ?? draftComment ?? '';
-
-            if (nextValue !== commentRef.current) {
-                // We switched to editing a different message while staying in narrow layout.
-                applyComposerValue(nextValue, currentEditMessageSelection, true);
-                return;
-            }
-
-            if (currentEditMessageSelection) {
-                setSelection(currentEditMessageSelection);
-                composerRef.current?.focus();
-            }
+            const nextValue = editingMessage ?? '';
+            applyComposerValue(nextValue, false);
         }
     }, [applyComposerValue, currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
 

From 50dec5fdbe16359b6bee0f62a6e2760c0dcc6e50 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 14:15:41 +0000
Subject: [PATCH 056/233] fix: send button incompatible with tests

---
 .../ReportActionCompose.tsx                   |  4 +-
 .../ReportActionComposeSendButton.tsx         | 70 ++++++++++++++
 .../report/ReportActionCompose/SendButton.tsx | 93 -------------------
 .../ReportActionCompose/SendOrSaveButton.tsx  | 59 ++++++++++++
 .../report/ReportActionItemMessageEdit.tsx    | 21 +++--
 5 files changed, 145 insertions(+), 102 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
 delete mode 100644 src/pages/inbox/report/ReportActionCompose/SendButton.tsx
 create mode 100644 src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 418a4da9e891..db4c8aea21d4 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -76,7 +76,7 @@ import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
 import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
 import MessageEditCancelButton from './MessageEditCancelButton';
-import SendButton from './SendButton';
+import ReportActionComposeSendButton from './ReportActionComposeSendButton';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
 import useDebouncedCommentMaxLengthValidation from './useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './useEditMessage';
@@ -680,7 +680,7 @@ function ReportActionCompose({
                                 shiftVertical={emojiShiftVertical}
                             />
                         )}
-                        <SendButton
+                        <ReportActionComposeSendButton
                             isEditing={isEditingInComposer}
                             isDisabled={isSendDisabled}
                             onSend={sendMessage}
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
new file mode 100644
index 000000000000..f25096e1f8e6
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
@@ -0,0 +1,70 @@
+import React, {memo} from 'react';
+import {View} from 'react-native';
+import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import SendOrSaveButton from './SendOrSaveButton';
+
+type SendButtonProps = {
+    /** Whether the button is disabled */
+    isDisabled: boolean;
+
+    /** Whether the button is in editing mode */
+    isEditing?: boolean;
+
+    /** Handle clicking on send button */
+    onSend: () => void;
+};
+
+function ReportActionComposeSendButton({isDisabled: isDisabledProp = false, isEditing = false, onSend}: SendButtonProps) {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+
+    // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to manage GestureDetector correctly
+    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+    const {isSmallScreenWidth} = useResponsiveLayout();
+    const Tap = Gesture.Tap()
+        .enabled(!isDisabledProp)
+        .onEnd(() => {
+            onSend();
+        })
+        .runOnJS(true)
+        .withTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
+
+    const accessibilityLabel = translate(isEditing ? 'common.saveChanges' : 'common.send');
+
+    return (
+        <View
+            style={styles.justifyContentEnd}
+            // Keep focus on the composer when Send message is clicked.
+            onMouseDown={(e) => e.preventDefault()}
+        >
+            <GestureDetector
+                // A new GestureDetector instance must be created when switching from a large screen to a small screen
+                // if not, the GestureDetector may not function correctly.
+                key={`send-button-${isSmallScreenWidth ? 'small-screen' : 'normal-screen'}`}
+                gesture={Tap}
+            >
+                <View
+                    // In order to make buttons accessible, we have to wrap children in a View with accessible and accessibilityRole="button" props based on the docs: https://docs.swmansion.com/react-native-gesture-handler/docs/components/buttons/
+                    accessible
+                    role={CONST.ROLE.BUTTON}
+                    accessibilityLabel={accessibilityLabel}
+                    collapsable={false}
+                >
+                    <SendOrSaveButton
+                        isDisabled={isDisabledProp}
+                        isEditing={isEditing}
+                        onSendOrSave={onSend}
+                        accessible={false}
+                        focusable={false}
+                    />
+                </View>
+            </GestureDetector>
+        </View>
+    );
+}
+
+export default memo(ReportActionComposeSendButton);
diff --git a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
deleted file mode 100644
index 10027addd4e3..000000000000
--- a/src/pages/inbox/report/ReportActionCompose/SendButton.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, {memo} from 'react';
-import {View} from 'react-native';
-import {Gesture, GestureDetector} from 'react-native-gesture-handler';
-import Icon from '@components/Icon';
-import * as Expensicons from '@components/Icon/Expensicons';
-import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import Tooltip from '@components/Tooltip';
-import useLocalize from '@hooks/useLocalize';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
-
-type SendButtonProps = {
-    /** Whether the button is disabled */
-    isDisabled: boolean;
-
-    /** Whether the button is in editing mode */
-    isEditing?: boolean;
-
-    /** Handle clicking on send button */
-    onSend: () => void;
-};
-
-function SendButton({isDisabled: isDisabledProp = false, isEditing = false, onSend}: SendButtonProps) {
-    const theme = useTheme();
-    const styles = useThemeStyles();
-    const {translate} = useLocalize();
-    const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
-
-    // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to manage GestureDetector correctly
-    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
-    const {isSmallScreenWidth} = useResponsiveLayout();
-    const Tap = Gesture.Tap()
-        .enabled(!isDisabledProp)
-        .onEnd(() => {
-            onSend();
-        })
-        .runOnJS(true)
-        .withTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
-
-    const label = translate(isEditing ? 'common.saveChanges' : 'common.send');
-    const sentryLabel = isEditing ? CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON : CONST.SENTRY_LABEL.REPORT.SEND_BUTTON;
-
-    return (
-        <View
-            style={styles.justifyContentEnd}
-            // Keep focus on the composer when Send message is clicked.
-            onMouseDown={(e) => e.preventDefault()}
-        >
-            <GestureDetector
-                // A new GestureDetector instance must be created when switching from a large screen to a small screen
-                // if not, the GestureDetector may not function correctly.
-                key={`send-button-${isSmallScreenWidth ? 'small-screen' : 'normal-screen'}`}
-                gesture={Tap}
-            >
-                <View
-                    // In order to make buttons accessible, we have to wrap children in a View with accessible and accessibilityRole="button" props based on the docs: https://docs.swmansion.com/react-native-gesture-handler/docs/components/buttons/
-                    accessible
-                    role={CONST.ROLE.BUTTON}
-                    accessibilityLabel={label}
-                    collapsable={false}
-                >
-                    <Tooltip text={label}>
-                        <PressableWithFeedback
-                            style={({pressed, isDisabled}) => [
-                                styles.chatItemSubmitButton,
-                                isDisabledProp || pressed || isDisabled ? undefined : styles.buttonSuccess,
-                                isDisabledProp ? styles.cursorDisabled : undefined,
-                            ]}
-                            // Since the parent View has accessible, we need to set accessible to false here to avoid duplicate accessibility elements.
-                            // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.
-                            accessible={false}
-                            focusable={false}
-                            sentryLabel={sentryLabel}
-                            disabled={isDisabledProp}
-                        >
-                            {({pressed}) => (
-                                <Icon
-                                    src={isEditing ? icons.Checkmark : Expensicons.Send}
-                                    fill={isDisabledProp || pressed ? theme.icon : theme.textLight}
-                                />
-                            )}
-                        </PressableWithFeedback>
-                    </Tooltip>
-                </View>
-            </GestureDetector>
-        </View>
-    );
-}
-
-export default memo(SendButton);
diff --git a/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
new file mode 100644
index 000000000000..3128e73d0739
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import type {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback';
+import Tooltip from '@components/Tooltip';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+
+type SendOrSaveButtonProps = PressableWithFeedbackProps & {
+    /** Whether the button is disabled */
+    isDisabled: boolean;
+
+    /** Whether the button is in editing mode */
+    isEditing?: boolean;
+
+    /** Handle clicking on send button */
+    onSendOrSave?: () => void;
+};
+
+function SendOrSaveButton({isDisabled: isDisabledProp = false, isEditing = false, onSendOrSave, ...restProps}: SendOrSaveButtonProps) {
+    const theme = useTheme();
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+    const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
+    const label = translate(isEditing ? 'common.saveChanges' : 'common.send');
+    const sentryLabel = isEditing ? CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON : CONST.SENTRY_LABEL.REPORT.SEND_BUTTON;
+
+    return (
+        <Tooltip text={label}>
+            <PressableWithFeedback
+                style={({pressed, isDisabled}) => [
+                    styles.chatItemSubmitButton,
+                    isDisabledProp || pressed || isDisabled ? undefined : styles.buttonSuccess,
+                    isDisabledProp ? styles.cursorDisabled : undefined,
+                ]}
+                // Since the parent View has accessible, we need to set accessible to false here to avoid duplicate accessibility elements.
+                // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.
+                onPress={onSendOrSave}
+                sentryLabel={sentryLabel}
+                disabled={isDisabledProp}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...restProps}
+            >
+                {({pressed}) => (
+                    <Icon
+                        src={isEditing ? icons.Checkmark : Expensicons.Send}
+                        fill={isDisabledProp || pressed ? theme.icon : theme.textLight}
+                    />
+                )}
+            </PressableWithFeedback>
+        </Tooltip>
+    );
+}
+
+export default SendOrSaveButton;
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index b52a4ab24714..2449e9e26507 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -42,12 +42,12 @@ import getCursorPosition from './ReportActionCompose/getCursorPosition';
 import getScrollPosition from './ReportActionCompose/getScrollPosition';
 import MessageEditCancelButton from './ReportActionCompose/MessageEditCancelButton';
 import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose';
-import SendButton from './ReportActionCompose/SendButton';
 import Suggestions from './ReportActionCompose/Suggestions';
 import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './ReportActionCompose/useEditMessage';
 import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
+import SendOrSaveButton from './ReportActionCompose/SendOrSaveButton';
 import useDraftMessageVideoAttributeCache from './useDraftMessageVideoAttributeCache';
 
 type ReportActionItemMessageEditProps = {
@@ -102,7 +102,7 @@ function ReportActionItemMessageEdit({
     const StyleUtils = useStyleUtils();
     const containerRef = useRef<View>(null);
     const reportScrollManager = useReportScrollManager();
-    const {preferredLocale} = useLocalize();
+    const {translate, preferredLocale} = useLocalize();
     const {isKeyboardShown} = useKeyboardState();
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const suggestionsRef = useRef<SuggestionsRef>(null);
@@ -520,11 +520,18 @@ function ReportActionItemMessageEdit({
                         />
                     </View>
 
-                    <SendButton
-                        isDisabled={isExceedingMaxLength}
-                        onSend={() => publishDraft(draft)}
-                        isEditing
-                    />
+                    <View style={styles.alignSelfEnd}>
+                        <SendOrSaveButton
+                            isDisabled={isExceedingMaxLength}
+                            isEditing
+                            onSendOrSave={() => publishDraft(draft)}
+                            accessibilityLabel={translate('common.saveChanges')}
+                            role={CONST.ROLE.BUTTON}
+                            hoverDimmingValue={1}
+                            pressDimmingValue={0.2}
+                            onMouseDown={(e) => e.preventDefault()}
+                        />
+                    </View>
                 </View>
             </View>
             {isExceedingMaxLength && !!exceededMaxLength && <ExceededCommentLength maxCommentLength={exceededMaxLength} />}

From 75442640f58e9b3f41fd5059ddfc5d1c5ecf57b8 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 14:15:50 +0000
Subject: [PATCH 057/233] fix: failing tests

---
 .../report/ReportActionItemMessageEdit.tsx    | 16 ++++---
 tests/ui/ReportActionItemMessageEditTest.tsx  | 48 ++++++++++++++-----
 2 files changed, 46 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 2449e9e26507..9bcade3891b7 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -37,18 +37,18 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
-import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
 import getCursorPosition from './ReportActionCompose/getCursorPosition';
 import getScrollPosition from './ReportActionCompose/getScrollPosition';
-import MessageEditCancelButton from './ReportActionCompose/MessageEditCancelButton';
 import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose';
 import Suggestions from './ReportActionCompose/Suggestions';
+import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
 import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './ReportActionCompose/useEditMessage';
-import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
-import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
+import MessageEditCancelButton from './ReportActionCompose/MessageEditCancelButton';
+import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
 import SendOrSaveButton from './ReportActionCompose/SendOrSaveButton';
 import useDraftMessageVideoAttributeCache from './useDraftMessageVideoAttributeCache';
+import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 
 type ReportActionItemMessageEditProps = {
     /** All the data of the action */
@@ -135,8 +135,10 @@ function ReportActionItemMessageEdit({
     }, [currentEditMessageSelection, defaultSelection, draft.length, setSelection]);
 
     const [isFocused, setIsFocused] = useState<boolean>(false);
-
-    const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength} = useDebouncedCommentMaxLengthValidation({reportID, isEditing: true});
+    const {debouncedCommentMaxLengthValidation, isExceedingMaxLength, exceededMaxLength} = useDebouncedCommentMaxLengthValidation({
+        reportID,
+        isEditing: true,
+    });
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
@@ -253,7 +255,7 @@ function ReportActionItemMessageEdit({
             debouncedSaveDraft(newDraft);
             isCommentPendingSaved.current = true;
         },
-        [raiseIsScrollLayoutTriggered, preferredSkinTone, preferredLocale, debouncedSaveDraft, selection?.end, setSelection],
+        [debouncedSaveDraft, preferredLocale, preferredSkinTone, raiseIsScrollLayoutTriggered, selection?.end, setSelection],
     );
 
     useEffect(() => {
diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index 204755c12d34..95a9c42d2ded 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -1,11 +1,16 @@
 import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, screen} from '@testing-library/react-native';
+import {act, fireEvent, render, screen} from '@testing-library/react-native';
+import React from 'react';
 import Onyx from 'react-native-onyx';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
 import {editReportComment} from '@libs/actions/Report';
+import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
+import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
-import {pressReportActionComposeSendButton, renderReportActionItemMessageEdit} from '../utils/ReportActionComposeUtils';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
 import * as TestHelper from '../utils/TestHelper';
 
 const mockEditReportComment = jest.mocked(editReportComment);
@@ -41,6 +46,29 @@ jest.mock('@react-navigation/native', () => ({
 
 TestHelper.setupGlobalFetchMock();
 
+const defaultReport = LHNTestUtils.getFakeReport();
+const defaultProps: ReportActionItemMessageEditProps = {
+    action: LHNTestUtils.getFakeReportAction(),
+    draftMessage: '',
+    reportID: defaultReport.reportID,
+    originalReportID: defaultReport.reportID,
+    index: 0,
+    isGroupPolicyReport: false,
+};
+
+const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
+    return render(
+        <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
+            <ReportActionItemMessageEdit
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...props}
+            />
+        </ComposeProviders>,
+    );
+};
+
 describe('ReportActionCompose Integration Tests', () => {
     beforeAll(() => {
         Onyx.init({
@@ -59,16 +87,15 @@ describe('ReportActionCompose Integration Tests', () => {
     describe('Message validation', () => {
         it('should edit when length is within the limit', async () => {
             renderReportActionItemMessageEdit();
-            const composer = screen.getByTestId(CONST.COMPOSER.NATIVE_ID);
+            const composer = screen.getByTestId('composer');
+            const saveChangesButton = screen.getByLabelText('common.saveChanges');
 
             // Given a message that is within the length limit
             const validMessage = 'x'.repeat(CONST.MAX_COMMENT_LENGTH);
             fireEvent.changeText(composer, validMessage);
 
-            await waitForBatchedUpdatesWithAct();
-
             // When the message is saved
-            pressReportActionComposeSendButton();
+            fireEvent.press(saveChangesButton);
 
             // Then the message should be edited
             expect(mockEditReportComment).toHaveBeenCalledTimes(1);
@@ -76,16 +103,15 @@ describe('ReportActionCompose Integration Tests', () => {
 
         it('should not edit when length exceeds the limit', async () => {
             renderReportActionItemMessageEdit();
-            const composer = screen.getByTestId(CONST.COMPOSER.NATIVE_ID);
+            const composer = screen.getByTestId('composer');
+            const saveChangesButton = screen.getByLabelText('common.saveChanges');
 
             // Given a message that is over the length limit
             const invalidMessage = 'x'.repeat(CONST.MAX_COMMENT_LENGTH + 1);
             fireEvent.changeText(composer, invalidMessage);
 
-            await waitForBatchedUpdatesWithAct();
-
             // When the message is saved
-            pressReportActionComposeSendButton();
+            fireEvent.press(saveChangesButton);
 
             // Then the message should NOT be edited
             expect(mockEditReportComment).toHaveBeenCalledTimes(0);

From 43d6e9fbb48ef5be1fb3bbf36d068586ff97e7f0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 14:15:58 +0000
Subject: [PATCH 058/233] fix: Composer ref

---
 src/components/Composer/implementation/index.native.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index 722e13cab437..34e528a3c36d 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -1,4 +1,4 @@
-import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
+import type {MarkdownStyle, MarkdownTextInput} from '@expensify/react-native-live-markdown';
 import mimeDb from 'mime-db';
 import React, {useCallback, useEffect, useMemo, useRef} from 'react';
 import type {NativeSyntheticEvent, TextInputChangeEvent, TextInputPasteEventData} from 'react-native';
@@ -36,7 +36,7 @@ function Composer({
     ref,
     ...props
 }: ComposerProps) {
-    const textInputRef = useRef<AnimatedMarkdownTextInputRef | null>(null);
+    const textInputRef = useRef<MarkdownTextInput | null>(null);
     const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? ''))), [value]);
     const theme = useTheme();
     const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);

From 6692e981613be0c725d7b513f7ceba4a73a793c4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 14:16:50 +0000
Subject: [PATCH 059/233] Update ReportActionComposeUtils.tsx

---
 tests/utils/ReportActionComposeUtils.tsx | 16 +---------------
 1 file changed, 1 insertion(+), 15 deletions(-)

diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index b0cb8fe24be3..d8ef98e2a04c 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -8,9 +8,6 @@ import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportA
 import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
 import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
 import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
-import {fireGestureHandler, getByGestureTestId} from 'react-native-gesture-handler/jest-utils';
-import CONST from '@src/CONST';
-import {State} from 'react-native-gesture-handler';
 import * as LHNTestUtils from './LHNTestUtils';
 
 const defaultReport = LHNTestUtils.getFakeReport();
@@ -87,15 +84,4 @@ const renderReportActionMessageEditComponents = (
     );
 };
 
-function pressReportActionComposeSendButton() {
-    const gesture = getByGestureTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
-
-    fireGestureHandler(gesture, [
-        {oldState: State.UNDETERMINED, state: State.BEGAN},
-        {oldState: State.BEGAN, state: State.ACTIVE},
-        {oldState: State.ACTIVE, state: State.ACTIVE},
-        {oldState: State.ACTIVE, state: State.END},
-    ]);
-}
-
-export {renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents, pressReportActionComposeSendButton};
+export {renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents};

From 7942284d61b59a47167380128a9001251b2f3b74 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Feb 2026 16:01:20 +0000
Subject: [PATCH 060/233] revert: unrelated change

---
 src/pages/inbox/report/ReportActionsList.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index ba7b7db10049..4a49f23c46fd 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -186,7 +186,7 @@ function ReportActionsList({
     const [allDraftMessages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}`, {canBeMissing: true});
     const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}`, {canBeMissing: true});
     const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true});
-    const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true});
+    const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: false});
     const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
     const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(false);

From 1dd03ba58dd82963f8d7e289acd1de843e53ff61 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 17:15:44 +0000
Subject: [PATCH 061/233] fix: extract `ReportActionComposeWrapper` to utils

---
 .../ReportActionCompose.perf-test.tsx         | 19 +------------------
 tests/utils/ReportActionComposeUtils.tsx      | 16 +++++++++++-----
 2 files changed, 12 insertions(+), 23 deletions(-)

diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx
index 4aa022dfa749..f439c4cecb01 100644
--- a/tests/perf-test/ReportActionCompose.perf-test.tsx
+++ b/tests/perf-test/ReportActionCompose.perf-test.tsx
@@ -3,15 +3,10 @@ import React from 'react';
 import Onyx from 'react-native-onyx';
 import type Animated from 'react-native-reanimated';
 import {measureRenders} from 'reassure';
-import OnyxListItemProvider from '@components/OnyxListItemProvider';
 import type {EmojiPickerRef} from '@libs/actions/EmojiPickerAction';
 import type Navigation from '@libs/Navigation/Navigation';
-import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import ComposeProviders from '@src/components/ComposeProviders';
-import {LocaleContextProvider} from '@src/components/LocaleContextProvider';
-import {KeyboardStateProvider} from '@src/components/withKeyboardState';
 import ONYXKEYS from '@src/ONYXKEYS';
-import * as LHNTestUtils from '../utils/LHNTestUtils';
+import {ReportActionComposeWrapper} from '../utils/ReportActionComposeUtils';
 import {translateLocal} from '../utils/TestHelper';
 import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
 
@@ -79,18 +74,6 @@ beforeEach(() => {
     Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false});
 });
 
-function ReportActionComposeWrapper() {
-    return (
-        <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider]}>
-            <ReportActionCompose
-                onSubmit={() => jest.fn()}
-                reportID="1"
-                report={LHNTestUtils.getFakeReport()}
-                isComposerFullSize
-            />
-        </ComposeProviders>
-    );
-}
 const mockEvent = {preventDefault: jest.fn()};
 
 test('[ReportActionCompose] should render Composer with text input interactions', async () => {
diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index d8ef98e2a04c..763e84b1e5db 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -8,6 +8,7 @@ import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportA
 import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
 import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
 import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
+import {KeyboardStateProvider} from '@src/components/withKeyboardState';
 import * as LHNTestUtils from './LHNTestUtils';
 
 const defaultReport = LHNTestUtils.getFakeReport();
@@ -17,7 +18,7 @@ function ReportActionEditMessageContextProviderForReport({children}: PropsWithCh
 }
 
 function ReportScreenProviders({children}: PropsWithChildren) {
-    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
 }
 
 const defaultReportActionComposeProps: ReportActionComposeProps = {
@@ -27,8 +28,8 @@ const defaultReportActionComposeProps: ReportActionComposeProps = {
     report: defaultReport,
 };
 
-const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
-    return render(
+function ReportActionComposeWrapper(props?: Partial<ReportActionComposeProps>) {
+    return (
         <ReportScreenProviders>
             <ReportActionCompose
                 // eslint-disable-next-line react/jsx-props-no-spreading
@@ -36,8 +37,13 @@ const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) =>
                 // eslint-disable-next-line react/jsx-props-no-spreading
                 {...props}
             />
-        </ReportScreenProviders>,
+        </ReportScreenProviders>
     );
+}
+
+const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
+    // eslint-disable-next-line react/jsx-props-no-spreading
+    return render(<ReportActionComposeWrapper {...props} />);
 };
 
 const defaultReportActionItemMessageEditProps: ReportActionItemMessageEditProps = {
@@ -84,4 +90,4 @@ const renderReportActionMessageEditComponents = (
     );
 };
 
-export {renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents};
+export {ReportActionComposeWrapper, renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents};

From 13e4d6fd4ca4793aacf77b3532b72532091fd655 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 18:04:56 +0000
Subject: [PATCH 062/233] fix: extract composer handle

---
 .../Composer/implementation/index.native.tsx  | 11 ++++---
 .../Composer/implementation/index.tsx         |  5 ++++
 src/components/Composer/useComposerHandle.ts  | 29 +++++++++++++++++++
 3 files changed, 41 insertions(+), 4 deletions(-)
 create mode 100644 src/components/Composer/useComposerHandle.ts

diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index 34e528a3c36d..5af4a929a2e9 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -1,6 +1,6 @@
-import type {MarkdownStyle, MarkdownTextInput} from '@expensify/react-native-live-markdown';
+import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
 import mimeDb from 'mime-db';
-import React, {useCallback, useEffect, useMemo, useRef} from 'react';
+import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react';
 import type {NativeSyntheticEvent, TextInputChangeEvent, TextInputPasteEventData} from 'react-native';
 import {StyleSheet} from 'react-native';
 import type {ComposerProps, ComposerRef} from '@components/Composer/types';
@@ -16,6 +16,7 @@ import Parser from '@libs/Parser';
 import getFileSize from '@pages/Share/getFileSize';
 import CONST from '@src/CONST';
 import type {FileObject} from '@src/types/utils/Attachment';
+import useComposerHandle, {getComposerHandle} from '@components/Composer/useComposerHandle';
 
 const excludeNoStyles: Array<keyof MarkdownStyle> = [];
 const excludeReportMentionStyle: Array<keyof MarkdownStyle> = ['mentionReport'];
@@ -36,7 +37,7 @@ function Composer({
     ref,
     ...props
 }: ComposerProps) {
-    const textInputRef = useRef<MarkdownTextInput | null>(null);
+    const textInputRef = useRef<AnimatedMarkdownTextInputRef | null>(null);
     const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? ''))), [value]);
     const theme = useTheme();
     const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
@@ -76,7 +77,7 @@ function Composer({
         // get a ref to the inner textInput element e.g. if we do
         // <constructor ref={el => this.textInput = el} /> this will not
         // return a ref to the component, but rather the HTML element by default
-        ref(textInputRef.current as ComposerRef);
+        ref(getComposerHandle(textInputRef.current, {}));
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, []);
 
@@ -110,6 +111,8 @@ function Composer({
     const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]);
     const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]);
 
+    useComposerHandle(ref, textInputRef, {});
+
     return (
         <RNMarkdownTextInput
             id={CONST.COMPOSER.NATIVE_ID}
diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index f3a47b879700..7a2fc80bd5e4 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -21,6 +21,7 @@ import {base64ToFile} from '@libs/fileDownload/FileUtils';
 import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
 import Parser from '@libs/Parser';
 import CONST from '@src/CONST';
+import useComposerHandle from '@components/Composer/useComposerHandle';
 
 const excludeNoStyles: Array<keyof MarkdownStyle> = [];
 const excludeReportMentionStyle: Array<keyof MarkdownStyle> = ['mentionReport'];
@@ -304,6 +305,10 @@ function Composer({
         } as ComposerRef;
     }, [clear]);
 
+    useComposerHandle(ref, textInputRef, {
+        clear,
+    });
+
     const handleKeyPress = useCallback(
         (e: TextInputKeyPressEvent) => {
             // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed
diff --git a/src/components/Composer/useComposerHandle.ts b/src/components/Composer/useComposerHandle.ts
new file mode 100644
index 000000000000..193f17d9eaee
--- /dev/null
+++ b/src/components/Composer/useComposerHandle.ts
@@ -0,0 +1,29 @@
+import {useImperativeHandle} from 'react';
+import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
+import type {ComposerRef} from './types';
+
+function useComposerHandle(ref: React.Ref<ComposerRef> | undefined, textInputRef: React.RefObject<AnimatedMarkdownTextInputRef | null>, additionalMethods: Partial<ComposerRef>) {
+    useImperativeHandle(ref, () => {
+        const textInput = textInputRef.current;
+        if (!textInput) {
+            throw new Error('textInput is not available. This should never happen and indicates a developer error.');
+        }
+
+        return getComposerHandle(textInput, additionalMethods);
+    }, [additionalMethods, textInputRef]);
+}
+
+function getComposerHandle(textInput: AnimatedMarkdownTextInputRef | null, additionalMethods: Partial<ComposerRef>) {
+    return {
+        ...textInput,
+        blur: () => textInput?.blur(),
+        focus: () => textInput?.focus(),
+        get scrollTop() {
+            return textInput?.scrollTop;
+        },
+        ...additionalMethods,
+    } as ComposerRef;
+}
+
+export default useComposerHandle;
+export {getComposerHandle};

From f56af3e1eb9e0490cddce29ef253c803193e374a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Feb 2026 18:28:08 +0000
Subject: [PATCH 063/233] fix: composer ref

---
 .../Composer/implementation/index.native.tsx  | 11 +++----
 .../Composer/implementation/index.tsx         |  5 ---
 src/components/Composer/useComposerHandle.ts  | 29 ----------------
 .../ComposerWithSuggestions.tsx               | 33 ++++++++++++++-----
 4 files changed, 28 insertions(+), 50 deletions(-)
 delete mode 100644 src/components/Composer/useComposerHandle.ts

diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index 5af4a929a2e9..34e528a3c36d 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -1,6 +1,6 @@
-import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
+import type {MarkdownStyle, MarkdownTextInput} from '@expensify/react-native-live-markdown';
 import mimeDb from 'mime-db';
-import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef} from 'react';
 import type {NativeSyntheticEvent, TextInputChangeEvent, TextInputPasteEventData} from 'react-native';
 import {StyleSheet} from 'react-native';
 import type {ComposerProps, ComposerRef} from '@components/Composer/types';
@@ -16,7 +16,6 @@ import Parser from '@libs/Parser';
 import getFileSize from '@pages/Share/getFileSize';
 import CONST from '@src/CONST';
 import type {FileObject} from '@src/types/utils/Attachment';
-import useComposerHandle, {getComposerHandle} from '@components/Composer/useComposerHandle';
 
 const excludeNoStyles: Array<keyof MarkdownStyle> = [];
 const excludeReportMentionStyle: Array<keyof MarkdownStyle> = ['mentionReport'];
@@ -37,7 +36,7 @@ function Composer({
     ref,
     ...props
 }: ComposerProps) {
-    const textInputRef = useRef<AnimatedMarkdownTextInputRef | null>(null);
+    const textInputRef = useRef<MarkdownTextInput | null>(null);
     const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? ''))), [value]);
     const theme = useTheme();
     const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
@@ -77,7 +76,7 @@ function Composer({
         // get a ref to the inner textInput element e.g. if we do
         // <constructor ref={el => this.textInput = el} /> this will not
         // return a ref to the component, but rather the HTML element by default
-        ref(getComposerHandle(textInputRef.current, {}));
+        ref(textInputRef.current as ComposerRef);
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, []);
 
@@ -111,8 +110,6 @@ function Composer({
     const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]);
     const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]);
 
-    useComposerHandle(ref, textInputRef, {});
-
     return (
         <RNMarkdownTextInput
             id={CONST.COMPOSER.NATIVE_ID}
diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 7a2fc80bd5e4..f3a47b879700 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -21,7 +21,6 @@ import {base64ToFile} from '@libs/fileDownload/FileUtils';
 import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
 import Parser from '@libs/Parser';
 import CONST from '@src/CONST';
-import useComposerHandle from '@components/Composer/useComposerHandle';
 
 const excludeNoStyles: Array<keyof MarkdownStyle> = [];
 const excludeReportMentionStyle: Array<keyof MarkdownStyle> = ['mentionReport'];
@@ -305,10 +304,6 @@ function Composer({
         } as ComposerRef;
     }, [clear]);
 
-    useComposerHandle(ref, textInputRef, {
-        clear,
-    });
-
     const handleKeyPress = useCallback(
         (e: TextInputKeyPressEvent) => {
             // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed
diff --git a/src/components/Composer/useComposerHandle.ts b/src/components/Composer/useComposerHandle.ts
deleted file mode 100644
index 193f17d9eaee..000000000000
--- a/src/components/Composer/useComposerHandle.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {useImperativeHandle} from 'react';
-import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
-import type {ComposerRef} from './types';
-
-function useComposerHandle(ref: React.Ref<ComposerRef> | undefined, textInputRef: React.RefObject<AnimatedMarkdownTextInputRef | null>, additionalMethods: Partial<ComposerRef>) {
-    useImperativeHandle(ref, () => {
-        const textInput = textInputRef.current;
-        if (!textInput) {
-            throw new Error('textInput is not available. This should never happen and indicates a developer error.');
-        }
-
-        return getComposerHandle(textInput, additionalMethods);
-    }, [additionalMethods, textInputRef]);
-}
-
-function getComposerHandle(textInput: AnimatedMarkdownTextInputRef | null, additionalMethods: Partial<ComposerRef>) {
-    return {
-        ...textInput,
-        blur: () => textInput?.blur(),
-        focus: () => textInput?.focus(),
-        get scrollTop() {
-            return textInput?.scrollTop;
-        },
-        ...additionalMethods,
-    } as ComposerRef;
-}
-
-export default useComposerHandle;
-export {getComposerHandle};
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 54925af370fa..d6a24261af77 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -979,15 +979,30 @@ function ComposerWithSuggestions({
     useImperativeHandle(
         ref,
         () =>
-            ({
-                ...composerRef.current,
-                focus,
-                replaceSelectionWithText,
-                getCurrentText,
-                clearWorklet,
-                resetHeight,
-            }) as ComposerWithSuggestionsRef,
-        [focus, replaceSelectionWithText, clearWorklet, resetHeight, getCurrentText],
+            new Proxy(
+                {},
+                {
+                    get: (_target, prop) => {
+                        if (prop === 'focus') {
+                            return focus;
+                        }
+                        if (prop === 'replaceSelectionWithText') {
+                            return replaceSelectionWithText;
+                        }
+                        if (prop === 'getCurrentText') {
+                            return getCurrentText;
+                        }
+                        if (prop === 'clearWorklet') {
+                            return clearWorklet;
+                        }
+                        if (prop === 'resetHeight') {
+                            return resetHeight;
+                        }
+
+                        return composerRef.current?.[prop as keyof ComposerRef];
+                    },
+                },
+            ) as ComposerWithSuggestionsRef,
     );
 
     useEffect(() => {

From ba352b5f25c263e2d786387b581341fbefab5a58 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 24 Feb 2026 10:44:51 +0000
Subject: [PATCH 064/233] fix: remove `useOnyx` `canBeMissing` flag

---
 src/pages/inbox/report/ReportActionCompose/useEditMessage.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 4b3ea4bc59ca..dc0aa022f982 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -39,9 +39,9 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
     const isFocusedRef = useRef(isFocused);
 
     const {email} = useCurrentUserPersonalDetails();
-    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, {canBeMissing: true});
+    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`);
     const originalParentReportID = getOriginalReportID(originalReportID, reportAction, reportActions);
-    const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {canBeMissing: true});
+    const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`);
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
     const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
     const ancestors = useAncestors(originalReport);

From 32d196b35669f75d708184bdc25dc8eb343d9f9f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 12:48:25 +0000
Subject: [PATCH 065/233] refactor: remove unused gesture detector test id

---
 src/CONST/index.ts                                             | 1 -
 .../ReportActionCompose/ReportActionComposeSendButton.tsx      | 3 +--
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 45001dd83229..06350bea2d00 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1681,7 +1681,6 @@ const CONST = {
         MAX_LINES_FULL: -1,
         // The minimum height needed to enable the full screen composer
         FULL_COMPOSER_MIN_HEIGHT: 60,
-        SEND_BUTTON_TEST_ID: 'send-button',
     },
     MODAL: {
         MODAL_TYPE: {
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
index f25096e1f8e6..e3b07a1793c7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
@@ -30,8 +30,7 @@ function ReportActionComposeSendButton({isDisabled: isDisabledProp = false, isEd
         .onEnd(() => {
             onSend();
         })
-        .runOnJS(true)
-        .withTestId(CONST.COMPOSER.SEND_BUTTON_TEST_ID);
+        .runOnJS(true);
 
     const accessibilityLabel = translate(isEditing ? 'common.saveChanges' : 'common.send');
 

From 8ae2a531f3f38552f5a264a816ea4d5a167bf159 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:00:49 +0000
Subject: [PATCH 066/233] fix: duplicate click event

---
 .../report/ReportActionCompose/ReportActionComposeSendButton.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
index e3b07a1793c7..93061a49aab0 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
@@ -56,7 +56,6 @@ function ReportActionComposeSendButton({isDisabled: isDisabledProp = false, isEd
                     <SendOrSaveButton
                         isDisabled={isDisabledProp}
                         isEditing={isEditing}
-                        onSendOrSave={onSend}
                         accessible={false}
                         focusable={false}
                     />

From 21f3597e83186df9631acfb6942a80b872540b12 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:01:30 +0000
Subject: [PATCH 067/233] fix: don't update `isCommentEmpty` based on editing
 message

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 3d6c8163dd74..f340e3935c7d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -453,11 +453,6 @@ function ReportActionCompose({
         onComposerFocus?.();
     }, [onComposerFocus]);
 
-    useEffect(() => {
-        const valueToCheck = shouldUseNarrowLayout ? editingMessage : draftComment;
-        setIsCommentEmpty(!valueToCheck || !!valueToCheck.match(CONST.REGEX.EMPTY_COMMENT));
-    }, [editingMessage, draftComment, shouldUseNarrowLayout]);
-
     // We are returning a callback here as we want to invoke the method on unmount only
     useEffect(
         () => () => {

From b939f9c798338cbe0da5a2b00ae6ff4b9a85ad8a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:01:44 +0000
Subject: [PATCH 068/233] refactor: simplify condition

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx   | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index f340e3935c7d..8787f0edf191 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -473,8 +473,7 @@ function ReportActionCompose({
 
     const hasReportRecipient = !isEmptyObject(reportRecipient);
 
-    const isNewCommentEmpty = isCommentEmpty && !isEditingInComposer;
-    const isSendDisabled = !isEditingInComposer && (isBlockedFromConcierge || isExceedingMaxLength || isNewCommentEmpty);
+    const isSendDisabled = !isEditingInComposer && (isBlockedFromConcierge || isExceedingMaxLength || isCommentEmpty);
 
     // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value
     // useSharedValue on web doesn't support functions, so we need to wrap it in an object.

From 7a1bba6ac91b8bf847354ad52163a6cf72f0166c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:03:45 +0000
Subject: [PATCH 069/233] fix: don't re-mount composer based on editing state

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx     | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 8787f0edf191..28f9d9232c9c 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -642,7 +642,6 @@ function ReportActionCompose({
                             onValueChange={onValueChange}
                             didHideComposerInput={didHideComposerInput}
                             forwardedFSClass={fsClass}
-                            // key={editingReportActionID}
                         />
                         {shouldDisplayDualDropZone && (
                             <DualDropZone

From 8de4c4c5dbb0c1b8ffb24020ecb0c42af80ff2bd Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:09:10 +0000
Subject: [PATCH 070/233] fix: invalid array index on object and refactor

---
 .../ReportActionCompose/ReportActionCompose.tsx     | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 28f9d9232c9c..c4104f95bcc5 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -171,15 +171,8 @@ function ReportActionCompose({
 
     const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID;
 
-    const isEditingLastReportAction = useMemo(() => {
-        if (!reportActions) {
-            return false;
-        }
-
-        const lastIndex = Object.keys(reportActions).length - 1;
-
-        return editingReportActionID === reportActions[lastIndex]?.reportActionID;
-    }, [editingReportActionID, reportActions]);
+    const reportActionEntries = useMemo(() => (reportActions ? Object.entries(reportActions) : []), [reportActions]);
+    const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionEntries.at(-1)?.[0], [editingReportActionID, reportActionEntries]);
 
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
 
@@ -242,7 +235,7 @@ function ReportActionCompose({
 
     const personalDetail = useCurrentUserPersonalDetails();
 
-    const iouAction = reportActions ? Object.values(reportActions).find((action) => isMoneyRequestAction(action)) : null;
+    const iouAction = reportActionEntries.find(([, action]) => isMoneyRequestAction(action))?.[1];
     const linkedTransactionID = iouAction && !isExpensesReport ? getLinkedTransactionID(iouAction) : undefined;
 
     const transactionID = useMemo(() => getTransactionID(report) ?? linkedTransactionID, [report, linkedTransactionID]);

From dda59f73e5290d58152dc98363e40e83c0fbe02f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:13:46 +0000
Subject: [PATCH 071/233] fix: Composer imports

---
 .../Composer/implementation/index.tsx         | 34 +++++++++----------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 1daf65b2e916..b3953bc2401d 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -5,23 +5,23 @@ import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, use
 // eslint-disable-next-line no-restricted-imports
 import type {TextInputKeyPressEvent, TextInputSelectionChangeEvent} from 'react-native';
 import {DeviceEventEmitter, StyleSheet} from 'react-native';
-import type {ComposerProps, ComposerRef} from './src/components/Composer/types';
-import {useSession} from './src/components/OnyxListItemProvider';
-import type {AnimatedMarkdownTextInputRef} from './src/components/RNMarkdownTextInput';
-import RNMarkdownTextInput from './src/components/RNMarkdownTextInput';
-import CONST from './src/CONST';
-import useHtmlPaste from './src/hooks/useHtmlPaste';
-import useIsScrollBarVisible from './src/hooks/useIsScrollBarVisible';
-import useMarkdownStyle from './src/hooks/useMarkdownStyle';
-import useStyleUtils from './src/hooks/useStyleUtils';
-import useTheme from './src/hooks/useTheme';
-import useThemeStyles from './src/hooks/useThemeStyles';
-import addEncryptedAuthTokenToURL from './src/libs/addEncryptedAuthTokenToURL';
-import {isMobileSafari, isSafari} from './src/libs/Browser';
-import {containsOnlyEmojis} from './src/libs/EmojiUtils';
-import {base64ToFile} from './src/libs/fileDownload/FileUtils';
-import isEnterWhileComposition from './src/libs/KeyboardShortcut/isEnterWhileComposition';
-import Parser from './src/libs/Parser';
+import type {ComposerProps} from '@components/Composer/types';
+import {useSession} from '@components/OnyxListItemProvider';
+import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
+import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
+import useHtmlPaste from '@hooks/useHtmlPaste';
+import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible';
+import useMarkdownStyle from '@hooks/useMarkdownStyle';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
+import {isMobileSafari, isSafari} from '@libs/Browser';
+import {containsOnlyEmojis} from '@libs/EmojiUtils';
+import {base64ToFile} from '@libs/fileDownload/FileUtils';
+import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
+import Parser from '@libs/Parser';
+import CONST from '@src/CONST';
 
 const excludeNoStyles: Array<keyof MarkdownStyle> = [];
 const excludeReportMentionStyle: Array<keyof MarkdownStyle> = ['mentionReport'];

From f8815c8e3e646d620f4f59b01973bb78997416c2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:24:39 +0000
Subject: [PATCH 072/233] fix: prettier

---
 .../inbox/report/ReportActionCompose/SendOrSaveButton.tsx   | 2 +-
 .../inbox/report/ReportActionCompose/useEditMessage.ts      | 2 +-
 .../inbox/report/useDraftMessageVideoAttributeCache.ts      | 6 +++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx b/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
index 3128e73d0739..6ed69876731f 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
@@ -4,11 +4,11 @@ import * as Expensicons from '@components/Icon/Expensicons';
 import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
 import type {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback';
 import Tooltip from '@components/Tooltip';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 
 type SendOrSaveButtonProps = PressableWithFeedbackProps & {
     /** Whether the button is disabled */
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index dc0aa022f982..893fa006a818 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -16,10 +16,10 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag
 import {getOriginalReportID} from '@libs/ReportUtils';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
 import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
+import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 import KeyboardUtils from '@src/utils/keyboard';
-import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 
 type UseEditMessageProps = {
     reportID: string | undefined;
diff --git a/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts b/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts
index 95c19ee15530..8124ec8b2378 100644
--- a/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts
+++ b/src/pages/inbox/report/useDraftMessageVideoAttributeCache.ts
@@ -1,9 +1,9 @@
-import Parser from '@libs/Parser';
-import {getReportActionHtml, isDeletedAction} from '@libs/ReportActionsUtils';
 import type React from 'react';
 import {useEffect} from 'react';
-import type * as OnyxTypes from '@src/types/onyx';
 import usePrevious from '@hooks/usePrevious';
+import Parser from '@libs/Parser';
+import {getReportActionHtml, isDeletedAction} from '@libs/ReportActionsUtils';
+import type * as OnyxTypes from '@src/types/onyx';
 
 type DraftMessageVideoAttributeCache = Map<string, string>;
 

From eed0b024678256aaeb6df884843dc76c93e4911d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:39:07 +0000
Subject: [PATCH 073/233] fix: ComposerRef import

---
 src/components/Composer/implementation/index.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index b3953bc2401d..2d82c05ab3ab 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, use
 // eslint-disable-next-line no-restricted-imports
 import type {TextInputKeyPressEvent, TextInputSelectionChangeEvent} from 'react-native';
 import {DeviceEventEmitter, StyleSheet} from 'react-native';
-import type {ComposerProps} from '@components/Composer/types';
+import type {ComposerProps, ComposerRef} from '@components/Composer/types';
 import {useSession} from '@components/OnyxListItemProvider';
 import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
 import RNMarkdownTextInput from '@components/RNMarkdownTextInput';

From 9c39ab6d0df2d1c7650cde1e38b2c7695280f0a9 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:51:33 +0000
Subject: [PATCH 074/233] fix: editing main thread message

---
 src/pages/inbox/report/PureReportActionItem.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 672ba805be48..856a51bd744e 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -637,7 +637,7 @@ function PureReportActionItem({
             clearError(transactionID);
         }
         clearAllRelatedReportActionErrors(reportID, action, originalReportID);
-    }, [action, isSendingMoney, reportID, clearAllRelatedReportActionErrors, report, chatReport, clearError]);
+    }, [action, isSendingMoney, reportID, clearAllRelatedReportActionErrors, originalReportID, report, chatReport, clearError]);
 
     const showDismissReceiptErrorModal = useCallback(async () => {
         const result = await showConfirmModal({
@@ -1835,7 +1835,7 @@ function PureReportActionItem({
                 ?.split(',')
                 .map((accountID) => Number(accountID))
                 .filter((accountID): accountID is number => typeof accountID === 'number') ?? [];
-        const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {};
+        const draftMessageRightAlign = isEditingInline ? styles.chatItemReactionsDraftRight : {};
 
         const itemContent = (
             <>

From 6578581daae4f22ab163a18bdf8491f44ea4b45c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:55:21 +0000
Subject: [PATCH 075/233] fix: remove duplicate `deleteDraft` call

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx    | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index c4104f95bcc5..7424aec467c7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -359,7 +359,6 @@ function ReportActionCompose({
 
             if (isEditingInComposer && !attachmentFileRef.current) {
                 publishDraft(draftMessageTrimmed);
-                deleteDraft();
                 return;
             }
 
@@ -407,7 +406,6 @@ function ReportActionCompose({
             isEditingInComposer,
             isConciergeChat,
             publishDraft,
-            deleteDraft,
             kickoffWaitingIndicator,
             transactionThreadReport,
             report,

From d9139cea012045261d4504ef6ada2a7be22001bd Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 13:55:53 +0000
Subject: [PATCH 076/233] fix: prevent value changes when comment/draft has
 been submitted

---
 .../ComposerWithSuggestions/ComposerWithSuggestions.tsx     | 6 +++++-
 .../inbox/report/ReportActionCompose/useEditMessage.ts      | 4 ++--
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index d7232395ff23..f1f206b99f11 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -313,6 +313,10 @@ function ComposerWithSuggestions({
     );
 
     useEffect(() => {
+        if (didSubmitEditRef.current) {
+            return;
+        }
+
         const isEditing = !!editingReportActionID;
         const previousEditingReportActionID = previousEditingReportActionIDRef.current;
         const currentEditingReportActionID = editingReportActionID ?? null;
@@ -370,7 +374,7 @@ function ComposerWithSuggestions({
             const nextValue = editingMessage ?? '';
             applyComposerValue(nextValue, false);
         }
-    }, [applyComposerValue, currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
+    }, [applyComposerValue, currentEditMessageSelection, didSubmitEditRef, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 893fa006a818..cda305573d35 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -67,8 +67,6 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        didSubmitEditRef.current = true;
-
         deleteReportActionDraft(reportID, reportAction);
 
         if (isActive()) {
@@ -100,6 +98,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
+        didSubmitEditRef.current = true;
+
         const trimmedNewDraft = draftMessage.trim();
 
         // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.

From b3901c3311717edae7015edeaf66f5f0b68512a6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 14:04:06 +0000
Subject: [PATCH 077/233] fix: remove `canBeMissing` flag

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 9d6c07c67656..b9bd5b6a5a32 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -35,9 +35,8 @@ type ReportActionEditMessageContextProviderProps = {
 function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
-        canBeMissing: true,
     });
-    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {canBeMissing: true});
+    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
 
     const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
     const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);

From 85892fa21ccfe37fee450a64e43b50225585250d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Feb 2026 14:11:06 +0000
Subject: [PATCH 078/233] fix: remove unused import

---
 .../ReportActionCompose/ReportActionComposeSendButton.tsx       | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
index e21bc825ef87..93061a49aab0 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeSendButton.tsx
@@ -1,7 +1,6 @@
 import React, {memo} from 'react';
 import {View} from 'react-native';
 import {Gesture, GestureDetector} from 'react-native-gesture-handler';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -26,7 +25,6 @@ function ReportActionComposeSendButton({isDisabled: isDisabledProp = false, isEd
     // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to manage GestureDetector correctly
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
     const {isSmallScreenWidth} = useResponsiveLayout();
-    const icons = useMemoizedLazyExpensifyIcons(['Send'] as const);
     const Tap = Gesture.Tap()
         .enabled(!isDisabledProp)
         .onEnd(() => {

From 37dfe5637ca5cf4947eae6dae3c4aee922a641a4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 14:29:20 +0000
Subject: [PATCH 079/233] refactor: remove unnecessary nested folder

---
 .../{ComposerWithSuggestions => }/ComposerWithSuggestions.tsx  | 0
 .../ReportActionCompose/ComposerWithSuggestions/index.tsx      | 3 ---
 2 files changed, 3 deletions(-)
 rename src/pages/inbox/report/ReportActionCompose/{ComposerWithSuggestions => }/ComposerWithSuggestions.tsx (100%)
 delete mode 100644 src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
similarity index 100%
rename from src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
rename to src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx
deleted file mode 100644
index f2aebd390ba6..000000000000
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import ComposerWithSuggestions from './ComposerWithSuggestions';
-
-export default ComposerWithSuggestions;

From b9c7105ebdfea8e5537d45cd4f5b6bb34d640890 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 14:43:04 +0000
Subject: [PATCH 080/233] refactor: update `ComposerWithSuggestions` import

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index a993ccca21aa..f95d8c1d19e1 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -72,7 +72,7 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
-import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
 import MessageEditCancelButton from './MessageEditCancelButton';
 import ReportActionComposeSendButton from './ReportActionComposeSendButton';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';

From 471fa77b17c3c2217bf4faa696d62e0e78f1663d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 14:43:41 +0000
Subject: [PATCH 081/233] refactor: import order

---
 .../ReportActionCompose/ComposerWithSuggestions.tsx    | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index f1f206b99f11..d9779a0e4057 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -38,11 +38,6 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag
 import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUtils';
 import updateMultilineInputRange from '@libs/updateMultilineInputRange';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
-import getCursorPosition from '@pages/inbox/report/ReportActionCompose/getCursorPosition';
-import getScrollPosition from '@pages/inbox/report/ReportActionCompose/getScrollPosition';
-import type {SuggestionsRef} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import SilentCommentUpdater from '@pages/inbox/report/ReportActionCompose/SilentCommentUpdater';
-import Suggestions from '@pages/inbox/report/ReportActionCompose/Suggestions';
 import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import useDraftMessageVideoAttributeCache from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
@@ -58,6 +53,11 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
+import getCursorPosition from './getCursorPosition';
+import getScrollPosition from './getScrollPosition';
+import type {SuggestionsRef} from './ReportActionCompose';
+import SilentCommentUpdater from './SilentCommentUpdater';
+import Suggestions from './Suggestions';
 
 type SyncSelection = {
     position: number;

From 400a1e1ede3098f7f564b9c5bed68ec36db1749d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 14:44:02 +0000
Subject: [PATCH 082/233] fix: improve editing state ref

---
 .../ComposerWithSuggestions.tsx               | 16 +++++-----
 .../ReportActionCompose/useEditMessage.ts     |  6 ++--
 .../report/ReportActionEditMessageContext.tsx | 29 ++++++++++++++-----
 3 files changed, 33 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index d9779a0e4057..1460a4d6637a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -261,7 +261,7 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, didSubmitEditRef} = useReportActionActiveEdit();
+    const {editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, getEditingState} = useReportActionActiveEdit();
 
     const [value, setValue] = useState(() => {
         const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
@@ -313,7 +313,7 @@ function ComposerWithSuggestions({
     );
 
     useEffect(() => {
-        if (didSubmitEditRef.current) {
+        if (getEditingState() === 'submitted') {
             return;
         }
 
@@ -374,7 +374,7 @@ function ComposerWithSuggestions({
             const nextValue = editingMessage ?? '';
             applyComposerValue(nextValue, false);
         }
-    }, [applyComposerValue, currentEditMessageSelection, didSubmitEditRef, draftComment, editingMessage, editingReportActionID, shouldUseNarrowLayout]);
+    }, [applyComposerValue, currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, getEditingState, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -602,7 +602,7 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            if (!!editingReportActionID && shouldUseNarrowLayout && !didSubmitEditRef.current) {
+            if (!!editingReportActionID && shouldUseNarrowLayout && !getEditingState()) {
                 if (shouldDebounceSaveComment) {
                     isDraftPendingSaved.current = true;
                     debouncedSaveDraft(newCommentConverted);
@@ -631,17 +631,17 @@ function ComposerWithSuggestions({
             findNewlyAddedChars,
             preferredSkinTone,
             preferredLocale,
+            editingReportActionID,
             shouldUseNarrowLayout,
+            getEditingState,
             suggestionsRef,
             setIsCommentEmpty,
             setCurrentEditMessageSelection,
             currentEditMessageSelection,
-            editingReportActionID,
-            didSubmitEditRef,
-            debouncedSaveDraft,
             reportID,
-            currentUserAccountID,
+            debouncedSaveDraft,
             debouncedSaveReportComment,
+            currentUserAccountID,
         ],
     );
 
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 65262f917a4e..c6bd16a5e6a1 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -47,7 +47,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
     const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
     const ancestors = useAncestors(originalReport);
 
-    const {didSubmitEditRef} = useReportActionActiveEdit();
+    const {setEditingState} = useReportActionActiveEdit();
 
     useEffect(() => {
         // required for keeping last state of isFocused variable
@@ -68,6 +68,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
+        setEditingState('cancelled');
+
         deleteReportActionDraft(reportID, reportAction);
 
         if (isActive()) {
@@ -99,7 +101,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        didSubmitEditRef.current = true;
+        setEditingState('submitted');
 
         const trimmedNewDraft = draftMessage.trim();
 
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index b9bd5b6a5a32..3abadc859973 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -4,7 +4,11 @@ import useOnyx from '@hooks/useOnyx';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 
-const NOOP = () => {};
+function NOOP() {
+    return null;
+}
+
+type EditingState = 'editing' | 'submitted' | 'cancelled';
 
 type ReportActionActiveEdit = {
     editingReportActionID: string | null;
@@ -15,7 +19,8 @@ type ReportActionActiveEdit = {
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
     currentEditMessageSelection: TextSelection | null;
     setCurrentEditMessageSelection: (selection: TextSelection) => void;
-    didSubmitEditRef: React.RefObject<boolean | null>;
+    getEditingState: () => EditingState | null;
+    setEditingState: (state: EditingState | null) => void;
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
@@ -24,7 +29,8 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingMessage: null,
     currentEditMessageSelection: null,
     setCurrentEditMessageSelection: NOOP,
-    didSubmitEditRef: {current: null},
+    getEditingState: NOOP,
+    setEditingState: NOOP,
 });
 
 type ReportActionEditMessageContextProviderProps = {
@@ -42,7 +48,13 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
     const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
-    const didSubmitEditRef = useRef<boolean | null>(null);
+    const editingStateRef = useRef<EditingState | null>(null);
+    const getEditingState = () => {
+        return editingStateRef.current;
+    };
+    const setEditingState = (state: EditingState | null) => {
+        editingStateRef.current = state;
+    };
 
     const updateActiveEditState = useCallback(
         (activeEdit: ReportActionActiveEdit | null) => {
@@ -75,11 +87,11 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     );
 
     const reset = useCallback(() => {
-        if (didSubmitEditRef.current === false) {
+        if (editingStateRef.current === 'editing') {
             return;
         }
 
-        didSubmitEditRef.current = null;
+        editingStateRef.current = null;
         updateActiveEditState(null);
         setCurrentEditMessageSelection(null);
     }, [updateActiveEditState, setCurrentEditMessageSelection]);
@@ -103,7 +115,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
         const [reportActionID, draft] = reportDraftEntry;
 
-        didSubmitEditRef.current = false;
+        editingStateRef.current = 'editing';
         updateActiveEditState({
             editingReportActionID: reportActionID,
             editingReportAction: reportActions?.[reportActionID] ?? null,
@@ -120,7 +132,8 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
                 editingMessage,
                 currentEditMessageSelection,
                 setCurrentEditMessageSelection,
-                didSubmitEditRef,
+                getEditingState,
+                setEditingState,
             }}
         >
             {children}

From 65ee2fc82f2c04a018f5e1a13dad03bbaa9c19cb Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 20:36:11 +0000
Subject: [PATCH 083/233] fix: inconsistent state naming in
 `ComposerWithSuggestions`

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 1460a4d6637a..c24f47bbdc2d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -398,7 +398,7 @@ function ComposerWithSuggestions({
     const valueRef = useRef(value);
     valueRef.current = value;
 
-    const [composerHeightAfterClear, setDefaultComposerHeight] = useState<number | null>(null);
+    const [composerHeightAfterClear, setComposerHeightAfterClear] = useState<number | null>(null);
     const emptyComposerHeightRef = useRef<number | null>(null);
 
     const syncSelectionWithOnChangeTextRef = useRef<SyncSelection | null>(null);
@@ -743,7 +743,7 @@ function ComposerWithSuggestions({
         if (composerHeightAfterClear == null) {
             return;
         }
-        setDefaultComposerHeight(null);
+        setComposerHeightAfterClear(null);
     }, [composerHeightAfterClear]);
 
     const onChangeText = useCallback(
@@ -911,7 +911,7 @@ function ComposerWithSuggestions({
         if (!emptyComposerHeightRef.current) {
             return;
         }
-        setDefaultComposerHeight(emptyComposerHeightRef.current);
+        setComposerHeightAfterClear(emptyComposerHeightRef.current);
     }, []);
 
     const getCurrentText = useCallback(() => {

From b450e88339f023790473dbbf6386179acabfa042 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 20:36:32 +0000
Subject: [PATCH 084/233] fix: don't clear composer if editing and draftComment
 is set

---
 .../ReportActionCompose/ReportActionCompose.tsx     | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index f95d8c1d19e1..ee2e55f5f2c1 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -171,6 +171,7 @@ function ReportActionCompose({
     const {editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID;
+    const effectiveDraft = shouldUseNarrowLayout ? editingMessage : draftComment;
 
     const reportActionEntries = useMemo(() => (reportActions ? Object.entries(reportActions) : []), [reportActions]);
     const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionEntries.at(-1)?.[0], [editingReportActionID, reportActionEntries]);
@@ -192,8 +193,6 @@ function ReportActionCompose({
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
-    const effectiveDraft = shouldUseNarrowLayout ? editingMessage : draftComment;
-
     const [isCommentEmpty, setIsCommentEmpty] = useState(() => {
         return !effectiveDraft || !!effectiveDraft.match(CONST.REGEX.EMPTY_COMMENT);
     });
@@ -470,11 +469,17 @@ function ReportActionCompose({
             return;
         }
 
-        composerRef.current?.resetHeight();
         if (isComposerFullSize) {
             setIsComposerFullSize(reportID, false);
         }
 
+        if (isEditingInComposer && effectiveDraft && draftComment) {
+            submitForm(effectiveDraft);
+            return;
+        }
+
+        composerRef.current?.resetHeight();
+
         scheduleOnUI(() => {
             const {clearWorklet} = composerRefShared.get();
 
@@ -484,7 +489,7 @@ function ReportActionCompose({
 
             clearWorklet?.();
         });
-    }, [isSendDisabled, debouncedCommentMaxLengthValidation, isComposerFullSize, reportID, composerRefShared]);
+    }, [isSendDisabled, debouncedCommentMaxLengthValidation, isComposerFullSize, isEditingInComposer, effectiveDraft, draftComment, reportID, submitForm, composerRefShared]);
     onSubmitAction = sendMessage;
 
     const emojiPositionValues = useMemo(

From 6db8fba34af92aa7ce553bee02c4e63a81e29514 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 21:15:55 +0000
Subject: [PATCH 085/233] fix: edit draft not being saved

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index c24f47bbdc2d..a203ae7d5c1e 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -602,7 +602,7 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            if (!!editingReportActionID && shouldUseNarrowLayout && !getEditingState()) {
+            if (!!editingReportActionID && shouldUseNarrowLayout && getEditingState() !== 'submitted') {
                 if (shouldDebounceSaveComment) {
                     isDraftPendingSaved.current = true;
                     debouncedSaveDraft(newCommentConverted);

From 340b98aa48a9fb2e25fa994ba5fe17dda5e57b99 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 22:07:38 +0000
Subject: [PATCH 086/233] fix: manually set the editing report action without
 debounce

---
 .../ComposerWithSuggestions.tsx               | 34 +++++++++++--------
 .../report/ReportActionEditMessageContext.tsx | 11 ++++--
 .../report/ReportActionItemMessageEdit.tsx    |  6 ++--
 3 files changed, 32 insertions(+), 19 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index a203ae7d5c1e..f7899f1ecbb8 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -261,7 +261,8 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, getEditingState} = useReportActionActiveEdit();
+    const {editingReportActionID, editingReportAction, editingMessage, setEditingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, getEditingState} =
+        useReportActionActiveEdit();
 
     const [value, setValue] = useState(() => {
         const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
@@ -602,7 +603,9 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            if (!!editingReportActionID && shouldUseNarrowLayout && getEditingState() !== 'submitted') {
+            const editingState = getEditingState();
+            if (editingState === 'editing' && shouldUseNarrowLayout) {
+                setEditingMessage(newCommentConverted);
                 if (shouldDebounceSaveComment) {
                     isDraftPendingSaved.current = true;
                     debouncedSaveDraft(newCommentConverted);
@@ -625,23 +628,24 @@ function ComposerWithSuggestions({
             }
         },
         [
-            raiseIsScrollLikelyLayoutTriggered,
-            selection?.start,
-            selection.end,
+            currentEditMessageSelection,
+            currentUserAccountID,
+            debouncedSaveDraft,
+            debouncedSaveReportComment,
+            editingReportActionID,
             findNewlyAddedChars,
-            preferredSkinTone,
+            getEditingState,
             preferredLocale,
-            editingReportActionID,
+            preferredSkinTone,
+            raiseIsScrollLikelyLayoutTriggered,
+            reportID,
+            selection.end,
+            selection?.start,
+            setCurrentEditMessageSelection,
+            setEditingMessage,
+            setIsCommentEmpty,
             shouldUseNarrowLayout,
-            getEditingState,
             suggestionsRef,
-            setIsCommentEmpty,
-            setCurrentEditMessageSelection,
-            currentEditMessageSelection,
-            reportID,
-            debouncedSaveDraft,
-            debouncedSaveReportComment,
-            currentUserAccountID,
         ],
     );
 
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 3abadc859973..eb1f73f215bf 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -17,6 +17,7 @@ type ReportActionActiveEdit = {
 };
 
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
+    setEditingMessage: (message: string | null) => void;
     currentEditMessageSelection: TextSelection | null;
     setCurrentEditMessageSelection: (selection: TextSelection) => void;
     getEditingState: () => EditingState | null;
@@ -27,6 +28,7 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingReportActionID: null,
     editingReportAction: null,
     editingMessage: null,
+    setEditingMessage: NOOP,
     currentEditMessageSelection: null,
     setCurrentEditMessageSelection: NOOP,
     getEditingState: NOOP,
@@ -96,7 +98,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         setCurrentEditMessageSelection(null);
     }, [updateActiveEditState, setCurrentEditMessageSelection]);
 
-    // Set the active edit when the report actions or draft comments change
+    // Initially set the editing report action state when the draft comments change
     useEffect(() => {
         const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
@@ -106,7 +108,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
             return;
         }
 
-        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message);
+        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message !== undefined);
 
         if (!reportDraftEntry) {
             reset();
@@ -115,6 +117,10 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
         const [reportActionID, draft] = reportDraftEntry;
 
+        if (editingStateRef.current !== null) {
+            return;
+        }
+
         editingStateRef.current = 'editing';
         updateActiveEditState({
             editingReportActionID: reportActionID,
@@ -130,6 +136,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
                 editingReportActionID,
                 editingReportAction,
                 editingMessage,
+                setEditingMessage,
                 currentEditMessageSelection,
                 setCurrentEditMessageSelection,
                 getEditingState,
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 3f1ec47a7f7f..60e99366e00c 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -117,7 +117,7 @@ function ReportActionItemMessageEdit({
         return draftMessage;
     });
 
-    const {currentEditMessageSelection, setCurrentEditMessageSelection} = useReportActionActiveEdit();
+    const {currentEditMessageSelection, setCurrentEditMessageSelection, setEditingMessage} = useReportActionActiveEdit();
 
     const defaultSelection = useMemo(() => ({start: draft.length, end: draft.length, positionX: 0, positionY: 0}), [draft.length]);
     const [selection, setSelectionState] = useState<TextSelection>(() => currentEditMessageSelection ?? defaultSelection);
@@ -251,11 +251,13 @@ function ReportActionItemMessageEdit({
 
             draftRef.current = newDraft;
 
+            setEditingMessage(newDraft);
+
             // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted.
             debouncedSaveDraft(newDraft);
             isCommentPendingSaved.current = true;
         },
-        [debouncedSaveDraft, preferredLocale, preferredSkinTone, raiseIsScrollLayoutTriggered, selection?.end, setSelection],
+        [debouncedSaveDraft, preferredLocale, preferredSkinTone, raiseIsScrollLayoutTriggered, selection?.end, setEditingMessage, setSelection],
     );
 
     useEffect(() => {

From 22a579da64cff41830df993a949a6989932096b1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 22:24:54 +0000
Subject: [PATCH 087/233] refactor: use editing state ref

---
 .../ReportActionCompose/ComposerWithSuggestions.tsx   | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index f7899f1ecbb8..5d8a9ed64247 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -276,9 +276,10 @@ function ComposerWithSuggestions({
     // The ref to check whether the comment saving is in progress
     const isDraftPendingSaved = useRef(false);
 
+    const editingState = getEditingState();
     useDraftMessageVideoAttributeCache({
         draftMessage: value,
-        isEditing: !!editingReportActionID,
+        isEditing: editingState === 'editing',
         editingReportAction,
         updateDraftMessage: setValue,
         isEditInProgressRef: isDraftPendingSaved,
@@ -290,7 +291,7 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
-    const wasEditing = useRef(!!editingReportActionID);
+    const wasEditing = useRef(editingState === 'editing');
 
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousEditingReportActionIDRef = useRef<string | null>(editingReportActionID ?? null);
@@ -318,7 +319,7 @@ function ComposerWithSuggestions({
             return;
         }
 
-        const isEditing = !!editingReportActionID;
+        const isEditing = currentEditingState === 'editing';
         const previousEditingReportActionID = previousEditingReportActionIDRef.current;
         const currentEditingReportActionID = editingReportActionID ?? null;
         const didChangeEditedAction = isEditing && previousEditingReportActionID && currentEditingReportActionID && previousEditingReportActionID !== currentEditingReportActionID;
@@ -603,8 +604,8 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            const editingState = getEditingState();
-            if (editingState === 'editing' && shouldUseNarrowLayout) {
+            const currentEditingState = getEditingState();
+            if (currentEditingState === 'editing' && shouldUseNarrowLayout) {
                 setEditingMessage(newCommentConverted);
                 if (shouldDebounceSaveComment) {
                     isDraftPendingSaved.current = true;

From ff433aa9cc86d01b31e3528c888f4487a1757589 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 22:26:58 +0000
Subject: [PATCH 088/233] refactor: use state instead of ref in render

---
 .../ComposerWithSuggestions.tsx                    | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 5d8a9ed64247..34be98ff9ac1 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -276,10 +276,9 @@ function ComposerWithSuggestions({
     // The ref to check whether the comment saving is in progress
     const isDraftPendingSaved = useRef(false);
 
-    const editingState = getEditingState();
     useDraftMessageVideoAttributeCache({
         draftMessage: value,
-        isEditing: editingState === 'editing',
+        isEditing: !!editingReportActionID,
         editingReportAction,
         updateDraftMessage: setValue,
         isEditInProgressRef: isDraftPendingSaved,
@@ -291,7 +290,7 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
-    const wasEditing = useRef(editingState === 'editing');
+    const wasEditing = useRef(!!editingReportActionID);
 
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousEditingReportActionIDRef = useRef<string | null>(editingReportActionID ?? null);
@@ -315,11 +314,12 @@ function ComposerWithSuggestions({
     );
 
     useEffect(() => {
-        if (getEditingState() === 'submitted') {
+        const editingState = getEditingState();
+        if (editingState === 'submitted') {
             return;
         }
 
-        const isEditing = currentEditingState === 'editing';
+        const isEditing = editingState === 'editing';
         const previousEditingReportActionID = previousEditingReportActionIDRef.current;
         const currentEditingReportActionID = editingReportActionID ?? null;
         const didChangeEditedAction = isEditing && previousEditingReportActionID && currentEditingReportActionID && previousEditingReportActionID !== currentEditingReportActionID;
@@ -604,8 +604,8 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            const currentEditingState = getEditingState();
-            if (currentEditingState === 'editing' && shouldUseNarrowLayout) {
+            const editingState = getEditingState();
+            if (editingState === 'editing' && shouldUseNarrowLayout) {
                 setEditingMessage(newCommentConverted);
                 if (shouldDebounceSaveComment) {
                     isDraftPendingSaved.current = true;

From 6bad09be1111bbe19e05463940be3ba03672c244 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 22:27:20 +0000
Subject: [PATCH 089/233] refactor: pass along set state action

---
 .../ComposerWithSuggestions.tsx               | 21 ++++++++++---------
 .../report/ReportActionEditMessageContext.tsx |  9 ++++----
 2 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 34be98ff9ac1..8de45e28be72 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -376,7 +376,7 @@ function ComposerWithSuggestions({
             const nextValue = editingMessage ?? '';
             applyComposerValue(nextValue, false);
         }
-    }, [applyComposerValue, currentEditMessageSelection, draftComment, editingMessage, editingReportActionID, getEditingState, shouldUseNarrowLayout]);
+    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, getEditingState, shouldUseNarrowLayout]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -600,7 +600,7 @@ function ComposerWithSuggestions({
                     positionY: prevSelection.positionY,
                 }));
 
-                setCurrentEditMessageSelection({...currentEditMessageSelection, start: position, end: position});
+                setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: position, end: position}));
             }
 
             commentRef.current = newCommentConverted;
@@ -629,7 +629,6 @@ function ComposerWithSuggestions({
             }
         },
         [
-            currentEditMessageSelection,
             currentUserAccountID,
             debouncedSaveDraft,
             debouncedSaveReportComment,
@@ -719,7 +718,8 @@ function ComposerWithSuggestions({
                         positionY: prevSelection.positionY,
                     }));
 
-                    setCurrentEditMessageSelection({...currentEditMessageSelection, start: newStart, end: newEnd});
+                    setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: newStart, end: newEnd}));
+
                     updateComment(newText, true);
                 }
             }
@@ -734,9 +734,8 @@ function ComposerWithSuggestions({
             onEnterKeyPress,
             lastReportAction,
             reportID,
-            setCurrentEditMessageSelection,
-            currentEditMessageSelection,
             updateComment,
+            setCurrentEditMessageSelection,
         ],
     );
 
@@ -779,12 +778,13 @@ function ComposerWithSuggestions({
         (e: CustomSelectionChangeEvent) => {
             setSelection(e.nativeEvent.selection);
 
-            setCurrentEditMessageSelection({
+            setCurrentEditMessageSelection((prevSelection) => ({
+                ...prevSelection,
                 start: e.nativeEvent.selection.start,
                 end: e.nativeEvent.selection.end,
                 positionX: 0,
                 positionY: 0,
-            });
+            }));
 
             if (!composerRef.current?.isFocused()) {
                 return;
@@ -1102,12 +1102,13 @@ function ComposerWithSuggestions({
         (suggestionSelection: TextSelection) => {
             const endOfSuggestionSelection = suggestionSelection.end;
             setSelection(suggestionSelection);
-            setCurrentEditMessageSelection({
+            setCurrentEditMessageSelection((prevSelection) => ({
+                ...prevSelection,
                 start: suggestionSelection.start,
                 end: suggestionSelection.end,
                 positionX: 0,
                 positionY: 0,
-            });
+            }));
 
             if (endOfSuggestionSelection === undefined) {
                 return;
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index eb1f73f215bf..7ebd71c8de1a 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,5 @@
 import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from 'react';
+import type {Dispatch, SetStateAction} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -17,9 +18,9 @@ type ReportActionActiveEdit = {
 };
 
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
-    setEditingMessage: (message: string | null) => void;
+    setEditingMessage: Dispatch<SetStateAction<string | null>>;
     currentEditMessageSelection: TextSelection | null;
-    setCurrentEditMessageSelection: (selection: TextSelection) => void;
+    setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
     getEditingState: () => EditingState | null;
     setEditingState: (state: EditingState | null) => void;
 };
@@ -78,12 +79,12 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     );
 
     const setCurrentEditMessageSelection = useCallback(
-        (selection: TextSelection | null) => {
+        (setSelectionStateAction: SetStateAction<TextSelection | null>) => {
             if (!editingReportActionID) {
                 return;
             }
 
-            setCurrentEditMessageSelectionState(selection);
+            setCurrentEditMessageSelectionState(setSelectionStateAction);
         },
         [editingReportActionID],
     );

From db08232ad6280643d7814b006a88490af986a1bb Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 22:27:33 +0000
Subject: [PATCH 090/233] refactor: unnecessary check for editing state

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx   | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 8de45e28be72..c13e18f6bfaf 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -463,13 +463,10 @@ function ComposerWithSuggestions({
     const debouncedSaveReportComment = useMemo(
         () =>
             lodashDebounce((selectedReportID: string, newComment: string | null) => {
-                if (editingReportActionID) {
-                    return;
-                }
                 saveReportDraftComment(selectedReportID, newComment);
                 isCommentPendingSaved.current = false;
             }, 1000),
-        [editingReportActionID],
+        [],
     );
 
     useEffect(() => {

From 1a44924db45b2f1e76f390e7030a047e06542c80 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 22:31:29 +0000
Subject: [PATCH 091/233] fix: selection issues

---
 .../ReportActionCompose/ComposerWithSuggestions.tsx    | 10 +++-------
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx |  2 +-
 2 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index c13e18f6bfaf..db46cc1c36af 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -591,10 +591,9 @@ function ComposerWithSuggestions({
                 }
 
                 setSelection((prevSelection) => ({
+                    ...prevSelection,
                     start: position,
                     end: position,
-                    positionX: prevSelection.positionX,
-                    positionY: prevSelection.positionY,
                 }));
 
                 setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: position, end: position}));
@@ -765,10 +764,11 @@ function ComposerWithSuggestions({
                     // note: this implementation is only available on non-web RN, thus the wrapping
                     // 'if' block contains a redundant (since the ref is only used on iOS) platform check
                     composerRef.current?.setSelection(positionSnapshot, positionSnapshot);
+                    setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: positionSnapshot, end: positionSnapshot}));
                 });
             }
         },
-        [clearComposerHeight, updateComment],
+        [clearComposerHeight, setCurrentEditMessageSelection, updateComment],
     );
 
     const onSelectionChange = useCallback(
@@ -779,8 +779,6 @@ function ComposerWithSuggestions({
                 ...prevSelection,
                 start: e.nativeEvent.selection.start,
                 end: e.nativeEvent.selection.end,
-                positionX: 0,
-                positionY: 0,
             }));
 
             if (!composerRef.current?.isFocused()) {
@@ -1103,8 +1101,6 @@ function ComposerWithSuggestions({
                 ...prevSelection,
                 start: suggestionSelection.start,
                 end: suggestionSelection.end,
-                positionX: 0,
-                positionY: 0,
             }));
 
             if (endOfSuggestionSelection === undefined) {
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 60e99366e00c..962a4b3663f8 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -125,7 +125,7 @@ function ReportActionItemMessageEdit({
     const setSelection = useCallback(
         (newSelection: TextSelection) => {
             setSelectionState(newSelection);
-            setCurrentEditMessageSelection({...newSelection, positionX: 0, positionY: 0});
+            setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, ...newSelection}));
         },
         [setSelectionState, setCurrentEditMessageSelection],
     );

From 130be1a8f3abb2547dd5b507e454dbff9f723c70 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 23:07:19 +0000
Subject: [PATCH 092/233] fix: reset to previous draft when switching from
 narrow to wide

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index db46cc1c36af..d5c0089831da 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -373,7 +373,7 @@ function ComposerWithSuggestions({
         // Editing is ongoing and layout toggled from narrow to wide.
         if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = false;
-            const nextValue = editingMessage ?? '';
+            const nextValue = draftComment ?? '';
             applyComposerValue(nextValue, false);
         }
     }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, getEditingState, shouldUseNarrowLayout]);

From 1e0fcec6990e0af92fee64c1ef11fbce232ca685 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 23:07:41 +0000
Subject: [PATCH 093/233] fix: onSelectionChange crashing

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index d5c0089831da..277253994a92 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -773,12 +773,11 @@ function ComposerWithSuggestions({
 
     const onSelectionChange = useCallback(
         (e: CustomSelectionChangeEvent) => {
-            setSelection(e.nativeEvent.selection);
-
+            const newSelection = {...e.nativeEvent.selection};
+            setSelection(newSelection);
             setCurrentEditMessageSelection((prevSelection) => ({
                 ...prevSelection,
-                start: e.nativeEvent.selection.start,
-                end: e.nativeEvent.selection.end,
+                ...newSelection,
             }));
 
             if (!composerRef.current?.isFocused()) {

From 0bc83b9128563efbd30ffd89fee010d9f6a6b05f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 23:07:55 +0000
Subject: [PATCH 094/233] fix: simplify `setSelection` call

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx     | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 277253994a92..efff0f15123e 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -708,10 +708,9 @@ function ComposerWithSuggestions({
                     const newEnd = selection.start - lastGraphemeLength;
 
                     setSelection((prevSelection) => ({
+                        ...prevSelection,
                         start: newStart,
                         end: newEnd,
-                        positionX: prevSelection.positionX,
-                        positionY: prevSelection.positionY,
                     }));
 
                     setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: newStart, end: newEnd}));

From 4da51593256592567fbee3b7f2c4559fd8b797ca Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 23:08:24 +0000
Subject: [PATCH 095/233] refactor: extract imperative composer selection
 callback

---
 .../ComposerWithSuggestions.tsx               | 34 ++++++++++++-------
 1 file changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index efff0f15123e..3033b12d005d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -295,6 +295,20 @@ function ComposerWithSuggestions({
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousEditingReportActionIDRef = useRef<string | null>(editingReportActionID ?? null);
 
+    const updateSelectionImperatively = useCallback((start: number, end: number) => {
+        if (!isIOSNative) {
+            return;
+        }
+
+        // ensure that selection is set imperatively after all state changes are effective
+        // eslint-disable-next-line @typescript-eslint/no-deprecated
+        InteractionManager.runAfterInteractions(() => {
+            // note: this implementation is only available on non-web RN, thus the wrapping
+            // 'if' block contains a redundant (since the ref is only used on iOS) platform check
+            composerRef.current?.setSelection(start, end);
+        });
+    }, []);
+
     const applyComposerValue = useCallback(
         (nextValue: string, isEditingInComposer?: boolean) => {
             const defaultSelection = {start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0} satisfies TextSelection;
@@ -753,21 +767,15 @@ function ComposerWithSuggestions({
 
             updateComment(commentValue, true);
 
-            if (isIOSNative && syncSelectionWithOnChangeTextRef.current) {
-                const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position;
-                syncSelectionWithOnChangeTextRef.current = null;
-
-                // ensure that selection is set imperatively after all state changes are effective
-                // eslint-disable-next-line @typescript-eslint/no-deprecated
-                InteractionManager.runAfterInteractions(() => {
-                    // note: this implementation is only available on non-web RN, thus the wrapping
-                    // 'if' block contains a redundant (since the ref is only used on iOS) platform check
-                    composerRef.current?.setSelection(positionSnapshot, positionSnapshot);
-                    setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: positionSnapshot, end: positionSnapshot}));
-                });
+            if (!syncSelectionWithOnChangeTextRef.current) {
+                return;
             }
+
+            const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position;
+            syncSelectionWithOnChangeTextRef.current = null;
+            updateSelectionImperatively(positionSnapshot, positionSnapshot);
         },
-        [clearComposerHeight, setCurrentEditMessageSelection, updateComment],
+        [clearComposerHeight, updateComment, updateSelectionImperatively],
     );
 
     const onSelectionChange = useCallback(

From 125e106d2718b3e14b7ab2c87134c40a26460731 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 23:08:38 +0000
Subject: [PATCH 096/233] refactor: change default selection type

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 3033b12d005d..86d00d4c160f 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -311,7 +311,7 @@ function ComposerWithSuggestions({
 
     const applyComposerValue = useCallback(
         (nextValue: string, isEditingInComposer?: boolean) => {
-            const defaultSelection = {start: nextValue.length, end: nextValue.length, positionX: 0, positionY: 0} satisfies TextSelection;
+            const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
             const selectionToApply = isEditingInComposer ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection;
 
             commentRef.current = nextValue;

From fb0d927d76406eb4af808e35f3495708923ef41b Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 10 Mar 2026 23:08:54 +0000
Subject: [PATCH 097/233] refactor: remove hardcoded selection position value

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 86d00d4c160f..998c35a4a5d9 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -284,7 +284,7 @@ function ComposerWithSuggestions({
         isEditInProgressRef: isDraftPendingSaved,
     });
 
-    const [selection, setSelection] = useState<TextSelection>(() => currentEditMessageSelection ?? {start: value.length, end: value.length, positionX: 0, positionY: 0});
+    const [selection, setSelection] = useState<TextSelection>(() => currentEditMessageSelection ?? {start: value.length, end: value.length});
 
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
 

From bf110c0be1475bfe69bd0f78c01caf8277ca0c23 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 11:15:40 +0000
Subject: [PATCH 098/233] fix: composer text selection on edit

---
 .../ComposerWithSuggestions.tsx               | 29 ++++++++++++-------
 1 file changed, 19 insertions(+), 10 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 998c35a4a5d9..d4668fe225c2 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -309,22 +309,31 @@ function ComposerWithSuggestions({
         });
     }, []);
 
+    type ApplyComposerValueOptions = {
+        isEditingInComposer?: boolean;
+        shouldForceSelectionToEnd?: boolean;
+    };
+
     const applyComposerValue = useCallback(
-        (nextValue: string, isEditingInComposer?: boolean) => {
+        (nextValue: string, options?: ApplyComposerValueOptions) => {
             const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
-            const selectionToApply = isEditingInComposer ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection;
+            const shouldUseEditingSelection = options?.isEditingInComposer ?? false;
+            const shouldForceSelectionToEnd = options?.shouldForceSelectionToEnd ?? false;
+
+            const selectionToApply = shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection;
 
             commentRef.current = nextValue;
             emojisPresentBefore.current = extractEmojis(nextValue);
 
             setValue(nextValue);
             setSelection(selectionToApply);
+            updateSelectionImperatively(selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
 
-            if (isEditingInComposer) {
+            if (options?.isEditingInComposer) {
                 composerRef.current?.focus();
             }
         },
-        [currentEditMessageSelection],
+        [currentEditMessageSelection, updateSelectionImperatively],
     );
 
     useEffect(() => {
@@ -344,7 +353,7 @@ function ComposerWithSuggestions({
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment.
                 const nextValue = draftComment ?? '';
-                applyComposerValue(nextValue, false);
+                applyComposerValue(nextValue);
             }
 
             wasEditing.current = false;
@@ -361,17 +370,17 @@ function ComposerWithSuggestions({
                 // Wide layout – another editor handles the edit, keep composer draft as-is.
                 return;
             }
-
             // In narrow layout we always show the message being edited.
             const nextValue = editingMessage ?? '';
-            applyComposerValue(nextValue, true);
+            // When starting to edit in the composer, always place the cursor at the end of the message.
+            applyComposerValue(nextValue, {isEditingInComposer: true, shouldForceSelectionToEnd: true});
             return;
         }
 
         // We are already in editing mode, but the target message changed.
         if (didChangeEditedAction && shouldUseNarrowLayout) {
             const nextValue = editingMessage ?? '';
-            applyComposerValue(nextValue, true);
+            applyComposerValue(nextValue, {isEditingInComposer: true});
             return;
         }
 
@@ -380,7 +389,7 @@ function ComposerWithSuggestions({
             wasEditingInComposerRef.current = true;
             // We just moved from wide to narrow while editing – start editing in the composer.
             const nextValue = editingMessage ?? '';
-            applyComposerValue(nextValue, true);
+            applyComposerValue(nextValue, {isEditingInComposer: true});
             return;
         }
 
@@ -388,7 +397,7 @@ function ComposerWithSuggestions({
         if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = false;
             const nextValue = draftComment ?? '';
-            applyComposerValue(nextValue, false);
+            applyComposerValue(nextValue);
         }
     }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, getEditingState, shouldUseNarrowLayout]);
 

From 246888bfc629f5464036288065cac18ec36ddc54 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 11:25:18 +0000
Subject: [PATCH 099/233] fix: go back to previous selection if editing stops

---
 .../ComposerWithSuggestions.tsx               | 21 ++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index d4668fe225c2..7cbbd04e4e05 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -294,6 +294,7 @@ function ComposerWithSuggestions({
 
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousEditingReportActionIDRef = useRef<string | null>(editingReportActionID ?? null);
+    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
     const updateSelectionImperatively = useCallback((start: number, end: number) => {
         if (!isIOSNative) {
@@ -311,16 +312,18 @@ function ComposerWithSuggestions({
 
     type ApplyComposerValueOptions = {
         isEditingInComposer?: boolean;
-        shouldForceSelectionToEnd?: boolean;
+        shouldMoveSelectionToEnd?: boolean;
+        selection?: TextSelection | null;
     };
 
     const applyComposerValue = useCallback(
         (nextValue: string, options?: ApplyComposerValueOptions) => {
             const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
             const shouldUseEditingSelection = options?.isEditingInComposer ?? false;
-            const shouldForceSelectionToEnd = options?.shouldForceSelectionToEnd ?? false;
+            const shouldForceSelectionToEnd = options?.shouldMoveSelectionToEnd ?? false;
+            const explicitSelection = options?.selection ?? null;
 
-            const selectionToApply = shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection;
+            const selectionToApply = explicitSelection ?? (shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection);
 
             commentRef.current = nextValue;
             emojisPresentBefore.current = extractEmojis(nextValue);
@@ -351,18 +354,22 @@ function ComposerWithSuggestions({
 
         if (!isEditing) {
             if (wasEditing.current && wasEditingInComposerRef.current) {
-                // Editing just ended in the composer – restore the draft comment.
+                // Editing just ended in the composer – restore the draft comment and its previous selection.
                 const nextValue = draftComment ?? '';
-                applyComposerValue(nextValue);
+                applyComposerValue(nextValue, {selection: previousDraftSelectionRef.current});
             }
 
             wasEditing.current = false;
             wasEditingInComposerRef.current = shouldUseNarrowLayout;
+            previousDraftSelectionRef.current = null;
             return;
         }
 
         // Editing just started.
         if (!wasEditing.current) {
+            // Store the draft selection before switching into edit mode so we can restore it later.
+            previousDraftSelectionRef.current = selection;
+
             wasEditing.current = true;
             wasEditingInComposerRef.current = shouldUseNarrowLayout;
 
@@ -373,7 +380,7 @@ function ComposerWithSuggestions({
             // In narrow layout we always show the message being edited.
             const nextValue = editingMessage ?? '';
             // When starting to edit in the composer, always place the cursor at the end of the message.
-            applyComposerValue(nextValue, {isEditingInComposer: true, shouldForceSelectionToEnd: true});
+            applyComposerValue(nextValue, {isEditingInComposer: true, shouldMoveSelectionToEnd: true});
             return;
         }
 
@@ -399,7 +406,7 @@ function ComposerWithSuggestions({
             const nextValue = draftComment ?? '';
             applyComposerValue(nextValue);
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, getEditingState, shouldUseNarrowLayout]);
+    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, getEditingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank

From 589ea10e3800b0cd7ae65dd1730594236a9057fe Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 12:00:53 +0000
Subject: [PATCH 100/233] fix: remove unnecessary
 `ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT` key

---
 src/ONYXKEYS.ts                               |  4 ---
 src/libs/actions/App.ts                       |  1 -
 src/libs/actions/Composer.ts                  | 11 -------
 src/libs/actions/QueuedOnyxUpdates.ts         |  1 -
 .../index.android.ts                          |  3 --
 .../index.ios.ts                              |  3 --
 .../index.ts                                  | 13 --------
 ...uldShowComposeInputKeyboardAwareBuilder.ts | 33 -------------------
 .../types.ts                                  |  3 --
 src/pages/inbox/ReportScreen.tsx              | 17 ----------
 .../ComposerWithSuggestions.tsx               | 10 +-----
 .../ReportActionCompose.tsx                   |  9 +----
 .../report/ReportActionItemMessageEdit.tsx    | 21 +++---------
 src/pages/inbox/report/ReportFooter.tsx       | 17 ++--------
 src/setup/index.ts                            |  1 -
 15 files changed, 8 insertions(+), 139 deletions(-)
 delete mode 100644 src/libs/actions/Composer.ts
 delete mode 100644 src/libs/setShouldShowComposeInputKeyboardAware/index.android.ts
 delete mode 100644 src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts
 delete mode 100644 src/libs/setShouldShowComposeInputKeyboardAware/index.ts
 delete mode 100644 src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts
 delete mode 100644 src/libs/setShouldShowComposeInputKeyboardAware/types.ts

diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 44eeb2f50b74..85eccfba0d25 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -395,9 +395,6 @@ const ONYXKEYS = {
     /** The policyID of the last workspace whose settings were accessed by the user */
     LAST_ACCESSED_WORKSPACE_POLICY_ID: 'lastAccessedWorkspacePolicyID',
 
-    /** Whether we should show the compose input or not */
-    SHOULD_SHOW_COMPOSE_INPUT: 'shouldShowComposeInput',
-
     /** Is app in beta version */
     IS_BETA: 'isBeta',
 
@@ -1357,7 +1354,6 @@ type OnyxValuesMapping = {
     [ONYXKEYS.HAS_LOADED_APP]: boolean;
     [ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer;
     [ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string;
-    [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean;
     [ONYXKEYS.IS_BETA]: boolean;
     [ONYXKEYS.IS_CHECKING_PUBLIC_ROOM]: boolean;
     [ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS]: Record<string, string>;
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index fc9c32d4920c..eb2bcc527bab 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -127,7 +127,6 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [
     ONYXKEYS.MODAL,
     ONYXKEYS.NETWORK,
     ONYXKEYS.SESSION,
-    ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT,
     ONYXKEYS.NVP_TRY_FOCUS_MODE,
     ONYXKEYS.PREFERRED_THEME,
     ONYXKEYS.NVP_PREFERRED_LOCALE,
diff --git a/src/libs/actions/Composer.ts b/src/libs/actions/Composer.ts
deleted file mode 100644
index edd42fb801f2..000000000000
--- a/src/libs/actions/Composer.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-function setShouldShowComposeInput(shouldShowComposeInput: boolean) {
-    Onyx.set(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, shouldShowComposeInput);
-}
-
-export {
-    // eslint-disable-next-line import/prefer-default-export
-    setShouldShowComposeInput,
-};
diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts
index be2a1480ccec..57301313d387 100644
--- a/src/libs/actions/QueuedOnyxUpdates.ts
+++ b/src/libs/actions/QueuedOnyxUpdates.ts
@@ -48,7 +48,6 @@ function flushQueue(): Promise<void> {
             ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
             ONYXKEYS.MODAL,
             ONYXKEYS.NETWORK,
-            ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT,
             ONYXKEYS.PRESERVED_USER_SESSION,
         ]);
 
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.android.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.android.ts
deleted file mode 100644
index 68c750b05a5f..000000000000
--- a/src/libs/setShouldShowComposeInputKeyboardAware/index.android.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import setShouldShowComposeInputKeyboardAwareBuilder from './setShouldShowComposeInputKeyboardAwareBuilder';
-
-export default setShouldShowComposeInputKeyboardAwareBuilder('keyboardDidHide');
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts
deleted file mode 100644
index 68c750b05a5f..000000000000
--- a/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import setShouldShowComposeInputKeyboardAwareBuilder from './setShouldShowComposeInputKeyboardAwareBuilder';
-
-export default setShouldShowComposeInputKeyboardAwareBuilder('keyboardDidHide');
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.ts
deleted file mode 100644
index b3a2e7542148..000000000000
--- a/src/libs/setShouldShowComposeInputKeyboardAware/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as Composer from '@userActions/Composer';
-import type SetShouldShowComposeInputKeyboardAware from './types';
-
-const setShouldShowComposeInputKeyboardAware: SetShouldShowComposeInputKeyboardAware = (shouldShow) => {
-    // We want to show the main composer when the edit composer loses focus.
-    // If it loses focus due to a pressable being pressed, the press event might not be captured.
-    // To address this, we delay showing the main composer to allow the press event to be completed.
-    setTimeout(() => {
-        Composer.setShouldShowComposeInput(shouldShow);
-    }, 0);
-};
-
-export default setShouldShowComposeInputKeyboardAware;
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts b/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts
deleted file mode 100644
index 72df7a730e02..000000000000
--- a/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import type {EmitterSubscription} from 'react-native';
-import {Keyboard} from 'react-native';
-import type {KeyboardEventName} from 'react-native/Libraries/Components/Keyboard/Keyboard';
-import * as Composer from '@userActions/Composer';
-import type SetShouldShowComposeInputKeyboardAware from './types';
-
-let keyboardEventListener: EmitterSubscription | null = null;
-
-const setShouldShowComposeInputKeyboardAwareBuilder: (keyboardEvent: KeyboardEventName) => SetShouldShowComposeInputKeyboardAware =
-    (keyboardEvent: KeyboardEventName) => (shouldShow: boolean) => {
-        if (keyboardEventListener) {
-            keyboardEventListener.remove();
-            keyboardEventListener = null;
-        }
-
-        if (!shouldShow) {
-            Composer.setShouldShowComposeInput(false);
-            return;
-        }
-
-        // If keyboard is already hidden, we should show composer immediately because keyboardDidHide event won't be called
-        if (!Keyboard.isVisible()) {
-            Composer.setShouldShowComposeInput(true);
-            return;
-        }
-
-        keyboardEventListener = Keyboard.addListener(keyboardEvent, () => {
-            Composer.setShouldShowComposeInput(true);
-            keyboardEventListener?.remove();
-        });
-    };
-
-export default setShouldShowComposeInputKeyboardAwareBuilder;
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/types.ts b/src/libs/setShouldShowComposeInputKeyboardAware/types.ts
deleted file mode 100644
index 7e3a604f562e..000000000000
--- a/src/libs/setShouldShowComposeInputKeyboardAware/types.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type SetShouldShowComposeInputKeyboardAware = (shouldShow: boolean) => void;
-
-export default SetShouldShowComposeInputKeyboardAware;
diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index bedb99476089..659df0bd6634 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -87,7 +87,6 @@ import {cancelSpan, cancelSpansByPrefix} from '@libs/telemetry/activeSpans';
 import {getParentReportActionDeletionStatus} from '@libs/TransactionNavigationUtils';
 import {isNumeric} from '@libs/ValidationUtils';
 import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types';
-import {setShouldShowComposeInput} from '@userActions/Composer';
 import {
     createTransactionThreadReport,
     navigateToConciergeChat,
@@ -652,12 +651,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
     useAppFocusEvent(clearNotifications);
 
     useEffect(() => {
-        // eslint-disable-next-line @typescript-eslint/no-deprecated
-        const interactionTask = InteractionManager.runAfterInteractions(() => {
-            setShouldShowComposeInput(true);
-        });
         return () => {
-            interactionTask.cancel();
             if (!didSubscribeToReportLeavingEvents.current) {
                 return;
             }
@@ -767,18 +761,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
             Navigation.isNavigationReady().then(() => {
                 navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, false);
             });
-            return;
         }
-
-        // If you already have a report open and are deeplinking to a new report on native,
-        // the ReportScreen never actually unmounts and the reportID in the route also doesn't change.
-        // Therefore, we need to compare if the existing reportID is the same as the one in the route
-        // before deciding that we shouldn't call OpenReport.
-        if (reportIDFromRoute === lastReportIDFromRoute && (!onyxReportID || onyxReportID === reportIDFromRoute)) {
-            return;
-        }
-
-        setShouldShowComposeInput(true);
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [
         route.name,
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 7cbbd04e4e05..fffd7c7d3dc1 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -130,9 +130,6 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
         /** Function to handle sending a message */
         onEnterKeyPress: () => void;
 
-        /** Whether the compose input should show */
-        shouldShowComposeInput: OnyxEntry<boolean>;
-
         /** Function to measure the parent container */
         measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void;
 
@@ -160,9 +157,6 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> &
         /** policy ID of the report */
         policyID?: string;
 
-        /** Whether the main composer was hidden */
-        didHideComposerInput?: boolean;
-
         /** Reference to the outer element */
         ref?: Ref<ComposerWithSuggestionsRef | null>;
     };
@@ -224,7 +218,6 @@ function ComposerWithSuggestions({
     disabled,
     setIsCommentEmpty,
     onEnterKeyPress,
-    shouldShowComposeInput,
     measureParentContainer = () => {},
     isScrollLikelyLayoutTriggered,
     raiseIsScrollLikelyLayoutTriggered,
@@ -238,7 +231,6 @@ function ComposerWithSuggestions({
 
     // For testing
     children,
-    didHideComposerInput,
 
     // Fullstory
     forwardedFSClass,
@@ -424,7 +416,7 @@ function ComposerWithSuggestions({
     }, [value]);
 
     const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;
-    const shouldAutoFocus = (shouldFocusInputOnScreenFocus || !!draftComment) && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput;
+    const shouldAutoFocus = (shouldFocusInputOnScreenFocus || !!draftComment) && areAllModalsHidden() && isFocused;
     const delayedAutoFocusRouteKeyRef = useRef<string | null>(null);
 
     const valueRef = useRef(value);
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index ee2e55f5f2c1..a3dc5637ccb8 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -111,9 +111,6 @@ type ReportActionComposeProps = Pick<ComposerWithSuggestionsProps, 'reportID' |
     /** A method to call when the input is blur */
     onComposerBlur?: () => void;
 
-    /** Whether the main composer was hidden */
-    didHideComposerInput?: boolean;
-
     /** Whether the report screen is being displayed in the side panel */
     isInSidePanel?: boolean;
 
@@ -139,7 +136,6 @@ function ReportActionCompose({
     lastReportAction,
     onComposerFocus,
     onComposerBlur,
-    didHideComposerInput,
     reportTransactions,
     transactionThreadReportID,
     isInSidePanel = false,
@@ -156,7 +152,6 @@ function ReportActionCompose({
     const personalDetails = usePersonalDetails();
     const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
     const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE);
-    const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT);
     const {isRestrictedToPreferredPolicy} = usePreferredPolicy();
     const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
     const [initialModalState] = useOnyx(ONYXKEYS.MODAL);
@@ -186,7 +181,7 @@ function ReportActionCompose({
      * Updates the Highlight state of the composer
      */
     const [isFocused, setIsFocused] = useState(() => {
-        return shouldFocusComposerOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
+        return shouldFocusComposerOnScreenFocus && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
     });
 
     const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize);
@@ -626,12 +621,10 @@ function ReportActionCompose({
                             disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
                             setIsCommentEmpty={setIsCommentEmpty}
                             onEnterKeyPress={sendMessage}
-                            shouldShowComposeInput={shouldShowComposeInput}
                             onFocus={onFocus}
                             onBlur={onBlur}
                             measureParentContainer={measureContainer}
                             onValueChange={onValueChange}
-                            didHideComposerInput={didHideComposerInput}
                             forwardedFSClass={fsClass}
                         />
                         {shouldDisplayDualDropZone && (
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 962a4b3663f8..e37f30e878ad 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -1,7 +1,7 @@
 import lodashDebounce from 'lodash/debounce';
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {InteractionManager, View} from 'react-native';
-import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native';
+import type {MeasureInWindowOnSuccessCallback, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native';
 import {useFocusedInputHandler} from 'react-native-keyboard-controller';
 import {useSharedValue} from 'react-native-reanimated';
 import type {Emoji} from '@assets/emojis/types';
@@ -19,8 +19,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useScrollBlocker from '@hooks/useScrollBlocker';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useThemeStyles from '@hooks/useThemeStyles';
-import {setShouldShowComposeInput} from '@libs/actions/Composer';
-import {clearActive, isActive as isEmojiPickerActive, isEmojiPickerVisible} from '@libs/actions/EmojiPickerAction';
+import {clearActive, isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
 import {composerFocusKeepFocusOn} from '@libs/actions/InputFocus';
 import {saveReportActionDraft} from '@libs/actions/Report';
 import {isMobileChrome} from '@libs/Browser';
@@ -31,7 +30,6 @@ import focusComposerWithDelay from '@libs/focusComposerWithDelay';
 import type {Selection} from '@libs/focusComposerWithDelay/types';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import reportActionItemEventHandler from '@libs/ReportActionItemEventHandler';
-import setShouldShowComposeInputKeyboardAware from '@libs/setShouldShowComposeInputKeyboardAware';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
@@ -197,9 +195,6 @@ function ReportActionItemMessageEdit({
         }, true);
     }, [focus]);
 
-    // show the composer after editing is complete for devices that hide the composer during editing.
-    useEffect(() => () => setShouldShowComposeInput(true), []);
-
     /**
      * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
      * allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
@@ -461,9 +456,8 @@ function ReportActionItemMessageEdit({
                                 if (isMobileChrome() && reportScrollManager.ref?.current) {
                                     reportScrollManager.ref.current.scrollToIndex({index, animated: false});
                                 }
-                                setShouldShowComposeInputKeyboardAware(false);
                                 // The last composer that had focus should re-gain focus
-                                setUpComposeFocusManager();
+                                // setUpComposeFocusManager();
 
                                 // Clear active report action when another action gets focused
                                 if (!isEmojiPickerActive(action.reportActionID)) {
@@ -473,14 +467,7 @@ function ReportActionItemMessageEdit({
                                     ReportActionContextMenu.clearActiveReportAction();
                                 }
                             }}
-                            onBlur={(event: BlurEvent) => {
-                                setIsFocused(false);
-                                const relatedTargetId = event.nativeEvent?.target;
-                                if (relatedTargetId === tag.get() || isEmojiPickerVisible()) {
-                                    return;
-                                }
-                                setShouldShowComposeInputKeyboardAware(true);
-                            }}
+                            onBlur={() => setIsFocused(false)}
                             onLayout={(event) => {
                                 if (!isFocused) {
                                     return;
diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx
index fe5ddf9f341a..352eb3ab86ce 100644
--- a/src/pages/inbox/report/ReportFooter.tsx
+++ b/src/pages/inbox/report/ReportFooter.tsx
@@ -1,6 +1,6 @@
 import {isBlockedFromChatSelector} from '@selectors/BlockedFromChat';
 import {Str} from 'expensify-common';
-import React, {useEffect, useState} from 'react';
+import React from 'react';
 import {Keyboard, View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import AnonymousReportFooter from '@components/AnonymousReportFooter';
@@ -91,7 +91,6 @@ function ReportFooter({
     const personalDetail = useCurrentUserPersonalDetails();
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Lightbulb']);
 
-    const [shouldShowComposeInput = false] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT);
     const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
     const isAnonymousUser = useIsAnonymousUser();
     const [isBlockedFromChat] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CHAT, {
@@ -200,17 +199,6 @@ function ReportFooter({
         });
     };
 
-    const [didHideComposerInput, setDidHideComposerInput] = useState(!shouldShowComposeInput);
-
-    useEffect(() => {
-        if (didHideComposerInput || shouldShowComposeInput) {
-            return;
-        }
-        // This is an intentional one-way latch: once the composer input has been hidden, it stays hidden.
-        // eslint-disable-next-line react-hooks/set-state-in-effect
-        setDidHideComposerInput(true);
-    }, [shouldShowComposeInput, didHideComposerInput]);
-
     return (
         <>
             {!!shouldHideComposer && (
@@ -248,7 +236,7 @@ function ReportFooter({
                     )}
                 </View>
             )}
-            {!shouldHideComposer && (!!shouldShowComposeInput || !isSmallScreenWidth) && (
+            {!shouldHideComposer && !isSmallScreenWidth && (
                 <View style={[chatFooterStyles, isComposerFullSize && styles.chatFooterFullCompose]}>
                     <SwipeableView onSwipeDown={Keyboard.dismiss}>
                         <ReportActionCompose
@@ -260,7 +248,6 @@ function ReportFooter({
                             lastReportAction={lastReportAction}
                             pendingAction={reportPendingAction}
                             isComposerFullSize={isComposerFullSize}
-                            didHideComposerInput={didHideComposerInput}
                             reportTransactions={reportTransactions}
                             transactionThreadReportID={transactionThreadReportID}
                             isInSidePanel={isInSidePanel}
diff --git a/src/setup/index.ts b/src/setup/index.ts
index 1c8f5655ff0d..fce4533aa09e 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -49,7 +49,6 @@ export default function () {
             [ONYXKEYS.ACCOUNT]: CONST.DEFAULT_ACCOUNT_DATA,
             [ONYXKEYS.NETWORK]: CONST.DEFAULT_NETWORK_DATA,
             [ONYXKEYS.IS_SIDEBAR_LOADED]: false,
-            [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: true,
             [ONYXKEYS.MODAL]: {
                 isVisible: false,
                 willAlertModalBecomeVisible: false,

From 63bf6570a79080bb0b58a8d4dbfea23a75e29e16 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 12:08:10 +0000
Subject: [PATCH 101/233] fix: always show composer

---
 src/pages/inbox/report/ReportFooter.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx
index 352eb3ab86ce..5d11002683ca 100644
--- a/src/pages/inbox/report/ReportFooter.tsx
+++ b/src/pages/inbox/report/ReportFooter.tsx
@@ -236,7 +236,7 @@ function ReportFooter({
                     )}
                 </View>
             )}
-            {!shouldHideComposer && !isSmallScreenWidth && (
+            {!shouldHideComposer && (
                 <View style={[chatFooterStyles, isComposerFullSize && styles.chatFooterFullCompose]}>
                     <SwipeableView onSwipeDown={Keyboard.dismiss}>
                         <ReportActionCompose

From eafcebe49f65710ef92e5929c911db1e028b6672 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 12:08:27 +0000
Subject: [PATCH 102/233] refactor: remove unused variable

---
 src/pages/inbox/report/ReportFooter.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx
index 5d11002683ca..99e07eb0efe0 100644
--- a/src/pages/inbox/report/ReportFooter.tsx
+++ b/src/pages/inbox/report/ReportFooter.tsx
@@ -87,7 +87,7 @@ function ReportFooter({
     const {translate} = useLocalize();
     const {windowWidth} = useWindowDimensions();
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
-    const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
     const personalDetail = useCurrentUserPersonalDetails();
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Lightbulb']);
 

From cc4d336e775583e7951f2206a42c91e709547df4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 14:23:04 +0000
Subject: [PATCH 103/233] fix: clear report action drafts when report is
 switched

---
 src/pages/inbox/ReportScreen.tsx | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index 659df0bd6634..836be940e7b9 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -88,6 +88,7 @@ import {getParentReportActionDeletionStatus} from '@libs/TransactionNavigationUt
 import {isNumeric} from '@libs/ValidationUtils';
 import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types';
 import {
+    clearReportActionDrafts,
     createTransactionThreadReport,
     navigateToConciergeChat,
     openReport,
@@ -180,6 +181,19 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
 
     const archivedReportsIdSet = useArchivedReportsIdSet();
 
+    // When the report screen is navigated away from, clear all report action edit drafts
+    useFocusEffect(
+        useCallback(() => {
+            if (!reportIDFromRoute) {
+                return;
+            }
+
+            return () => {
+                clearReportActionDrafts(reportIDFromRoute);
+            };
+        }, [reportIDFromRoute]),
+    );
+
     const parentReportAction = useParentReportAction(reportOnyx);
 
     const deletedParentAction = isDeletedParentAction(parentReportAction);

From e8417cb07a67ca7191aa2b2536000fe622ce1d59 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 11 Mar 2026 14:23:24 +0000
Subject: [PATCH 104/233] refactor: simplify report action draft clearance

---
 src/libs/actions/Report/index.ts                      | 11 -----------
 .../inbox/report/ContextMenu/ContextMenuActions.tsx   |  7 +++----
 src/pages/inbox/report/PureReportActionItem.tsx       |  9 +++------
 .../report/ReportActionCompose/useEditMessage.ts      |  4 ++--
 src/pages/inbox/report/ReportActionItem.tsx           |  9 +--------
 5 files changed, 9 insertions(+), 31 deletions(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index fdd3f5496cf8..d1d4459bb404 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2741,16 +2741,6 @@ function clearReportActionDrafts(reportID: string | undefined) {
     Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, null);
 }
 
-/** Deletes the draft for a comment report action. */
-function deleteReportActionDraft(reportID: string | undefined, reportAction: ReportAction | null | undefined) {
-    if (!reportAction) {
-        return;
-    }
-
-    const originalReportID = getOriginalReportID(reportID, reportAction, undefined);
-    Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`, {[reportAction.reportActionID]: null});
-}
-
 /** Saves the draft for a comment report action. This will put the comment into "edit mode" */
 function saveReportActionDraft(reportID: string | undefined, reportAction: ReportAction | null, draftMessage: string) {
     if (!reportAction) {
@@ -7073,7 +7063,6 @@ export {
     createNewReport,
     deleteReport,
     clearReportActionDrafts,
-    deleteReportActionDraft,
     deleteReportComment,
     deleteReportField,
     dismissTrackExpenseActionableWhisper,
diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 0fff2a5c214c..c0e7f7a948dd 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -172,7 +172,6 @@ import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils
 import {setDownload} from '@userActions/Download';
 import {
     clearReportActionDrafts,
-    deleteReportActionDraft,
     explain,
     markCommentAsUnread,
     navigateToAndOpenChildReport,
@@ -526,10 +525,10 @@ const ContextMenuActions: ContextMenuAction[] = [
                 clearReportActionDrafts(reportID);
 
                 if (draftMessage) {
-                    deleteReportActionDraft(reportID, reportAction);
-                } else {
-                    saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction)));
+                    return;
                 }
+
+                saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction)));
             };
 
             if (closePopover) {
diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 92aa0a7f604a..75dc2152aa6d 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -231,6 +231,7 @@ import type {IgnoreDirection} from '@userActions/ClearReportActionErrors';
 import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction';
 import {acceptJoinRequest, declineJoinRequest} from '@userActions/Policy/Member';
 import {
+    clearReportActionDrafts,
     createTransactionThreadReport,
     expandURLPreview,
     resolveActionableMentionConfirmWhisper,
@@ -368,9 +369,6 @@ type PureReportActionItemProps = {
     /** Original report from which the given reportAction is first created */
     originalReport?: OnyxTypes.Report;
 
-    /** Function to deletes the draft for a comment report action. */
-    deleteReportActionDraft?: (reportID: string | undefined, action: OnyxTypes.ReportAction) => void;
-
     /** Whether the room is archived */
     isArchivedRoom?: boolean;
 
@@ -516,7 +514,6 @@ function PureReportActionItem({
     blockedFromConcierge,
     originalReportID = '-1',
     originalReport,
-    deleteReportActionDraft = () => {},
     isArchivedRoom,
     isChronosReport,
     toggleEmojiReaction = () => {},
@@ -708,8 +705,8 @@ function PureReportActionItem({
         if (draftMessage === undefined || !isDeletedAction(action)) {
             return;
         }
-        deleteReportActionDraft(reportID, action);
-    }, [draftMessage, action, reportID, deleteReportActionDraft]);
+        clearReportActionDrafts(reportID);
+    }, [draftMessage, action, reportID]);
 
     // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator
     // Removed messages should not be shown anyway and should not need this flow
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index c6bd16a5e6a1..c6402930b653 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -10,7 +10,7 @@ import useOnyx from '@hooks/useOnyx';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useReportScrollManager from '@hooks/useReportScrollManager';
 import {isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
-import {deleteReportActionDraft, editReportComment} from '@libs/actions/Report';
+import {clearReportActionDrafts, editReportComment} from '@libs/actions/Report';
 import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import {getOriginalReportID} from '@libs/ReportUtils';
@@ -70,7 +70,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
 
         setEditingState('cancelled');
 
-        deleteReportActionDraft(reportID, reportAction);
+        clearReportActionDrafts(reportID);
 
         if (isActive()) {
             ReportActionComposeFocusManager.clear(true);
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index 3fafb482cdcd..4c87bcfccee6 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -23,13 +23,7 @@ import {
     isCurrentUserTheOnlyParticipant,
 } from '@libs/ReportUtils';
 import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors';
-import {
-    deleteReportActionDraft,
-    dismissTrackExpenseActionableWhisper,
-    resolveActionableMentionWhisper,
-    resolveActionableReportMentionWhisper,
-    toggleEmojiReaction,
-} from '@userActions/Report';
+import {dismissTrackExpenseActionableWhisper, resolveActionableMentionWhisper, resolveActionableReportMentionWhisper, toggleEmojiReaction} from '@userActions/Report';
 import {clearError} from '@userActions/Transaction';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -159,7 +153,6 @@ function ReportActionItem({
             blockedFromConcierge={blockedFromConcierge}
             originalReportID={originalReportID}
             originalReport={originalReport}
-            deleteReportActionDraft={deleteReportActionDraft}
             isArchivedRoom={isArchivedNonExpenseReport(originalReport, isOriginalReportArchived)}
             isChronosReport={chatIncludesChronosWithID(originalReportID)}
             toggleEmojiReaction={toggleEmojiReaction}

From c63f242daf4d33fa040e93875e07e0fe008bd141 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Mar 2026 10:18:22 +0000
Subject: [PATCH 105/233] fix: editing thread messages

---
 src/pages/inbox/ReportScreen.tsx              |  6 +-
 .../ReportActionCompose.tsx                   |  7 +-
 .../report/ReportActionEditMessageContext.tsx | 91 ++++++++++++++-----
 3 files changed, 77 insertions(+), 27 deletions(-)

diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index 836be940e7b9..a4611e6d7326 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -1001,7 +1001,11 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
     return (
         // Wide RHP overlays should be rendered only for the report screen displayed in RHP
         <WideRHPOverlayWrapper shouldWrap={route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT}>
-            <ReportActionEditMessageContextProvider reportID={reportID}>
+            <ReportActionEditMessageContextProvider
+                reportID={reportID}
+                parentReportID={reportOnyx?.parentReportID}
+                parentReportAction={parentReportAction}
+            >
                 <ActionListContext.Provider value={actionListValue}>
                     <ReactionListWrapper>
                         <ScreenWrapper
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index a3dc5637ccb8..1716344d6b91 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -163,7 +163,7 @@ function ReportActionCompose({
         canEvict: false,
     });
 
-    const {editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
+    const {editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID;
     const effectiveDraft = shouldUseNarrowLayout ? editingMessage : draftComment;
@@ -328,9 +328,10 @@ function ReportActionCompose({
         isEditing: !!editingReportAction,
     });
 
-    const originalReportID = useOriginalReportID(reportID, editingReportAction);
+    const originalReportID = useOriginalReportID(editingReportID ?? undefined, editingReportAction);
+
     const {publishDraft, deleteDraft} = useEditMessage({
-        reportID,
+        reportID: editingReportID ?? undefined,
         originalReportID,
         reportAction: editingReportAction,
         shouldScrollToLastMessage: isEditingLastReportAction,
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 7ebd71c8de1a..059ad6daeb8f 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,4 @@
-import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from 'react';
+import React, {createContext, useContext, useEffect, useRef, useState} from 'react';
 import type {Dispatch, SetStateAction} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
@@ -12,6 +12,7 @@ function NOOP() {
 type EditingState = 'editing' | 'submitted' | 'cancelled';
 
 type ReportActionActiveEdit = {
+    editingReportID: string | null;
     editingReportActionID: string | null;
     editingReportAction: OnyxTypes.ReportAction | null;
     editingMessage: string | null;
@@ -26,6 +27,7 @@ type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
+    editingReportID: null,
     editingReportActionID: null,
     editingReportAction: null,
     editingMessage: null,
@@ -38,15 +40,18 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
 
 type ReportActionEditMessageContextProviderProps = {
     reportID: string | undefined;
+    parentReportID: string | undefined;
+    parentReportAction: OnyxTypes.ReportAction | undefined;
     children: React.ReactNode;
 };
 
-function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
+function ReportActionEditMessageContextProvider({reportID, parentReportID, parentReportAction, children}: ReportActionEditMessageContextProviderProps) {
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
     });
     const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
 
+    const [editingReportID, setEditingReportID] = useState<string | null>(null);
     const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
     const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
@@ -64,6 +69,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
             const newEditingReportActionID = activeEdit?.editingReportActionID ?? null;
             const newEditingReportAction = activeEdit?.editingReportAction ?? null;
             const newEditingMessage = activeEdit?.editingMessage ?? null;
+        const newEditingReportID = activeEdit?.editingReportID ?? null;
 
             if (newEditingReportActionID !== editingReportActionID) {
                 setEditingReportActionID(newEditingReportActionID);
@@ -77,19 +83,30 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         },
         [editingReportActionID, editingReportAction, editingMessage],
     );
+        if (newEditingReportID !== editingReportID) {
+            setEditingReportID(newEditingReportID);
+        }
 
-    const setCurrentEditMessageSelection = useCallback(
-        (setSelectionStateAction: SetStateAction<TextSelection | null>) => {
-            if (!editingReportActionID) {
-                return;
-            }
+        if (newEditingReportActionID !== editingReportActionID) {
+            setEditingReportActionID(newEditingReportActionID);
+        }
+        if (newEditingReportAction !== editingReportAction) {
+            setEditingReportAction(newEditingReportAction);
+        }
+        if (newEditingMessage !== editingMessage) {
+            setEditingMessage(newEditingMessage);
+        }
+    };
 
-            setCurrentEditMessageSelectionState(setSelectionStateAction);
-        },
-        [editingReportActionID],
-    );
+    const setCurrentEditMessageSelection = (setSelectionStateAction: SetStateAction<TextSelection | null>) => {
+        if (!editingReportActionID) {
+            return;
+        }
+
+        setCurrentEditMessageSelectionState(setSelectionStateAction);
+    };
 
-    const reset = useCallback(() => {
+    const reset = () => {
         if (editingStateRef.current === 'editing') {
             return;
         }
@@ -103,20 +120,46 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     useEffect(() => {
         const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
-        if (!reportDrafts) {
-            // eslint-disable-next-line react-hooks/set-state-in-effect
-            reset();
-            return;
+        let parentReportActionDrafts: OnyxTypes.ReportActionsDrafts | undefined;
+        if (parentReportAction && parentReportID) {
+            parentReportActionDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${parentReportID}`];
         }
 
-        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message !== undefined);
-
-        if (!reportDraftEntry) {
+        if (!reportDrafts && !parentReportActionDrafts) {
             reset();
             return;
         }
 
-        const [reportActionID, draft] = reportDraftEntry;
+        let editReportID: string | undefined;
+        let reportActionID: string | undefined;
+        let reportAction: OnyxTypes.ReportAction | undefined;
+        let draft: OnyxTypes.ReportActionsDraft | undefined;
+
+        if (parentReportAction && parentReportActionDrafts) {
+            const parentReportActionDraft = parentReportActionDrafts[parentReportAction.reportActionID];
+            if (parentReportActionDraft) {
+                editReportID = parentReportID;
+                reportActionID = parentReportAction.reportActionID;
+                reportAction = parentReportAction;
+                draft = parentReportActionDraft;
+            }
+        }
+
+        if (!reportActionID) {
+            const reportDraftEntry = reportDrafts ? Object.entries(reportDrafts).find(([, d]) => d?.message !== undefined) : undefined;
+
+            if (!reportDraftEntry) {
+                reset();
+                return;
+            }
+
+            const [reportActionIDOfDraft, reportActionDraft] = reportDraftEntry;
+
+            editReportID = reportID;
+            reportActionID = reportActionIDOfDraft;
+            reportAction = reportActions?.[reportActionID];
+            draft = reportActionDraft;
+        }
 
         if (editingStateRef.current !== null) {
             return;
@@ -124,16 +167,18 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
         editingStateRef.current = 'editing';
         updateActiveEditState({
+            editingReportID: editReportID ?? null,
             editingReportActionID: reportActionID,
-            editingReportAction: reportActions?.[reportActionID] ?? null,
-            editingMessage: draft.message,
+            editingReportAction: reportAction ?? null,
+            editingMessage: draft?.message ?? null,
         });
-    }, [reportActionDrafts, reportActions, reportID, reset, updateActiveEditState]);
+    }, [parentReportAction, parentReportID, reportActionDrafts, reportActions, reportID, reset, updateActiveEditState]);
 
     return (
         <ReportActionEditMessageContext.Provider
             // eslint-disable-next-line react/jsx-no-constructed-context-values
             value={{
+                editingReportID,
                 editingReportActionID,
                 editingReportAction,
                 editingMessage,

From f90b647a34947c50a33ba80c38f0012aa57c030f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Mar 2026 10:18:34 +0000
Subject: [PATCH 106/233] refactor: remove manual memoization

---
 .../report/ReportActionEditMessageContext.tsx | 23 ++++---------------
 1 file changed, 5 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 059ad6daeb8f..8dc30eacbf3e 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -64,25 +64,12 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
         editingStateRef.current = state;
     };
 
-    const updateActiveEditState = useCallback(
-        (activeEdit: ReportActionActiveEdit | null) => {
-            const newEditingReportActionID = activeEdit?.editingReportActionID ?? null;
-            const newEditingReportAction = activeEdit?.editingReportAction ?? null;
-            const newEditingMessage = activeEdit?.editingMessage ?? null;
+    const updateActiveEditState = (activeEdit: ReportActionActiveEdit | null) => {
         const newEditingReportID = activeEdit?.editingReportID ?? null;
+        const newEditingReportActionID = activeEdit?.editingReportActionID ?? null;
+        const newEditingReportAction = activeEdit?.editingReportAction ?? null;
+        const newEditingMessage = activeEdit?.editingMessage ?? null;
 
-            if (newEditingReportActionID !== editingReportActionID) {
-                setEditingReportActionID(newEditingReportActionID);
-            }
-            if (newEditingReportAction !== editingReportAction) {
-                setEditingReportAction(newEditingReportAction);
-            }
-            if (newEditingMessage !== editingMessage) {
-                setEditingMessage(newEditingMessage);
-            }
-        },
-        [editingReportActionID, editingReportAction, editingMessage],
-    );
         if (newEditingReportID !== editingReportID) {
             setEditingReportID(newEditingReportID);
         }
@@ -114,7 +101,7 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
         editingStateRef.current = null;
         updateActiveEditState(null);
         setCurrentEditMessageSelection(null);
-    }, [updateActiveEditState, setCurrentEditMessageSelection]);
+    };
 
     // Initially set the editing report action state when the draft comments change
     useEffect(() => {

From ad44dfd235f1f8c7386ef1ca6794b056b1a789ef Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Mar 2026 11:03:39 +0000
Subject: [PATCH 107/233] fix: use editing message state in
 `ReportActionItemMessageEdit`

---
 .../inbox/report/PureReportActionItem.tsx     |  1 -
 .../report/ReportActionItemMessageEdit.tsx    | 27 +++++--------------
 2 files changed, 7 insertions(+), 21 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 75dc2152aa6d..828faca44be5 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -1775,7 +1775,6 @@ function PureReportActionItem({
                                 {isEditingInline ? (
                                     <ReportActionItemMessageEdit
                                         action={action}
-                                        draftMessage={draftMessage}
                                         reportID={reportID}
                                         originalReportID={originalReportID}
                                         policyID={report?.policyID}
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index e37f30e878ad..13e41c0e3573 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -52,9 +52,6 @@ type ReportActionItemMessageEditProps = {
     /** All the data of the action */
     action: OnyxTypes.ReportAction;
 
-    /** Draft message */
-    draftMessage: string;
-
     /** ReportID that holds the comment we're editing */
     reportID: string | undefined;
 
@@ -84,17 +81,7 @@ const DEFAULT_MODAL_VALUE = {
     isVisible: false,
 };
 
-function ReportActionItemMessageEdit({
-    action,
-    draftMessage,
-    reportID,
-    originalReportID,
-    policyID,
-    index,
-    isGroupPolicyReport,
-    shouldDisableEmojiPicker = false,
-    ref,
-}: ReportActionItemMessageEditProps) {
+function ReportActionItemMessageEdit({action, reportID, originalReportID, policyID, index, isGroupPolicyReport, shouldDisableEmojiPicker = false, ref}: ReportActionItemMessageEditProps) {
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
     const styles = useThemeStyles();
     const StyleUtils = useStyleUtils();
@@ -108,15 +95,15 @@ function ReportActionItemMessageEdit({
     const cursorPositionValue = useSharedValue({x: 0, y: 0});
     const tag = useSharedValue(-1);
     const emojisPresentBefore = useRef<Emoji[]>([]);
+
+    const {currentEditMessageSelection, setCurrentEditMessageSelection, editingMessage, setEditingMessage} = useReportActionActiveEdit();
     const [draft, setDraft] = useState(() => {
-        if (draftMessage) {
-            emojisPresentBefore.current = extractEmojis(draftMessage);
+        if (editingMessage) {
+            emojisPresentBefore.current = extractEmojis(editingMessage);
         }
-        return draftMessage;
+        return editingMessage ?? '';
     });
 
-    const {currentEditMessageSelection, setCurrentEditMessageSelection, setEditingMessage} = useReportActionActiveEdit();
-
     const defaultSelection = useMemo(() => ({start: draft.length, end: draft.length, positionX: 0, positionY: 0}), [draft.length]);
     const [selection, setSelectionState] = useState<TextSelection>(() => currentEditMessageSelection ?? defaultSelection);
 
@@ -152,7 +139,7 @@ function ReportActionItemMessageEdit({
     const isCommentPendingSaved = useRef(false);
 
     useDraftMessageVideoAttributeCache({
-        draftMessage,
+        draftMessage: editingMessage ?? '',
         isEditing: true,
         editingReportAction: action,
         updateDraftMessage: setDraft,

From a2c9308d122b67bb09075ad73849e0666234f464 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Mar 2026 11:10:15 +0000
Subject: [PATCH 108/233] refactor: Remove `useEffect` in and simplify
 `ReportActionEditMessageContext`

---
 .../ComposerWithSuggestions.tsx               |  23 +--
 .../report/ReportActionEditMessageContext.tsx | 157 +++++++-----------
 2 files changed, 65 insertions(+), 115 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index fffd7c7d3dc1..b70fa2f45159 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -253,7 +253,7 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingReportActionID, editingReportAction, editingMessage, setEditingMessage, currentEditMessageSelection, setCurrentEditMessageSelection, getEditingState} =
+    const {editingState, editingReportActionID, editingReportAction, editingMessage, setEditingMessage, currentEditMessageSelection, setCurrentEditMessageSelection} =
         useReportActionActiveEdit();
 
     const [value, setValue] = useState(() => {
@@ -332,7 +332,6 @@ function ComposerWithSuggestions({
     );
 
     useEffect(() => {
-        const editingState = getEditingState();
         if (editingState === 'submitted') {
             return;
         }
@@ -347,8 +346,7 @@ function ComposerWithSuggestions({
         if (!isEditing) {
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
-                const nextValue = draftComment ?? '';
-                applyComposerValue(nextValue, {selection: previousDraftSelectionRef.current});
+                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current});
             }
 
             wasEditing.current = false;
@@ -370,16 +368,14 @@ function ComposerWithSuggestions({
                 return;
             }
             // In narrow layout we always show the message being edited.
-            const nextValue = editingMessage ?? '';
             // When starting to edit in the composer, always place the cursor at the end of the message.
-            applyComposerValue(nextValue, {isEditingInComposer: true, shouldMoveSelectionToEnd: true});
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldMoveSelectionToEnd: true});
             return;
         }
 
         // We are already in editing mode, but the target message changed.
         if (didChangeEditedAction && shouldUseNarrowLayout) {
-            const nextValue = editingMessage ?? '';
-            applyComposerValue(nextValue, {isEditingInComposer: true});
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
             return;
         }
 
@@ -387,18 +383,16 @@ function ComposerWithSuggestions({
         if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = true;
             // We just moved from wide to narrow while editing – start editing in the composer.
-            const nextValue = editingMessage ?? '';
-            applyComposerValue(nextValue, {isEditingInComposer: true});
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
             return;
         }
 
         // Editing is ongoing and layout toggled from narrow to wide.
         if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = false;
-            const nextValue = draftComment ?? '';
-            applyComposerValue(nextValue);
+            applyComposerValue(draftComment ?? '');
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, getEditingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
+    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -622,7 +616,6 @@ function ComposerWithSuggestions({
             }
 
             commentRef.current = newCommentConverted;
-            const editingState = getEditingState();
             if (editingState === 'editing' && shouldUseNarrowLayout) {
                 setEditingMessage(newCommentConverted);
                 if (shouldDebounceSaveComment) {
@@ -651,8 +644,8 @@ function ComposerWithSuggestions({
             debouncedSaveDraft,
             debouncedSaveReportComment,
             editingReportActionID,
+            editingState,
             findNewlyAddedChars,
-            getEditingState,
             preferredLocale,
             preferredSkinTone,
             raiseIsScrollLikelyLayoutTriggered,
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 8dc30eacbf3e..b160265413af 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,5 @@
-import React, {createContext, useContext, useEffect, useRef, useState} from 'react';
+import {useIsFocused} from '@react-navigation/native';
+import React, {createContext, useContext, useState} from 'react';
 import type {Dispatch, SetStateAction} from 'react';
 import type {TextSelection} from '@components/Composer/types';
 import useOnyx from '@hooks/useOnyx';
@@ -22,8 +23,8 @@ type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
     setEditingMessage: Dispatch<SetStateAction<string | null>>;
     currentEditMessageSelection: TextSelection | null;
     setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
-    getEditingState: () => EditingState | null;
-    setEditingState: (state: EditingState | null) => void;
+    editingState: EditingState | null;
+    setEditingState: Dispatch<SetStateAction<EditingState | null>>;
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
@@ -34,7 +35,7 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     setEditingMessage: NOOP,
     currentEditMessageSelection: null,
     setCurrentEditMessageSelection: NOOP,
-    getEditingState: NOOP,
+    editingState: null,
     setEditingState: NOOP,
 });
 
@@ -46,125 +47,83 @@ type ReportActionEditMessageContextProviderProps = {
 };
 
 function ReportActionEditMessageContextProvider({reportID, parentReportID, parentReportAction, children}: ReportActionEditMessageContextProviderProps) {
+    const isFocused = useIsFocused();
+
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
     });
     const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
 
-    const [editingReportID, setEditingReportID] = useState<string | null>(null);
-    const [editingReportActionID, setEditingReportActionID] = useState<string | null>(null);
-    const [editingReportAction, setEditingReportAction] = useState<OnyxTypes.ReportAction | null>(null);
+    const [editingState, setEditingState] = useState<EditingState | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
     const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
-    const editingStateRef = useRef<EditingState | null>(null);
-    const getEditingState = () => {
-        return editingStateRef.current;
-    };
-    const setEditingState = (state: EditingState | null) => {
-        editingStateRef.current = state;
-    };
-
-    const updateActiveEditState = (activeEdit: ReportActionActiveEdit | null) => {
-        const newEditingReportID = activeEdit?.editingReportID ?? null;
-        const newEditingReportActionID = activeEdit?.editingReportActionID ?? null;
-        const newEditingReportAction = activeEdit?.editingReportAction ?? null;
-        const newEditingMessage = activeEdit?.editingMessage ?? null;
-
-        if (newEditingReportID !== editingReportID) {
-            setEditingReportID(newEditingReportID);
-        }
-
-        if (newEditingReportActionID !== editingReportActionID) {
-            setEditingReportActionID(newEditingReportActionID);
-        }
-        if (newEditingReportAction !== editingReportAction) {
-            setEditingReportAction(newEditingReportAction);
-        }
-        if (newEditingMessage !== editingMessage) {
-            setEditingMessage(newEditingMessage);
-        }
-    };
-
-    const setCurrentEditMessageSelection = (setSelectionStateAction: SetStateAction<TextSelection | null>) => {
-        if (!editingReportActionID) {
-            return;
-        }
-
-        setCurrentEditMessageSelectionState(setSelectionStateAction);
-    };
 
-    const reset = () => {
-        if (editingStateRef.current === 'editing') {
-            return;
-        }
+    const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
-        editingStateRef.current = null;
-        updateActiveEditState(null);
-        setCurrentEditMessageSelection(null);
-    };
+    let parentReportDrafts: OnyxTypes.ReportActionsDrafts | undefined;
+    if (parentReportAction && parentReportID) {
+        parentReportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${parentReportID}`];
+    }
 
-    // Initially set the editing report action state when the draft comments change
-    useEffect(() => {
-        const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
+    let editingReportID: string | null = null;
+    let editingReportActionID: string | null = null;
+    let editingReportAction: OnyxTypes.ReportAction | null = null;
 
-        let parentReportActionDrafts: OnyxTypes.ReportActionsDrafts | undefined;
-        if (parentReportAction && parentReportID) {
-            parentReportActionDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${parentReportID}`];
-        }
+    if (isFocused) {
+        if (parentReportAction && parentReportDrafts?.[parentReportAction.reportActionID]) {
+            const parentReportActionDraft = parentReportDrafts[parentReportAction.reportActionID];
 
-        if (!reportDrafts && !parentReportActionDrafts) {
-            reset();
-            return;
-        }
+            editingReportID = parentReportID ?? null;
+            editingReportActionID = parentReportAction.reportActionID;
+            editingReportAction = parentReportAction;
+            const nextMessage = parentReportActionDraft?.message ?? null;
 
-        let editReportID: string | undefined;
-        let reportActionID: string | undefined;
-        let reportAction: OnyxTypes.ReportAction | undefined;
-        let draft: OnyxTypes.ReportActionsDraft | undefined;
-
-        if (parentReportAction && parentReportActionDrafts) {
-            const parentReportActionDraft = parentReportActionDrafts[parentReportAction.reportActionID];
-            if (parentReportActionDraft) {
-                editReportID = parentReportID;
-                reportActionID = parentReportAction.reportActionID;
-                reportAction = parentReportAction;
-                draft = parentReportActionDraft;
+            if (editingState === null) {
+                setEditingState('editing');
             }
-        }
-
-        if (!reportActionID) {
-            const reportDraftEntry = reportDrafts ? Object.entries(reportDrafts).find(([, d]) => d?.message !== undefined) : undefined;
-
-            if (!reportDraftEntry) {
-                reset();
-                return;
+            if (editingMessage == null && editingMessage !== nextMessage) {
+                setEditingMessage(nextMessage);
+            }
+        } else if (reportDrafts) {
+            const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message !== undefined);
+
+            if (reportDraftEntry) {
+                const [reportActionIDOfDraft, reportActionDraft] = reportDraftEntry;
+
+                editingReportID = reportID ?? null;
+                editingReportActionID = reportActionIDOfDraft;
+                editingReportAction = reportActions?.[reportActionIDOfDraft] ?? null;
+                const nextMessage = reportActionDraft?.message ?? null;
+
+                if (editingState === null) {
+                    setEditingState('editing');
+                }
+                if (editingMessage == null && editingMessage !== nextMessage) {
+                    setEditingMessage(nextMessage);
+                }
             }
-
-            const [reportActionIDOfDraft, reportActionDraft] = reportDraftEntry;
-
-            editReportID = reportID;
-            reportActionID = reportActionIDOfDraft;
-            reportAction = reportActions?.[reportActionID];
-            draft = reportActionDraft;
         }
+    }
 
-        if (editingStateRef.current !== null) {
+    if (editingReportID == null && editingState !== null) {
+        setEditingState(null);
+        setEditingMessage(null);
+    }
+
+    const setCurrentEditMessageSelection = (setSelectionStateAction: SetStateAction<TextSelection | null>) => {
+        if (editingState !== 'editing') {
             return;
         }
 
-        editingStateRef.current = 'editing';
-        updateActiveEditState({
-            editingReportID: editReportID ?? null,
-            editingReportActionID: reportActionID,
-            editingReportAction: reportAction ?? null,
-            editingMessage: draft?.message ?? null,
-        });
-    }, [parentReportAction, parentReportID, reportActionDrafts, reportActions, reportID, reset, updateActiveEditState]);
+        setCurrentEditMessageSelectionState(setSelectionStateAction);
+    };
 
     return (
         <ReportActionEditMessageContext.Provider
             // eslint-disable-next-line react/jsx-no-constructed-context-values
             value={{
+                editingState,
+                setEditingState,
                 editingReportID,
                 editingReportActionID,
                 editingReportAction,
@@ -172,8 +131,6 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
                 setEditingMessage,
                 currentEditMessageSelection,
                 setCurrentEditMessageSelection,
-                getEditingState,
-                setEditingState,
             }}
         >
             {children}

From 0b1037cf0525ea2b3359bf442a72b1a3c2ac626b Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Mar 2026 12:51:15 +0000
Subject: [PATCH 109/233] feat: allow full size composer while editing

---
 .../AttachmentPickerWithMenuItems.tsx         | 81 +++------------
 .../ExpandCollapseComposerButton.tsx          | 98 +++++++++++++++++++
 .../MessageEditCancelButton.tsx               |  8 +-
 .../ReportActionCompose.tsx                   | 18 +++-
 .../report/ReportActionItemMessageEdit.tsx    |  5 +-
 5 files changed, 134 insertions(+), 76 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx

diff --git a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
index 1d1c9f603d0d..f3f526a20343 100644
--- a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
@@ -49,6 +49,7 @@ import ROUTES from '@src/ROUTES';
 import type {AnchorPosition} from '@src/styles';
 import type * as OnyxTypes from '@src/types/onyx';
 import type {FileObject} from '@src/types/utils/Attachment';
+import ExpandCollapseComposerButton from './ExpandCollapseComposerButton';
 
 type MoneyRequestOptions = Record<
     Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.CREATE | typeof CONST.IOU.TYPE.SPLIT_EXPENSE>,
@@ -134,21 +135,7 @@ function AttachmentPickerWithMenuItems({
     raiseIsScrollLikelyLayoutTriggered,
     shouldDisableAttachmentItem,
 }: AttachmentPickerWithMenuItemsProps) {
-    const icons = useMemoizedLazyExpensifyIcons([
-        'Cash',
-        'Coins',
-        'Collapse',
-        'Document',
-        'Expand',
-        'InvoiceGeneric',
-        'Location',
-        'MoneyCircle',
-        'Paperclip',
-        'Plus',
-        'Receipt',
-        'Task',
-        'Transfer',
-    ] as const);
+    const icons = useMemoizedLazyExpensifyIcons(['Cash', 'Coins', 'Document', 'InvoiceGeneric', 'Location', 'MoneyCircle', 'Paperclip', 'Plus', 'Receipt', 'Task', 'Transfer'] as const);
     const isFocused = useIsFocused();
     const theme = useTheme();
     const styles = useThemeStyles();
@@ -460,61 +447,15 @@ function AttachmentPickerWithMenuItems({
                                         </PressableWithFeedback>
                                     </Tooltip>
                                 </View>
-                                {(isFullComposerAvailable || isComposerFullSize) && (
-                                    <View style={expandCollapseButtonContainerStyles}>
-                                        {isComposerFullSize ? (
-                                            <Tooltip
-                                                text={translate('reportActionCompose.collapse')}
-                                                key="composer-collapse"
-                                            >
-                                                <PressableWithFeedback
-                                                    onPress={(e) => {
-                                                        e?.preventDefault();
-                                                        raiseIsScrollLikelyLayoutTriggered();
-                                                        setIsComposerFullSize(reportID, false);
-                                                    }}
-                                                    // Keep focus on the composer when Collapse button is clicked.
-                                                    onMouseDown={(e) => e.preventDefault()}
-                                                    style={styles.composerSizeButton}
-                                                    disabled={disabled}
-                                                    role={CONST.ROLE.BUTTON}
-                                                    accessibilityLabel={translate('reportActionCompose.collapse')}
-                                                    sentryLabel={CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_COLLAPSE_BUTTON}
-                                                >
-                                                    <Icon
-                                                        fill={theme.icon}
-                                                        src={icons.Collapse}
-                                                    />
-                                                </PressableWithFeedback>
-                                            </Tooltip>
-                                        ) : (
-                                            <Tooltip
-                                                text={translate('reportActionCompose.expand')}
-                                                key="composer-expand"
-                                            >
-                                                <PressableWithFeedback
-                                                    onPress={(e) => {
-                                                        e?.preventDefault();
-                                                        raiseIsScrollLikelyLayoutTriggered();
-                                                        setIsComposerFullSize(reportID, true);
-                                                    }}
-                                                    // Keep focus on the composer when Expand button is clicked.
-                                                    onMouseDown={(e) => e.preventDefault()}
-                                                    style={styles.composerSizeButton}
-                                                    disabled={disabled}
-                                                    role={CONST.ROLE.BUTTON}
-                                                    accessibilityLabel={translate('reportActionCompose.expand')}
-                                                    sentryLabel={CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_EXPAND_BUTTON}
-                                                >
-                                                    <Icon
-                                                        fill={theme.icon}
-                                                        src={icons.Expand}
-                                                    />
-                                                </PressableWithFeedback>
-                                            </Tooltip>
-                                        )}
-                                    </View>
-                                )}
+                                <ExpandCollapseComposerButton
+                                    isFullComposerAvailable={isFullComposerAvailable}
+                                    isComposerFullSize={isComposerFullSize}
+                                    reportID={reportID}
+                                    disabled={disabled}
+                                    raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered}
+                                    setIsComposerFullSize={setIsComposerFullSize}
+                                    style={expandCollapseButtonContainerStyles}
+                                />
                             </View>
                         </View>
                         <PopoverMenu
diff --git a/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx b/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx
new file mode 100644
index 000000000000..c3a3cdb4e5b9
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {ViewProps} from 'react-native';
+import Icon from '@components/Icon';
+import {PressableWithFeedback} from '@components/Pressable';
+import Tooltip from '@components/Tooltip';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type ExpandCollapseComposerButtonProps = ViewProps & {
+    isFullComposerAvailable: boolean;
+    isComposerFullSize: boolean;
+    reportID: string;
+    raiseIsScrollLikelyLayoutTriggered: () => void;
+    setIsComposerFullSize: (reportID: string, isFullSize: boolean) => void;
+    disabled?: boolean;
+};
+
+function ExpandCollapseComposerButton({
+    isFullComposerAvailable,
+    isComposerFullSize,
+    reportID,
+    disabled = false,
+    raiseIsScrollLikelyLayoutTriggered,
+    setIsComposerFullSize,
+    ...restProps
+}: ExpandCollapseComposerButtonProps) {
+    const {translate} = useLocalize();
+    const theme = useTheme();
+    const styles = useThemeStyles();
+    const icons = useMemoizedLazyExpensifyIcons(['Collapse', 'Expand'] as const);
+
+    if (!isFullComposerAvailable && !isComposerFullSize) {
+        return null;
+    }
+
+    return (
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        <View {...restProps}>
+            {isComposerFullSize ? (
+                <Tooltip
+                    text={translate('reportActionCompose.collapse')}
+                    key="composer-collapse"
+                >
+                    <PressableWithFeedback
+                        onPress={(e) => {
+                            e?.preventDefault();
+                            raiseIsScrollLikelyLayoutTriggered();
+                            setIsComposerFullSize(reportID, false);
+                        }}
+                        // Keep focus on the composer when Collapse button is clicked.
+                        onMouseDown={(e) => e.preventDefault()}
+                        style={styles.composerSizeButton}
+                        disabled={disabled}
+                        role={CONST.ROLE.BUTTON}
+                        accessibilityLabel={translate('reportActionCompose.collapse')}
+                        sentryLabel={CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_COLLAPSE_BUTTON}
+                    >
+                        <Icon
+                            fill={theme.icon}
+                            src={icons.Collapse}
+                        />
+                    </PressableWithFeedback>
+                </Tooltip>
+            ) : (
+                <Tooltip
+                    text={translate('reportActionCompose.expand')}
+                    key="composer-expand"
+                >
+                    <PressableWithFeedback
+                        onPress={(e) => {
+                            e?.preventDefault();
+                            raiseIsScrollLikelyLayoutTriggered();
+                            setIsComposerFullSize(reportID, true);
+                        }}
+                        // Keep focus on the composer when Expand button is clicked.
+                        onMouseDown={(e) => e.preventDefault()}
+                        style={styles.composerSizeButton}
+                        disabled={disabled}
+                        role={CONST.ROLE.BUTTON}
+                        accessibilityLabel={translate('reportActionCompose.expand')}
+                        sentryLabel={CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_EXPAND_BUTTON}
+                    >
+                        <Icon
+                            fill={theme.icon}
+                            src={icons.Expand}
+                        />
+                    </PressableWithFeedback>
+                </Tooltip>
+            )}
+        </View>
+    );
+}
+
+export default ExpandCollapseComposerButton;
diff --git a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
index 12bdb74b7c4c..92f6ed61f1fe 100644
--- a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import type {ViewProps} from 'react-native';
 import {View} from 'react-native';
 import Icon from '@components/Icon';
 import {PressableWithFeedback} from '@components/Pressable';
@@ -9,11 +10,11 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
 
-type MessageEditCancelButtonProps = {
+type MessageEditCancelButtonProps = ViewProps & {
     onCancel: () => void;
 };
 
-function MessageEditCancelButton({onCancel}: MessageEditCancelButtonProps) {
+function MessageEditCancelButton({onCancel, ...restProps}: MessageEditCancelButtonProps) {
     const styles = useThemeStyles();
     const theme = useTheme();
     const {translate} = useLocalize();
@@ -22,7 +23,8 @@ function MessageEditCancelButton({onCancel}: MessageEditCancelButtonProps) {
     const closeButtonStyles = [styles.composerSizeButton, {marginVertical: styles.composerSizeButton.marginHorizontal}];
 
     return (
-        <View style={[styles.justifyContentEnd, styles.mb1]}>
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        <View {...restProps}>
             <Tooltip text={translate('common.cancel')}>
                 <PressableWithFeedback
                     onPress={onCancel}
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 1716344d6b91..3a05e1ee0efc 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -1,6 +1,6 @@
 import noop from 'lodash/noop';
 import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
-import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInputSelectionChangeEvent} from 'react-native';
+import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInputSelectionChangeEvent, ViewStyle} from 'react-native';
 import {View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import {useSharedValue} from 'react-native-reanimated';
@@ -73,6 +73,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
 import type {ComposerWithSuggestionsProps, ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
+import ExpandCollapseComposerButton from './ExpandCollapseComposerButton';
 import MessageEditCancelButton from './MessageEditCancelButton';
 import ReportActionComposeSendButton from './ReportActionComposeSendButton';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
@@ -570,7 +571,20 @@ function ReportActionCompose({
                     >
                         {PDFValidationComponent}
                         {isEditingInComposer ? (
-                            <MessageEditCancelButton onCancel={deleteDraft} />
+                            <View
+                                style={[styles.dFlex, styles.alignItemsCenter, styles.flexWrap, styles.justifyContentCenter, {paddingVertical: styles.composerSizeButton.marginHorizontal}]}
+                            >
+                                <ExpandCollapseComposerButton
+                                    isFullComposerAvailable={isFullComposerAvailable}
+                                    isComposerFullSize={isComposerFullSize}
+                                    reportID={reportID}
+                                    disabled={isBlockedFromConcierge}
+                                    raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLayoutTriggered}
+                                    setIsComposerFullSize={setIsComposerFullSize}
+                                    style={[styles.flexGrow1, styles.flexShrink0]}
+                                />
+                                <MessageEditCancelButton onCancel={deleteDraft} />
+                            </View>
                         ) : (
                             <AttachmentPickerWithMenuItems
                                 onAttachmentPicked={(files) => validateAttachments({files})}
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 13e41c0e3573..e679f8c6a247 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -408,7 +408,10 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
                         isExceedingMaxLength && styles.borderColorDanger,
                     ]}
                 >
-                    <MessageEditCancelButton onCancel={deleteDraft} />
+                    <MessageEditCancelButton
+                        onCancel={deleteDraft}
+                        style={[styles.justifyContentEnd, styles.mb1]}
+                    />
                     <View style={[StyleUtils.getContainerComposeStyles(), styles.textInputComposeBorder]}>
                         <Composer
                             multiline

From 84718e957718e762c2fd6d6aa0f77f70a9b7751b Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 12 Mar 2026 13:04:49 +0000
Subject: [PATCH 110/233] feat: allow editing multiple parent reports

---
 .../report/ReportActionEditMessageContext.tsx | 31 ++++++++++++++++++-
 1 file changed, 30 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index b160265413af..bbbdae64b2ef 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native';
 import React, {createContext, useContext, useState} from 'react';
 import type {Dispatch, SetStateAction} from 'react';
 import type {TextSelection} from '@components/Composer/types';
+import useAncestors from '@hooks/useAncestors';
 import useOnyx from '@hooks/useOnyx';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
@@ -49,10 +50,12 @@ type ReportActionEditMessageContextProviderProps = {
 function ReportActionEditMessageContextProvider({reportID, parentReportID, parentReportAction, children}: ReportActionEditMessageContextProviderProps) {
     const isFocused = useIsFocused();
 
+    const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
     });
     const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
+    const ancestors = useAncestors(report);
 
     const [editingState, setEditingState] = useState<EditingState | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
@@ -70,7 +73,33 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
     let editingReportAction: OnyxTypes.ReportAction | null = null;
 
     if (isFocused) {
-        if (parentReportAction && parentReportDrafts?.[parentReportAction.reportActionID]) {
+        const ancestorWithDraft = [...ancestors]
+            .slice()
+            .reverse()
+            .find(({report: ancestorReport, reportAction}) => {
+                const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorReport.reportID}`];
+                const ancestorDraft = ancestorDrafts?.[reportAction.reportActionID];
+
+                return ancestorDraft?.message !== undefined;
+            });
+
+        if (ancestorWithDraft) {
+            const {report: ancestorReport, reportAction: ancestorReportAction} = ancestorWithDraft;
+            const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorReport.reportID}`];
+            const ancestorReportActionDraft = ancestorDrafts?.[ancestorReportAction.reportActionID];
+
+            editingReportID = ancestorReport.reportID;
+            editingReportActionID = ancestorReportAction.reportActionID;
+            editingReportAction = ancestorReportAction;
+            const nextMessage = ancestorReportActionDraft?.message ?? null;
+
+            if (editingState === null) {
+                setEditingState('editing');
+            }
+            if (editingMessage == null && editingMessage !== nextMessage) {
+                setEditingMessage(nextMessage);
+            }
+        } else if (parentReportAction && parentReportDrafts?.[parentReportAction.reportActionID]) {
             const parentReportActionDraft = parentReportDrafts[parentReportAction.reportActionID];
 
             editingReportID = parentReportID ?? null;

From fc0f17cc18635817d1f66f220cc68fa497426521 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Mar 2026 15:05:27 +0100
Subject: [PATCH 111/233] refactor: split context into separate state and
 actions

---
 .../ComposerWithSuggestions.tsx               |  6 +--
 .../ReportActionCompose/useEditMessage.ts     |  4 +-
 .../report/ReportActionEditMessageContext.tsx | 52 ++++++++++++-------
 .../report/ReportActionItemMessageEdit.tsx    |  5 +-
 4 files changed, 40 insertions(+), 27 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index c56f1d41bfd9..44d27a6326f4 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -38,7 +38,7 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag
 import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUtils';
 import updateMultilineInputRange from '@libs/updateMultilineInputRange';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
-import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
+import {useReportActionActiveEdit, useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext';
 import useDraftMessageVideoAttributeCache from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
@@ -253,8 +253,8 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingState, editingReportActionID, editingReportAction, editingMessage, setEditingMessage, currentEditMessageSelection, setCurrentEditMessageSelection} =
-        useReportActionActiveEdit();
+    const {editingState, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
+    const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
 
     const [value, setValue] = useState(() => {
         const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index c6402930b653..1a87463e27e7 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -15,7 +15,7 @@ import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import {getOriginalReportID} from '@libs/ReportUtils';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
-import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
+import {useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext';
 import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
@@ -47,7 +47,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
     const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
     const ancestors = useAncestors(originalReport);
 
-    const {setEditingState} = useReportActionActiveEdit();
+    const {setEditingState} = useReportActionActiveEditActions();
 
     useEffect(() => {
         // required for keeping last state of isFocused variable
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index bbbdae64b2ef..ecf722468ca1 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -21,11 +21,14 @@ type ReportActionActiveEdit = {
 };
 
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
-    setEditingMessage: Dispatch<SetStateAction<string | null>>;
     currentEditMessageSelection: TextSelection | null;
-    setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
     editingState: EditingState | null;
+};
+
+type ReportActionEditMessageContextActions = {
     setEditingState: Dispatch<SetStateAction<EditingState | null>>;
+    setEditingMessage: Dispatch<SetStateAction<string | null>>;
+    setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
@@ -33,11 +36,14 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingReportActionID: null,
     editingReportAction: null,
     editingMessage: null,
-    setEditingMessage: NOOP,
     currentEditMessageSelection: null,
-    setCurrentEditMessageSelection: NOOP,
     editingState: null,
+});
+
+const ReportActionEditMessageActionsContext = createContext<ReportActionEditMessageContextActions>({
     setEditingState: NOOP,
+    setEditingMessage: NOOP,
+    setCurrentEditMessageSelection: NOOP,
 });
 
 type ReportActionEditMessageContextProviderProps = {
@@ -147,22 +153,24 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
         setCurrentEditMessageSelectionState(setSelectionStateAction);
     };
 
+    const reportActionEditMessageContextValue: ReportActionEditMessageContextValue = {
+        editingState,
+        editingReportID,
+        editingReportActionID,
+        editingReportAction,
+        editingMessage,
+        currentEditMessageSelection,
+    };
+
+    const actions: ReportActionEditMessageContextActions = {
+        setEditingState,
+        setEditingMessage,
+        setCurrentEditMessageSelection,
+    };
+
     return (
-        <ReportActionEditMessageContext.Provider
-            // eslint-disable-next-line react/jsx-no-constructed-context-values
-            value={{
-                editingState,
-                setEditingState,
-                editingReportID,
-                editingReportActionID,
-                editingReportAction,
-                editingMessage,
-                setEditingMessage,
-                currentEditMessageSelection,
-                setCurrentEditMessageSelection,
-            }}
-        >
-            {children}
+        <ReportActionEditMessageContext.Provider value={reportActionEditMessageContextValue}>
+            <ReportActionEditMessageActionsContext.Provider value={actions}>{children}</ReportActionEditMessageActionsContext.Provider>
         </ReportActionEditMessageContext.Provider>
     );
 }
@@ -171,5 +179,9 @@ function useReportActionActiveEdit() {
     return useContext(ReportActionEditMessageContext);
 }
 
-export {ReportActionEditMessageContextProvider, useReportActionActiveEdit, ReportActionEditMessageContext};
+function useReportActionActiveEditActions() {
+    return useContext(ReportActionEditMessageActionsContext);
+}
+
+export {ReportActionEditMessageContextProvider, useReportActionActiveEdit, useReportActionActiveEditActions, ReportActionEditMessageContext};
 export type {ReportActionActiveEdit, ReportActionEditMessageContextValue};
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index e679f8c6a247..3722246f4ff8 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -44,7 +44,7 @@ import SendOrSaveButton from './ReportActionCompose/SendOrSaveButton';
 import Suggestions from './ReportActionCompose/Suggestions';
 import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './ReportActionCompose/useEditMessage';
-import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
+import {useReportActionActiveEdit, useReportActionActiveEditActions} from './ReportActionEditMessageContext';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
 import useDraftMessageVideoAttributeCache from './useDraftMessageVideoAttributeCache';
 
@@ -96,7 +96,8 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
     const tag = useSharedValue(-1);
     const emojisPresentBefore = useRef<Emoji[]>([]);
 
-    const {currentEditMessageSelection, setCurrentEditMessageSelection, editingMessage, setEditingMessage} = useReportActionActiveEdit();
+    const {currentEditMessageSelection, editingMessage} = useReportActionActiveEdit();
+    const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
     const [draft, setDraft] = useState(() => {
         if (editingMessage) {
             emojisPresentBefore.current = extractEmojis(editingMessage);

From 8865cb1e85cbc7e00d499770bde223563c402d9f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Mar 2026 15:38:24 +0100
Subject: [PATCH 112/233] fix: update editing state if report action changes

---
 .../report/ReportActionEditMessageContext.tsx | 25 +++++++++++--------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index ecf722468ca1..ed8c9e8c4092 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -89,6 +89,15 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
                 return ancestorDraft?.message !== undefined;
             });
 
+        const updateMessage = (nextMessage: string | null) => {
+            const isInitialEdit = editingMessage == null && editingMessage !== nextMessage;
+            const didReportActionChange = prevEditingReportActionID !== editingReportActionID;
+            if (isInitialEdit || didReportActionChange) {
+                setEditingMessage(nextMessage);
+                setPrevEditingReportActionID(editingReportActionID);
+            }
+        };
+
         if (ancestorWithDraft) {
             const {report: ancestorReport, reportAction: ancestorReportAction} = ancestorWithDraft;
             const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorReport.reportID}`];
@@ -97,14 +106,10 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
             editingReportID = ancestorReport.reportID;
             editingReportActionID = ancestorReportAction.reportActionID;
             editingReportAction = ancestorReportAction;
-            const nextMessage = ancestorReportActionDraft?.message ?? null;
 
             if (editingState === null) {
                 setEditingState('editing');
             }
-            if (editingMessage == null && editingMessage !== nextMessage) {
-                setEditingMessage(nextMessage);
-            }
         } else if (parentReportAction && parentReportDrafts?.[parentReportAction.reportActionID]) {
             const parentReportActionDraft = parentReportDrafts[parentReportAction.reportActionID];
 
@@ -116,9 +121,8 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
             if (editingState === null) {
                 setEditingState('editing');
             }
-            if (editingMessage == null && editingMessage !== nextMessage) {
-                setEditingMessage(nextMessage);
-            }
+            const nextMessage = ancestorReportActionDraft?.message ?? null;
+            updateMessage(nextMessage);
         } else if (reportDrafts) {
             const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message !== undefined);
 
@@ -128,14 +132,13 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
                 editingReportID = reportID ?? null;
                 editingReportActionID = reportActionIDOfDraft;
                 editingReportAction = reportActions?.[reportActionIDOfDraft] ?? null;
-                const nextMessage = reportActionDraft?.message ?? null;
 
                 if (editingState === null) {
                     setEditingState('editing');
                 }
-                if (editingMessage == null && editingMessage !== nextMessage) {
-                    setEditingMessage(nextMessage);
-                }
+
+                const nextMessage = reportActionDraft?.message ?? null;
+                updateMessage(nextMessage);
             }
         }
     }

From 34240c3522be9c9eb7050def64234ad34c50747a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Mar 2026 15:39:15 +0100
Subject: [PATCH 113/233] refactor: remove redundant `parentReportAction` path
 in favor of ancestors

---
 src/pages/inbox/ReportScreen.tsx              |  6 +-----
 .../report/ReportActionEditMessageContext.tsx | 20 ++-----------------
 2 files changed, 3 insertions(+), 23 deletions(-)

diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index a47d2b80c598..881ee186f6c4 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -1029,11 +1029,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
     return (
         // Wide RHP overlays should be rendered only for the report screen displayed in RHP
         <WideRHPOverlayWrapper shouldWrap={route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT}>
-            <ReportActionEditMessageContextProvider
-                reportID={reportID}
-                parentReportID={reportOnyx?.parentReportID}
-                parentReportAction={parentReportAction}
-            >
+            <ReportActionEditMessageContextProvider reportID={reportID}>
                 <ActionListContext.Provider value={actionListValue}>
                     <ReactionListWrapper>
                         <ScreenWrapper
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index ed8c9e8c4092..2959e8979c06 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -48,12 +48,10 @@ const ReportActionEditMessageActionsContext = createContext<ReportActionEditMess
 
 type ReportActionEditMessageContextProviderProps = {
     reportID: string | undefined;
-    parentReportID: string | undefined;
-    parentReportAction: OnyxTypes.ReportAction | undefined;
     children: React.ReactNode;
 };
 
-function ReportActionEditMessageContextProvider({reportID, parentReportID, parentReportAction, children}: ReportActionEditMessageContextProviderProps) {
+function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
     const isFocused = useIsFocused();
 
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
@@ -64,16 +62,12 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
     const ancestors = useAncestors(report);
 
     const [editingState, setEditingState] = useState<EditingState | null>(null);
+    const [prevEditingReportActionID, setPrevEditingReportActionID] = useState<string | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
     const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
 
     const reportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`];
 
-    let parentReportDrafts: OnyxTypes.ReportActionsDrafts | undefined;
-    if (parentReportAction && parentReportID) {
-        parentReportDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${parentReportID}`];
-    }
-
     let editingReportID: string | null = null;
     let editingReportActionID: string | null = null;
     let editingReportAction: OnyxTypes.ReportAction | null = null;
@@ -110,17 +104,7 @@ function ReportActionEditMessageContextProvider({reportID, parentReportID, paren
             if (editingState === null) {
                 setEditingState('editing');
             }
-        } else if (parentReportAction && parentReportDrafts?.[parentReportAction.reportActionID]) {
-            const parentReportActionDraft = parentReportDrafts[parentReportAction.reportActionID];
-
-            editingReportID = parentReportID ?? null;
-            editingReportActionID = parentReportAction.reportActionID;
-            editingReportAction = parentReportAction;
-            const nextMessage = parentReportActionDraft?.message ?? null;
 
-            if (editingState === null) {
-                setEditingState('editing');
-            }
             const nextMessage = ancestorReportActionDraft?.message ?? null;
             updateMessage(nextMessage);
         } else if (reportDrafts) {

From 16f7df3846c0cc1e2c0ab9fb408099f6b1018e73 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Mar 2026 16:02:56 +0100
Subject: [PATCH 114/233] fix: text selection on comment update and refactor

---
 .../ComposerWithSuggestions.tsx               | 25 ++++++-------------
 .../report/ReportActionEditMessageContext.tsx |  8 +++++-
 2 files changed, 15 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 44d27a6326f4..b8b51acc4dab 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -282,12 +282,6 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
-    const wasEditing = useRef(!!editingReportActionID);
-
-    const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
-    const previousEditingReportActionIDRef = useRef<string | null>(editingReportActionID ?? null);
-    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
-
     const updateSelectionImperatively = useCallback((start: number, end: number) => {
         if (!isIOSNative) {
             return;
@@ -331,17 +325,16 @@ function ComposerWithSuggestions({
         [currentEditMessageSelection, updateSelectionImperatively],
     );
 
+    const wasEditing = useRef(!!editingReportActionID);
+    const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
+    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
+
     useEffect(() => {
         if (editingState === 'submitted') {
             return;
         }
 
         const isEditing = editingState === 'editing';
-        const previousEditingReportActionID = previousEditingReportActionIDRef.current;
-        const currentEditingReportActionID = editingReportActionID ?? null;
-        const didChangeEditedAction = isEditing && previousEditingReportActionID && currentEditingReportActionID && previousEditingReportActionID !== currentEditingReportActionID;
-
-        previousEditingReportActionIDRef.current = currentEditingReportActionID;
 
         if (!isEditing) {
             if (wasEditing.current && wasEditingInComposerRef.current) {
@@ -373,12 +366,6 @@ function ComposerWithSuggestions({
             return;
         }
 
-        // We are already in editing mode, but the target message changed.
-        if (didChangeEditedAction && shouldUseNarrowLayout) {
-            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
-            return;
-        }
-
         // Editing is ongoing and layout toggled from wide to narrow.
         if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = true;
@@ -392,6 +379,10 @@ function ComposerWithSuggestions({
             wasEditingInComposerRef.current = false;
             applyComposerValue(draftComment ?? '');
         }
+
+        if (shouldUseNarrowLayout) {
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
+        }
     }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 2959e8979c06..fe044e89dc6f 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -84,11 +84,17 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
             });
 
         const updateMessage = (nextMessage: string | null) => {
-            const isInitialEdit = editingMessage == null && editingMessage !== nextMessage;
+            if (nextMessage == null) {
+                return;
+            }
+
+            const isInitialEdit = editingMessage == null;
             const didReportActionChange = prevEditingReportActionID !== editingReportActionID;
             if (isInitialEdit || didReportActionChange) {
                 setEditingMessage(nextMessage);
                 setPrevEditingReportActionID(editingReportActionID);
+                const defaultSelection: TextSelection = {start: nextMessage.length, end: nextMessage.length};
+                setCurrentEditMessageSelectionState(defaultSelection);
             }
         };
 

From 0ca44fce374e440e9cb60f40ec5250f5568c4817 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 17 Mar 2026 16:14:55 +0100
Subject: [PATCH 115/233] fix: TS checks

---
 .../MoneyRequestReportActionsList.tsx                       | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
index b2b500a86f67..e1628aeef4da 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
@@ -58,7 +58,7 @@ import {
     isReportActionVisible,
     wasMessageReceivedWhileOffline,
 } from '@libs/ReportActionsUtils';
-import {canUserPerformWriteAction, chatIncludesChronosWithID, getOriginalReportID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils';
+import {canUserPerformWriteAction, chatIncludesChronosWithID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils';
 import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';
 import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
 import {isTransactionPendingDelete} from '@libs/TransactionUtils';
@@ -684,8 +684,6 @@ function MoneyRequestReportActionsList({
                 !isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) &&
                 hasNextActionMadeBySameActor(visibleReportActions, index);
 
-            const originalReportID = getOriginalReportID(report.reportID, reportAction, reportActionsObject);
-
             return (
                 <ReportActionsListItemRenderer
                     reportAction={reportAction}
@@ -705,7 +703,6 @@ function MoneyRequestReportActionsList({
                     isUserValidated={isUserValidated}
                     personalDetails={personalDetails}
                     userBillingFundID={userBillingFundID}
-                    originalReportID={originalReportID}
                     isReportArchived={isReportArchived}
                     isTryNewDotNVPDismissed={isTryNewDotNVPDismissed}
                     reportNameValuePairsOrigin={reportNameValuePairs?.origin}
@@ -715,7 +712,6 @@ function MoneyRequestReportActionsList({
         },
         [
             visibleReportActions,
-            reportActionsObject,
             parentReportAction,
             report,
             transactionThreadReport,

From 58de521658eb51b9691c1b0e1e6b0e7e2230a167 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 11:52:21 +0100
Subject: [PATCH 116/233] refactor: rename callback

---
 .../report/ReportActionCompose/ReportActionCompose.tsx    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 77ccdab0bd2b..04ad95fb393e 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -464,7 +464,7 @@ function ReportActionCompose({
     // useSharedValue on web doesn't support functions, so we need to wrap it in an object.
     const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
 
-    const sendMessage = useCallback(() => {
+    const submitDraft = useCallback(() => {
         if (isSendDisabled || !debouncedCommentMaxLengthValidation.flush()) {
             return;
         }
@@ -490,7 +490,7 @@ function ReportActionCompose({
             clearWorklet?.();
         });
     }, [isSendDisabled, debouncedCommentMaxLengthValidation, isComposerFullSize, isEditingInComposer, effectiveDraft, draftComment, reportID, submitForm, composerRefShared]);
-    onSubmitAction = sendMessage;
+    onSubmitAction = submitDraft;
 
     const emojiPositionValues = useMemo(
         () => ({
@@ -638,7 +638,7 @@ function ReportActionCompose({
                             onClear={submitForm}
                             disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
                             setIsCommentEmpty={setIsCommentEmpty}
-                            onEnterKeyPress={sendMessage}
+                            onEnterKeyPress={submitDraft}
                             onFocus={onFocus}
                             onBlur={onBlur}
                             measureParentContainer={measureContainer}
@@ -685,7 +685,7 @@ function ReportActionCompose({
                         <ReportActionComposeSendButton
                             isEditing={isEditingInComposer}
                             isDisabled={isSendDisabled}
-                            onSend={sendMessage}
+                            onSend={submitDraft}
                         />
                     </View>
                     {ErrorModal}

From e559a9f8541703ace997226e2c9e1e4fa2b620cc Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 12:13:06 +0100
Subject: [PATCH 117/233] fix: reset whole state on edit end

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index fe044e89dc6f..b7bfe0e5c2cd 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -136,6 +136,8 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     if (editingReportID == null && editingState !== null) {
         setEditingState(null);
         setEditingMessage(null);
+        setPrevEditingReportActionID(null);
+        setCurrentEditMessageSelectionState(null);
     }
 
     const setCurrentEditMessageSelection = (setSelectionStateAction: SetStateAction<TextSelection | null>) => {

From 03c9d879ba33546cf9c67e49600f9343fe2cf06f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 12:14:06 +0100
Subject: [PATCH 118/233] fix: composer height on edit

---
 .../ReportActionCompose.tsx                   | 25 ++++++++++++++++---
 1 file changed, 21 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 04ad95fb393e..b69fba1cde1a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -168,7 +168,19 @@ function ReportActionCompose({
 
     const {editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
-    const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID;
+    const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
+    const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID && !didResetComposerHeight;
+
+    console.log({didResetComposerHeight, isEditingInComposer, editingReportActionID});
+
+    useEffect(() => {
+        if (editingReportActionID || !didResetComposerHeight) {
+            return;
+        }
+
+        setDidResetComposerHeight(false);
+    }, [didResetComposerHeight, editingReportActionID]);
+
     const effectiveDraft = shouldUseNarrowLayout ? editingMessage : draftComment;
 
     const reportActionEntries = useMemo(() => (reportActions ? Object.entries(reportActions) : []), [reportActions]);
@@ -351,7 +363,8 @@ function ReportActionCompose({
         (draftMessage: string) => {
             const draftMessageTrimmed = draftMessage.trim();
 
-            if (isEditingInComposer && !attachmentFileRef.current) {
+            const isSubmittingEdit = isEditingInComposer || didResetComposerHeight;
+            if (isSubmittingEdit && !attachmentFileRef.current) {
                 publishDraft(draftMessageTrimmed);
                 return;
             }
@@ -398,9 +411,10 @@ function ReportActionCompose({
         },
         [
             isEditingInComposer,
-            publishDraft,
+            didResetComposerHeight,
             shouldShowConciergeIndicator,
             kickoffWaitingIndicator,
+            publishDraft,
             transactionThreadReport,
             report,
             reportID,
@@ -408,8 +422,8 @@ function ReportActionCompose({
             currentUserPersonalDetails.accountID,
             personalDetail.timezone,
             isInSidePanel,
-            onSubmit,
             scrollOffsetRef,
+            onSubmit,
         ],
     );
 
@@ -479,6 +493,9 @@ function ReportActionCompose({
         }
 
         composerRef.current?.resetHeight();
+        if (isEditingInComposer) {
+            setDidResetComposerHeight(true);
+        }
 
         scheduleOnUI(() => {
             const {clearWorklet} = composerRefShared.get();

From 99f85eb68c82bdf2e54ba439da27d299f98bf9ca Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 12:15:16 +0100
Subject: [PATCH 119/233] remove console.log

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx    | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index b69fba1cde1a..205c51502012 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -171,8 +171,6 @@ function ReportActionCompose({
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID && !didResetComposerHeight;
 
-    console.log({didResetComposerHeight, isEditingInComposer, editingReportActionID});
-
     useEffect(() => {
         if (editingReportActionID || !didResetComposerHeight) {
             return;

From 4142b734ba2c94fb490217e91ddf604786ef42cd Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 13:39:37 +0100
Subject: [PATCH 120/233] fix: TS and ESLint errors

---
 .../MoneyRequestReportActionsList.tsx                    | 9 ---------
 tests/ui/ReportActionItemMessageEditTest.tsx             | 1 -
 tests/utils/ReportActionComposeUtils.tsx                 | 1 -
 3 files changed, 11 deletions(-)

diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
index e1628aeef4da..34f50330041e 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
@@ -175,15 +175,6 @@ function MoneyRequestReportActionsList({
 
     const transactionsWithoutPendingDelete = useMemo(() => transactions.filter((t) => !isTransactionPendingDelete(t)), [transactions]);
     const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]);
-    // reportActions is passed as an array because it's sorted chronologically for FlatList rendering and pagination.
-    // However, getOriginalReportID expects the Onyx object format (keyed by reportActionID) for efficient lookups.
-    const reportActionsObject = useMemo(() => {
-        const obj: OnyxTypes.ReportActions = {};
-        for (const action of reportActions) {
-            obj[action.reportActionID] = action;
-        }
-        return obj;
-    }, [reportActions]);
     const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], false, reportTransactionIDs);
     const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]);
     const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`);
diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index 5587f9027f79..df672efbe6ab 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -51,7 +51,6 @@ TestHelper.setupGlobalFetchMock();
 const defaultReport = LHNTestUtils.getFakeReport();
 const defaultProps: ReportActionItemMessageEditProps = {
     action: LHNTestUtils.getFakeReportAction(),
-    draftMessage: '',
     reportID: defaultReport.reportID,
     originalReportID: defaultReport.reportID,
     index: 0,
diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index 763e84b1e5db..c7a42c5a149e 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -48,7 +48,6 @@ const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) =>
 
 const defaultReportActionItemMessageEditProps: ReportActionItemMessageEditProps = {
     action: LHNTestUtils.getFakeReportAction(),
-    draftMessage: '',
     reportID: defaultReport.reportID,
     originalReportID: defaultReport.reportID,
     index: 0,

From c090745409b590d1bdce3c8b7ce38907ae506b12 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:28:37 +0100
Subject: [PATCH 121/233] fix: send button disabled state

---
 .../ReportActionCompose.tsx                   | 88 ++++++++++---------
 1 file changed, 45 insertions(+), 43 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 205c51502012..54ac7d61148b 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -179,8 +179,6 @@ function ReportActionCompose({
         setDidResetComposerHeight(false);
     }, [didResetComposerHeight, editingReportActionID]);
 
-    const effectiveDraft = shouldUseNarrowLayout ? editingMessage : draftComment;
-
     const reportActionEntries = useMemo(() => (reportActions ? Object.entries(reportActions) : []), [reportActions]);
     const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionEntries.at(-1)?.[0], [editingReportActionID, reportActionEntries]);
 
@@ -201,6 +199,7 @@ function ReportActionCompose({
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
+    const effectiveDraft = isEditingInComposer ? editingMessage : draftComment;
     const [isCommentEmpty, setIsCommentEmpty] = useState(() => {
         return !effectiveDraft || !!effectiveDraft.match(CONST.REGEX.EMPTY_COMMENT);
     });
@@ -354,10 +353,17 @@ function ReportActionCompose({
         composerRef,
     });
 
+    const isSubmittingDraftCommentDisabled = isBlockedFromConcierge || isExceedingMaxLength || isCommentEmpty;
+    const isSendDisabled = !isEditingInComposer && isSubmittingDraftCommentDisabled;
+
+    // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value
+    // useSharedValue on web doesn't support functions, so we need to wrap it in an object.
+    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
+
     /**
      * Add or edit a comment in the composer
      */
-    const submitForm = useCallback(
+    const validateAndSubmitDraft = useCallback(
         (draftMessage: string) => {
             const draftMessageTrimmed = draftMessage.trim();
 
@@ -425,6 +431,39 @@ function ReportActionCompose({
         ],
     );
 
+    const submitDraftAndClearComposer = useCallback(() => {
+        if (isSendDisabled || !debouncedCommentMaxLengthValidation.flush()) {
+            return;
+        }
+
+        if (isComposerFullSize) {
+            setIsComposerFullSize(reportID, false);
+        }
+
+        // If there is a draft comment and we are submitting an edit, we don't want to clear the composer height and the draft comment.
+        // Therefore, we directly trigger the validation and submission of the draft comment.
+        if (isEditingInComposer && editingMessage !== null && draftComment) {
+            validateAndSubmitDraft(editingMessage);
+            return;
+        }
+
+        composerRef.current?.resetHeight();
+        if (isEditingInComposer) {
+            setDidResetComposerHeight(true);
+        }
+
+        scheduleOnUI(() => {
+            const {clearWorklet} = composerRefShared.get();
+
+            if (!clearWorklet) {
+                throw new Error('The composerRef.clearWorklet function is not set yet. This should never happen, and indicates a developer error.');
+            }
+
+            clearWorklet?.();
+        });
+    }, [isSendDisabled, debouncedCommentMaxLengthValidation, isEditingInComposer, editingMessage, draftComment, isComposerFullSize, validateAndSubmitDraft, reportID, composerRefShared]);
+    onSubmitAction = submitDraftAndClearComposer;
+
     const onTriggerAttachmentPicker = useCallback(() => {
         isNextModalWillOpenRef.current = true;
         isKeyboardVisibleWhenShowingModalRef.current = true;
@@ -470,43 +509,6 @@ function ReportActionCompose({
 
     const hasReportRecipient = !isEmptyObject(reportRecipient);
 
-    const isSendDisabled = !isEditingInComposer && (isBlockedFromConcierge || isExceedingMaxLength || isCommentEmpty);
-
-    // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value
-    // useSharedValue on web doesn't support functions, so we need to wrap it in an object.
-    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
-
-    const submitDraft = useCallback(() => {
-        if (isSendDisabled || !debouncedCommentMaxLengthValidation.flush()) {
-            return;
-        }
-
-        if (isComposerFullSize) {
-            setIsComposerFullSize(reportID, false);
-        }
-
-        if (isEditingInComposer && effectiveDraft && draftComment) {
-            submitForm(effectiveDraft);
-            return;
-        }
-
-        composerRef.current?.resetHeight();
-        if (isEditingInComposer) {
-            setDidResetComposerHeight(true);
-        }
-
-        scheduleOnUI(() => {
-            const {clearWorklet} = composerRefShared.get();
-
-            if (!clearWorklet) {
-                throw new Error('The composerRef.clearWorklet function is not set yet. This should never happen, and indicates a developer error.');
-            }
-
-            clearWorklet?.();
-        });
-    }, [isSendDisabled, debouncedCommentMaxLengthValidation, isComposerFullSize, isEditingInComposer, effectiveDraft, draftComment, reportID, submitForm, composerRefShared]);
-    onSubmitAction = submitDraft;
-
     const emojiPositionValues = useMemo(
         () => ({
             secondaryRowHeight: styles.chatItemComposeSecondaryRow.height,
@@ -650,10 +652,10 @@ function ReportActionCompose({
                             isComposerFullSize={isComposerFullSize}
                             setIsFullComposerAvailable={setIsFullComposerAvailable}
                             onPasteFile={(files) => validateAttachments({files})}
-                            onClear={submitForm}
+                            onClear={validateAndSubmitDraft}
                             disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
                             setIsCommentEmpty={setIsCommentEmpty}
-                            onEnterKeyPress={submitDraft}
+                            onEnterKeyPress={submitDraftAndClearComposer}
                             onFocus={onFocus}
                             onBlur={onBlur}
                             measureParentContainer={measureContainer}
@@ -700,7 +702,7 @@ function ReportActionCompose({
                         <ReportActionComposeSendButton
                             isEditing={isEditingInComposer}
                             isDisabled={isSendDisabled}
-                            onSend={submitDraft}
+                            onSend={submitDraftAndClearComposer}
                         />
                     </View>
                     {ErrorModal}

From 8c544f4fc9ac204d98da1d6f7976a17fd97a0ed3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:47:25 +0100
Subject: [PATCH 122/233] fix: send button disabled after editing

---
 .../ReportActionCompose/ComposerWithSuggestions.tsx       | 8 +++++---
 .../report/ReportActionCompose/ReportActionCompose.tsx    | 5 +++--
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index b8b51acc4dab..a3b45884c284 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -256,8 +256,9 @@ function ComposerWithSuggestions({
     const {editingState, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
     const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
 
+    const isEditingInComposer = shouldUseNarrowLayout && editingState !== null;
     const [value, setValue] = useState(() => {
-        const initialValue = shouldUseNarrowLayout ? (editingMessage ?? draftComment) : draftComment;
+        const initialValue = isEditingInComposer ? (editingMessage ?? draftComment) : draftComment;
 
         if (initialValue) {
             emojisPresentBefore.current = extractEmojis(initialValue);
@@ -383,7 +384,7 @@ function ComposerWithSuggestions({
         if (shouldUseNarrowLayout) {
             applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
+    }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -585,7 +586,7 @@ function ComposerWithSuggestions({
             const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/);
 
             /** Only update isCommentEmpty state if it's different from previous one */
-            if (isNewCommentEmpty !== isPrevCommentEmpty) {
+            if (!isEditingInComposer && isNewCommentEmpty !== isPrevCommentEmpty) {
                 setIsCommentEmpty(isNewCommentEmpty);
             }
             emojisPresentBefore.current = emojis;
@@ -639,6 +640,7 @@ function ComposerWithSuggestions({
             editingReportActionID,
             editingState,
             findNewlyAddedChars,
+            isEditingInComposer,
             preferredLocale,
             preferredSkinTone,
             raiseIsScrollLikelyLayoutTriggered,
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 54ac7d61148b..b930b264418d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -166,11 +166,12 @@ function ReportActionCompose({
         canEvict: false,
     });
 
-    const {editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
+    const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
-    const isEditingInComposer = shouldUseNarrowLayout && !!editingReportActionID && !didResetComposerHeight;
+    const isEditingInComposer = shouldUseNarrowLayout && editingState !== null && !didResetComposerHeight;
 
+    console.log({isEditingInComposer, editingState, editingReportActionID});
     useEffect(() => {
         if (editingReportActionID || !didResetComposerHeight) {
             return;

From 33c37bc15ee16313195fb5edd4dc6d66bae2d6ee Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:47:40 +0100
Subject: [PATCH 123/233] refactor: use editing state instead of report action
 id

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx    | 4 ++--
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx  | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index a3b45884c284..a8d5ade6c6ed 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -271,7 +271,7 @@ function ComposerWithSuggestions({
 
     useDraftMessageVideoAttributeCache({
         draftMessage: value,
-        isEditing: !!editingReportActionID,
+        isEditing: editingState !== null,
         editingReportAction,
         updateDraftMessage: setValue,
         isEditInProgressRef: isDraftPendingSaved,
@@ -326,7 +326,7 @@ function ComposerWithSuggestions({
         [currentEditMessageSelection, updateSelectionImperatively],
     );
 
-    const wasEditing = useRef(!!editingReportActionID);
+    const wasEditing = useRef(editingState !== null);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index b930b264418d..edc045389711 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -173,12 +173,12 @@ function ReportActionCompose({
 
     console.log({isEditingInComposer, editingState, editingReportActionID});
     useEffect(() => {
-        if (editingReportActionID || !didResetComposerHeight) {
+        if (editingState !== null || !didResetComposerHeight) {
             return;
         }
 
         setDidResetComposerHeight(false);
-    }, [didResetComposerHeight, editingReportActionID]);
+    }, [didResetComposerHeight, editingState]);
 
     const reportActionEntries = useMemo(() => (reportActions ? Object.entries(reportActions) : []), [reportActions]);
     const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionEntries.at(-1)?.[0], [editingReportActionID, reportActionEntries]);

From 4c13fd2ad3d1c4386294ab49e5b8c70554936653 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:47:56 +0100
Subject: [PATCH 124/233] fix: remove console.log

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx     | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index edc045389711..27e0346cfbf1 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -171,7 +171,6 @@ function ReportActionCompose({
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     const isEditingInComposer = shouldUseNarrowLayout && editingState !== null && !didResetComposerHeight;
 
-    console.log({isEditingInComposer, editingState, editingReportActionID});
     useEffect(() => {
         if (editingState !== null || !didResetComposerHeight) {
             return;

From ac4ef84f660fcfb209ce4aaa3ef7c8e6da245b8c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:53:12 +0100
Subject: [PATCH 125/233] refactor: extract and memoize styles

---
 .../ReportActionCompose.tsx                   | 42 ++++++++++++++-----
 1 file changed, 31 insertions(+), 11 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 27e0346cfbf1..d62da0fc42a2 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -567,6 +567,34 @@ function ReportActionCompose({
 
     const fsClass = FS.getChatFSClass(report);
 
+    const containerStyles = useMemo(
+        () => [
+            shouldUseFocusedColor ? styles.chatItemComposeBoxFocusedColor : styles.chatItemComposeBoxColor,
+            styles.flexRow,
+            styles.chatItemComposeBox,
+            isComposerFullSize && styles.chatItemFullComposeBox,
+            isExceedingMaxLength && styles.borderColorDanger,
+        ],
+        [
+            shouldUseFocusedColor,
+            styles.chatItemComposeBoxFocusedColor,
+            styles.chatItemComposeBoxColor,
+            styles.flexRow,
+            styles.chatItemComposeBox,
+            styles.chatItemFullComposeBox,
+            styles.borderColorDanger,
+            isComposerFullSize,
+            isExceedingMaxLength,
+        ],
+    );
+
+    const editingButtonsContainerStyles = useMemo(
+        () => [styles.dFlex, styles.alignItemsCenter, styles.flexWrap, styles.justifyContentCenter, {paddingVertical: styles.composerSizeButton.marginHorizontal}],
+        [styles.alignItemsCenter, styles.composerSizeButton.marginHorizontal, styles.dFlex, styles.flexWrap, styles.justifyContentCenter],
+    );
+
+    const expandCollapseComposerButtonStyles = useMemo(() => [styles.flexGrow1, styles.flexShrink0], [styles.flexGrow1, styles.flexShrink0]);
+
     return (
         <View style={[shouldShowReportRecipientLocalTime && !isOffline && styles.chatItemComposeWithFirstRow, isComposerFullSize && styles.chatItemFullComposeRow]}>
             <OfflineWithFeedback pendingAction={pendingAction}>
@@ -581,19 +609,11 @@ function ReportActionCompose({
                 >
                     <View
                         ref={containerRef}
-                        style={[
-                            shouldUseFocusedColor ? styles.chatItemComposeBoxFocusedColor : styles.chatItemComposeBoxColor,
-                            styles.flexRow,
-                            styles.chatItemComposeBox,
-                            isComposerFullSize && styles.chatItemFullComposeBox,
-                            isExceedingMaxLength && styles.borderColorDanger,
-                        ]}
+                        style={containerStyles}
                     >
                         {PDFValidationComponent}
                         {isEditingInComposer ? (
-                            <View
-                                style={[styles.dFlex, styles.alignItemsCenter, styles.flexWrap, styles.justifyContentCenter, {paddingVertical: styles.composerSizeButton.marginHorizontal}]}
-                            >
+                            <View style={editingButtonsContainerStyles}>
                                 <ExpandCollapseComposerButton
                                     isFullComposerAvailable={isFullComposerAvailable}
                                     isComposerFullSize={isComposerFullSize}
@@ -601,7 +621,7 @@ function ReportActionCompose({
                                     disabled={isBlockedFromConcierge}
                                     raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLayoutTriggered}
                                     setIsComposerFullSize={setIsComposerFullSize}
-                                    style={[styles.flexGrow1, styles.flexShrink0]}
+                                    style={expandCollapseComposerButtonStyles}
                                 />
                                 <MessageEditCancelButton onCancel={deleteDraft} />
                             </View>

From 9bbe7b3bcc7790114fbce65a5187e2b12ed1960c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:55:51 +0100
Subject: [PATCH 126/233] refactor: simplify `ExpandCollapseComposerButton`
 component and remove duplicate code

---
 .../ExpandCollapseComposerButton.tsx          | 79 +++++++------------
 1 file changed, 29 insertions(+), 50 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx b/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx
index c3a3cdb4e5b9..4485f2c9d136 100644
--- a/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ExpandCollapseComposerButton.tsx
@@ -37,60 +37,39 @@ function ExpandCollapseComposerButton({
         return null;
     }
 
+    const shouldCollapse = isComposerFullSize;
+    const tooltipText = shouldCollapse ? translate('reportActionCompose.collapse') : translate('reportActionCompose.expand');
+    const nextComposerFullSizeValue = !shouldCollapse;
+    const iconSrc = shouldCollapse ? icons.Collapse : icons.Expand;
+    const sentryLabel = shouldCollapse ? CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_COLLAPSE_BUTTON : CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_EXPAND_BUTTON;
+
     return (
         // eslint-disable-next-line react/jsx-props-no-spreading
         <View {...restProps}>
-            {isComposerFullSize ? (
-                <Tooltip
-                    text={translate('reportActionCompose.collapse')}
-                    key="composer-collapse"
-                >
-                    <PressableWithFeedback
-                        onPress={(e) => {
-                            e?.preventDefault();
-                            raiseIsScrollLikelyLayoutTriggered();
-                            setIsComposerFullSize(reportID, false);
-                        }}
-                        // Keep focus on the composer when Collapse button is clicked.
-                        onMouseDown={(e) => e.preventDefault()}
-                        style={styles.composerSizeButton}
-                        disabled={disabled}
-                        role={CONST.ROLE.BUTTON}
-                        accessibilityLabel={translate('reportActionCompose.collapse')}
-                        sentryLabel={CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_COLLAPSE_BUTTON}
-                    >
-                        <Icon
-                            fill={theme.icon}
-                            src={icons.Collapse}
-                        />
-                    </PressableWithFeedback>
-                </Tooltip>
-            ) : (
-                <Tooltip
-                    text={translate('reportActionCompose.expand')}
-                    key="composer-expand"
+            <Tooltip
+                text={tooltipText}
+                key={shouldCollapse ? 'composer-collapse' : 'composer-expand'}
+            >
+                <PressableWithFeedback
+                    onPress={(e) => {
+                        e?.preventDefault();
+                        raiseIsScrollLikelyLayoutTriggered();
+                        setIsComposerFullSize(reportID, nextComposerFullSizeValue);
+                    }}
+                    // Keep focus on the composer when Collapse/Expand button is clicked.
+                    onMouseDown={(e) => e.preventDefault()}
+                    style={styles.composerSizeButton}
+                    disabled={disabled}
+                    role={CONST.ROLE.BUTTON}
+                    accessibilityLabel={tooltipText}
+                    sentryLabel={sentryLabel}
                 >
-                    <PressableWithFeedback
-                        onPress={(e) => {
-                            e?.preventDefault();
-                            raiseIsScrollLikelyLayoutTriggered();
-                            setIsComposerFullSize(reportID, true);
-                        }}
-                        // Keep focus on the composer when Expand button is clicked.
-                        onMouseDown={(e) => e.preventDefault()}
-                        style={styles.composerSizeButton}
-                        disabled={disabled}
-                        role={CONST.ROLE.BUTTON}
-                        accessibilityLabel={translate('reportActionCompose.expand')}
-                        sentryLabel={CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_EXPAND_BUTTON}
-                    >
-                        <Icon
-                            fill={theme.icon}
-                            src={icons.Expand}
-                        />
-                    </PressableWithFeedback>
-                </Tooltip>
-            )}
+                    <Icon
+                        fill={theme.icon}
+                        src={iconSrc}
+                    />
+                </PressableWithFeedback>
+            </Tooltip>
         </View>
     );
 }

From 9db5daa2d4c32762c993073031d9d1f5f52b5b54 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 14:57:12 +0100
Subject: [PATCH 127/233] chore: add back JSDoc comment

---
 src/libs/actions/Report/index.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index 76c774c86416..6e59c327b9da 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2831,6 +2831,7 @@ function editReportComment(
     );
 }
 
+/** Clears drafts for all comment report action in a report. */
 function clearReportActionDrafts(reportID: string | undefined) {
     Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, null);
 }

From fcb41fb2584ab39a02b94d6731ce16cfdae1839f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 15:08:45 +0100
Subject: [PATCH 128/233] test: add integration test for task title length
 validation in ReportActionCompose

---
 tests/ui/ReportActionComposeTest.tsx | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/tests/ui/ReportActionComposeTest.tsx b/tests/ui/ReportActionComposeTest.tsx
index 27c6b26db3bf..9159de2d2f14 100644
--- a/tests/ui/ReportActionComposeTest.tsx
+++ b/tests/ui/ReportActionComposeTest.tsx
@@ -247,5 +247,23 @@ describe('ReportActionCompose Integration Tests', () => {
             // And the error should be displayed
             expect(screen.getByText('composer.commentExceededMaxLength')).toBeOnTheScreen();
         });
+
+        it('should not send when task title length exceeds the limit', async () => {
+            renderReportActionCompose();
+            const composer = screen.getByTestId('composer');
+
+            // Given a task title that exceeds the title character limit
+            const taskTitle = 'x'.repeat(CONST.TITLE_CHARACTER_LIMIT + 1);
+            fireEvent.changeText(composer, `[] ${taskTitle}`);
+
+            // When the message is submitted
+            act(onSubmitAction);
+
+            // Then the message should NOT be sent
+            expect(mockForceClearInput).toHaveBeenCalledTimes(0);
+
+            // And the task-title-specific error should be displayed
+            expect(screen.getByText('composer.taskTitleExceededMaxLength')).toBeOnTheScreen();
+        });
     });
 });

From 59f8a181ef58d2fcf06d8d562e173536c6dfbc2d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 15:12:58 +0100
Subject: [PATCH 129/233] test: add more ui tests

---
 tests/ui/ReportActionItemMessageEditTest.tsx | 74 ++++++++++++++++++++
 1 file changed, 74 insertions(+)

diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index df672efbe6ab..4290637c2d30 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -6,14 +6,18 @@ import ComposeProviders from '@components/ComposeProviders';
 import {LocaleContextProvider} from '@components/LocaleContextProvider';
 import OnyxListItemProvider from '@components/OnyxListItemProvider';
 import {editReportComment} from '@libs/actions/Report';
+import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
 import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
 import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
+import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
+import type {Message} from '@src/types/onyx/ReportAction';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
 import * as TestHelper from '../utils/TestHelper';
 
 const mockEditReportComment = jest.mocked(editReportComment);
+const mockShowDeleteModal = jest.mocked(ReportActionContextMenu.showDeleteModal);
 
 jest.mock('@libs/actions/Report', () => {
     // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -25,6 +29,16 @@ jest.mock('@libs/actions/Report', () => {
     };
 });
 
+jest.mock('@pages/inbox/report/ContextMenu/ReportActionContextMenu', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@pages/inbox/report/ContextMenu/ReportActionContextMenu');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        showDeleteModal: jest.fn(),
+    };
+});
+
 jest.mock('@hooks/useLocalize', () =>
     jest.fn(() => ({
         translate: jest.fn((key: string) => key),
@@ -82,6 +96,7 @@ describe('ReportActionCompose Integration Tests', () => {
         await act(async () => {
             await Onyx.clear();
         });
+        draftMessageVideoAttributeCache.clear();
         jest.clearAllMocks();
     });
 
@@ -120,5 +135,64 @@ describe('ReportActionCompose Integration Tests', () => {
             // And the error should be displayed
             expect(screen.getByText('composer.commentExceededMaxLength')).toBeOnTheScreen();
         });
+
+        it('should open delete modal when saving an empty message', async () => {
+            renderReportActionItemMessageEdit();
+            const composer = screen.getByTestId('composer');
+            const saveChangesButton = screen.getByLabelText('common.saveChanges');
+
+            // Given a message that becomes empty after trimming
+            fireEvent.changeText(composer, '   ');
+
+            // When the message is saved
+            fireEvent.press(saveChangesButton);
+
+            // Then the message should NOT be edited
+            expect(mockEditReportComment).toHaveBeenCalledTimes(0);
+
+            // And the delete confirmation flow should be opened
+            expect(mockShowDeleteModal).toHaveBeenCalledTimes(1);
+        });
+
+        it('should cache and forward video attributes when saving an edited message', async () => {
+            const videoSource = 'https://example.com/video.mp4';
+            const videoHtml = `<video src="${videoSource}" data-expensify-source="${videoSource}" data-name="video.mp4" data-expensify-height="100" data-expensify-width="200">video.mp4</video>`;
+
+            const messages = defaultProps.action.message as Message[];
+
+            renderReportActionItemMessageEdit({
+                action: {
+                    ...defaultProps.action,
+                    message: [
+                        {
+                            ...messages.at(0),
+                            type: 'COMMENT',
+                            html: videoHtml,
+                            text: '[Attachment]',
+                        },
+                    ],
+                },
+            });
+
+            const composer = screen.getByTestId('composer');
+            const saveChangesButton = screen.getByLabelText('common.saveChanges');
+
+            // Given a valid edited message
+            fireEvent.changeText(composer, 'Edited message');
+
+            // When the message is saved
+            fireEvent.press(saveChangesButton);
+
+            expect(mockEditReportComment).toHaveBeenCalledTimes(1);
+
+            const editReportCommentArgs = mockEditReportComment.mock.calls.at(0);
+            const videoAttributeCache = editReportCommentArgs?.[7];
+
+            expect(videoAttributeCache).toEqual(expect.any(Object));
+            expect(videoAttributeCache?.[videoSource]).toEqual(expect.any(String));
+            expect(videoAttributeCache?.[videoSource]).toContain('data-name');
+            expect(videoAttributeCache?.[videoSource]).toContain('data-expensify-height');
+            expect(videoAttributeCache?.[videoSource]).toContain('data-expensify-width');
+        });
     });
 });

From 49845d22b34057e681f948834efc33b2a5de35ee Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 15:33:38 +0100
Subject: [PATCH 130/233] test: add unit tests for new hooks

---
 ...ebouncedCommentMaxLengthValidation.test.ts |  50 +++++++
 ...useDraftMessageVideoAttributeCache.test.ts |  77 ++++++++++
 tests/unit/hooks/useEditMessage.test.tsx      | 140 ++++++++++++++++++
 3 files changed, 267 insertions(+)
 create mode 100644 tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts
 create mode 100644 tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts
 create mode 100644 tests/unit/hooks/useEditMessage.test.tsx

diff --git a/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts b/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts
new file mode 100644
index 000000000000..60919154b86c
--- /dev/null
+++ b/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts
@@ -0,0 +1,50 @@
+import {act, renderHook, waitFor} from '@testing-library/react-native';
+import * as ReportUtils from '@libs/ReportUtils';
+import useDebouncedCommentMaxLengthValidation from '@pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation';
+import CONST from '@src/CONST';
+
+jest.mock('@libs/ReportUtils', () => {
+    return {
+        getCommentLength: jest.fn(),
+    };
+});
+
+const mockGetCommentLength = jest.mocked(ReportUtils.getCommentLength);
+
+describe('useDebouncedCommentMaxLengthValidation', () => {
+    afterEach(() => {
+        jest.clearAllMocks();
+    });
+
+    it('should apply task-title validation when not editing and input matches task pattern', () => {
+        const {result} = renderHook(() => useDebouncedCommentMaxLengthValidation({reportID: '1', isEditing: false}));
+
+        const title = 'x'.repeat(CONST.TITLE_CHARACTER_LIMIT + 1);
+
+        act(() => {
+            result.current.debouncedCommentMaxLengthValidation(`[] ${title}`);
+        });
+
+        expect(result.current.isTaskTitle).toBe(true);
+        expect(result.current.exceededMaxLength).toBe(CONST.TITLE_CHARACTER_LIMIT);
+        expect(result.current.isExceedingMaxLength).toBe(true);
+        expect(mockGetCommentLength).not.toHaveBeenCalled();
+    });
+
+    it('should apply comment-length validation when editing', async () => {
+        mockGetCommentLength.mockReturnValue(CONST.MAX_COMMENT_LENGTH + 1);
+
+        const {result} = renderHook(() => useDebouncedCommentMaxLengthValidation({reportID: '1', isEditing: true}));
+
+        act(() => {
+            result.current.debouncedCommentMaxLengthValidation('x'.repeat(CONST.MAX_COMMENT_LENGTH + 1));
+            result.current.debouncedCommentMaxLengthValidation.flush();
+        });
+
+        await waitFor(() => {
+            expect(result.current.isTaskTitle).toBe(false);
+            expect(result.current.exceededMaxLength).toBe(CONST.MAX_COMMENT_LENGTH);
+            expect(result.current.isExceedingMaxLength).toBe(true);
+        });
+    });
+});
diff --git a/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts b/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts
new file mode 100644
index 000000000000..f3c0285dbdfd
--- /dev/null
+++ b/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts
@@ -0,0 +1,77 @@
+import {renderHook} from '@testing-library/react-native';
+import Parser from '@libs/Parser';
+import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import useDraftMessageVideoAttributeCache, {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
+
+jest.mock('@libs/ReportActionsUtils', () => ({
+    getReportActionHtml: jest.fn(),
+    isDeletedAction: jest.fn(() => false),
+}));
+
+const mockGetReportActionHtml = jest.mocked(ReportActionsUtils.getReportActionHtml);
+
+describe('useDraftMessageVideoAttributeCache', () => {
+    let htmlToMarkdownSpy: jest.SpiedFunction<typeof Parser.htmlToMarkdown>;
+
+    beforeEach(() => {
+        htmlToMarkdownSpy = jest.spyOn(Parser, 'htmlToMarkdown');
+    });
+
+    afterEach(() => {
+        draftMessageVideoAttributeCache.clear();
+        jest.clearAllMocks();
+    });
+
+    it('should cache video attributes from the original message html when editing', () => {
+        const reportAction = {reportActionID: '1'} as never;
+        mockGetReportActionHtml.mockReturnValue('<video src="https://example.com/video.mp4" data-name="v.mp4">v.mp4</video>');
+
+        htmlToMarkdownSpy.mockImplementation((_html, extras) => {
+            extras?.cacheVideoAttributes?.('https://example.com/video.mp4', ' data-name="v.mp4"');
+            return 'original markdown';
+        });
+
+        const updateDraftMessage = jest.fn();
+        const isEditInProgressRef = {current: false};
+
+        renderHook(() =>
+            useDraftMessageVideoAttributeCache({
+                draftMessage: 'changed',
+                isEditing: true,
+                editingReportAction: reportAction,
+                updateDraftMessage,
+                isEditInProgressRef,
+            }),
+        );
+
+        expect(draftMessageVideoAttributeCache.get('https://example.com/video.mp4')).toBe(' data-name="v.mp4"');
+    });
+
+    it('should not call updateDraftMessage when edit is in progress', () => {
+        const reportAction = {reportActionID: '1'} as never;
+        mockGetReportActionHtml.mockReturnValue('<video src="https://example.com/video.mp4" data-name="v.mp4">v.mp4</video>');
+        htmlToMarkdownSpy.mockImplementation((_html, extras) => {
+            extras?.cacheVideoAttributes?.('https://example.com/video.mp4', ' data-name="v.mp4"');
+            return 'original markdown';
+        });
+
+        const updateDraftMessage = jest.fn();
+        const isEditInProgressRef = {current: true};
+
+        const {rerender} = renderHook(
+            ({draftMessage}) =>
+                useDraftMessageVideoAttributeCache({
+                    draftMessage,
+                    isEditing: true,
+                    editingReportAction: reportAction,
+                    updateDraftMessage,
+                    isEditInProgressRef,
+                }),
+            {initialProps: {draftMessage: 'changed'}},
+        );
+
+        rerender({draftMessage: 'changed again'});
+
+        expect(updateDraftMessage).toHaveBeenCalledTimes(0);
+    });
+});
diff --git a/tests/unit/hooks/useEditMessage.test.tsx b/tests/unit/hooks/useEditMessage.test.tsx
new file mode 100644
index 000000000000..aa043ec6c2cf
--- /dev/null
+++ b/tests/unit/hooks/useEditMessage.test.tsx
@@ -0,0 +1,140 @@
+import {act, renderHook} from '@testing-library/react-native';
+import Onyx from 'react-native-onyx';
+import * as Report from '@libs/actions/Report';
+import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
+import useEditMessage from '@pages/inbox/report/ReportActionCompose/useEditMessage';
+import ONYXKEYS from '@src/ONYXKEYS';
+import * as LHNTestUtils from '../../utils/LHNTestUtils';
+
+jest.mock('@libs/actions/Report', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@libs/actions/Report');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        editReportComment: jest.fn(),
+        clearReportActionDrafts: jest.fn(),
+    };
+});
+
+jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => ({
+    useReportActionActiveEditActions: () => ({
+        setEditingState: jest.fn(),
+    }),
+}));
+
+jest.mock('@pages/inbox/report/ContextMenu/ReportActionContextMenu', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@pages/inbox/report/ContextMenu/ReportActionContextMenu');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        showDeleteModal: jest.fn(),
+        isActiveReportAction: jest.fn(() => false),
+    };
+});
+
+jest.mock('@hooks/useAncestors', () => ({
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    __esModule: true,
+    default: () => [],
+}));
+
+jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    __esModule: true,
+    default: () => ({email: 'user@test.com'}),
+}));
+
+jest.mock('@hooks/useReportIsArchived', () => ({
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    __esModule: true,
+    default: () => false,
+}));
+
+jest.mock('@hooks/useReportScrollManager', () => ({
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    __esModule: true,
+    default: () => ({scrollToIndex: jest.fn()}),
+}));
+
+jest.mock('@libs/ReportUtils', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@libs/ReportUtils');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        getOriginalReportID: () => undefined,
+    };
+});
+
+const mockEditReportComment = jest.mocked(Report.editReportComment);
+const mockShowDeleteModal = jest.mocked(ReportActionContextMenu.showDeleteModal);
+
+type HookProps = Parameters<typeof useEditMessage>[0];
+
+type DebouncedValidator = HookProps['debouncedCommentMaxLengthValidation'];
+
+function makeDebouncedValidator({flushResult}: {flushResult: boolean}): DebouncedValidator {
+    return {
+        flush: jest.fn(() => flushResult),
+        cancel: jest.fn(),
+    } as unknown as DebouncedValidator;
+}
+
+describe('useEditMessage', () => {
+    beforeAll(() => {
+        Onyx.init({keys: ONYXKEYS});
+    });
+
+    afterEach(async () => {
+        await act(async () => {
+            await Onyx.clear();
+        });
+        jest.clearAllMocks();
+    });
+
+    function renderUseEditMessage(overrides?: Partial<HookProps>) {
+        const report = LHNTestUtils.getFakeReport();
+        const reportAction = LHNTestUtils.getFakeReportAction();
+
+        const props: HookProps = {
+            reportID: report.reportID,
+            originalReportID: report.reportID,
+            reportAction,
+            isFocused: true,
+            debouncedCommentMaxLengthValidation: makeDebouncedValidator({flushResult: true}),
+            composerRef: {current: {blur: jest.fn()} as never},
+            ...overrides,
+        };
+        const hook = renderHook(() => useEditMessage(props));
+        return {hook, props};
+    }
+
+    it('should not publish when validation flush fails', async () => {
+        const {hook} = renderUseEditMessage({
+            debouncedCommentMaxLengthValidation: makeDebouncedValidator({flushResult: false}),
+        });
+
+        act(() => {
+            hook.result.current.publishDraft('Hello');
+        });
+
+        expect(mockEditReportComment).toHaveBeenCalledTimes(0);
+        expect(mockShowDeleteModal).toHaveBeenCalledTimes(0);
+    });
+
+    it('should open delete modal when publishing an empty (trimmed) message', async () => {
+        const {hook, props} = renderUseEditMessage();
+
+        act(() => {
+            hook.result.current.publishDraft('   ');
+        });
+
+        expect(mockEditReportComment).toHaveBeenCalledTimes(0);
+        expect(mockShowDeleteModal).toHaveBeenCalledTimes(1);
+
+        const args = mockShowDeleteModal.mock.calls.at(0);
+        expect(args?.[1]?.reportActionID).toBe(props.reportAction?.reportActionID);
+    });
+});

From e71928550f2886bc869ff86065013647c7e8e37b Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 15:34:28 +0100
Subject: [PATCH 131/233] refactor: change file type

---
 .../hooks/{useEditMessage.test.tsx => useEditMessage.test.ts}     | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename tests/unit/hooks/{useEditMessage.test.tsx => useEditMessage.test.ts} (100%)

diff --git a/tests/unit/hooks/useEditMessage.test.tsx b/tests/unit/hooks/useEditMessage.test.ts
similarity index 100%
rename from tests/unit/hooks/useEditMessage.test.tsx
rename to tests/unit/hooks/useEditMessage.test.ts

From d9849250e2953e7eddc7c1148e05ca5e2ebb2470 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 18 Mar 2026 16:08:32 +0100
Subject: [PATCH 132/233] refactor: fix imports

---
 .../hooks/useDebouncedCommentMaxLengthValidation.test.ts  | 4 ++--
 .../unit/hooks/useDraftMessageVideoAttributeCache.test.ts | 4 ++--
 tests/unit/hooks/useEditMessage.test.ts                   | 8 ++++----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts b/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts
index 60919154b86c..db004d43d90d 100644
--- a/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts
+++ b/tests/unit/hooks/useDebouncedCommentMaxLengthValidation.test.ts
@@ -1,5 +1,5 @@
 import {act, renderHook, waitFor} from '@testing-library/react-native';
-import * as ReportUtils from '@libs/ReportUtils';
+import {getCommentLength} from '@libs/ReportUtils';
 import useDebouncedCommentMaxLengthValidation from '@pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation';
 import CONST from '@src/CONST';
 
@@ -9,7 +9,7 @@ jest.mock('@libs/ReportUtils', () => {
     };
 });
 
-const mockGetCommentLength = jest.mocked(ReportUtils.getCommentLength);
+const mockGetCommentLength = jest.mocked(getCommentLength);
 
 describe('useDebouncedCommentMaxLengthValidation', () => {
     afterEach(() => {
diff --git a/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts b/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts
index f3c0285dbdfd..83a04de88018 100644
--- a/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts
+++ b/tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts
@@ -1,6 +1,6 @@
 import {renderHook} from '@testing-library/react-native';
 import Parser from '@libs/Parser';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import {getReportActionHtml} from '@libs/ReportActionsUtils';
 import useDraftMessageVideoAttributeCache, {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 
 jest.mock('@libs/ReportActionsUtils', () => ({
@@ -8,7 +8,7 @@ jest.mock('@libs/ReportActionsUtils', () => ({
     isDeletedAction: jest.fn(() => false),
 }));
 
-const mockGetReportActionHtml = jest.mocked(ReportActionsUtils.getReportActionHtml);
+const mockGetReportActionHtml = jest.mocked(getReportActionHtml);
 
 describe('useDraftMessageVideoAttributeCache', () => {
     let htmlToMarkdownSpy: jest.SpiedFunction<typeof Parser.htmlToMarkdown>;
diff --git a/tests/unit/hooks/useEditMessage.test.ts b/tests/unit/hooks/useEditMessage.test.ts
index aa043ec6c2cf..607f89f05366 100644
--- a/tests/unit/hooks/useEditMessage.test.ts
+++ b/tests/unit/hooks/useEditMessage.test.ts
@@ -1,7 +1,7 @@
 import {act, renderHook} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
-import * as Report from '@libs/actions/Report';
-import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
+import {editReportComment} from '@libs/actions/Report';
+import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
 import useEditMessage from '@pages/inbox/report/ReportActionCompose/useEditMessage';
 import ONYXKEYS from '@src/ONYXKEYS';
 import * as LHNTestUtils from '../../utils/LHNTestUtils';
@@ -68,8 +68,8 @@ jest.mock('@libs/ReportUtils', () => {
     };
 });
 
-const mockEditReportComment = jest.mocked(Report.editReportComment);
-const mockShowDeleteModal = jest.mocked(ReportActionContextMenu.showDeleteModal);
+const mockEditReportComment = jest.mocked(editReportComment);
+const mockShowDeleteModal = jest.mocked(showDeleteModal);
 
 type HookProps = Parameters<typeof useEditMessage>[0];
 

From 9ddf9562f5e435e4ad31efeeed37fdf4a68a131c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 24 Mar 2026 12:48:23 +0000
Subject: [PATCH 133/233] fix: always delete all edit message drafts

---
 src/libs/actions/Report/index.ts                             | 4 ++--
 src/pages/inbox/ReportScreen.tsx                             | 2 +-
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx    | 2 +-
 src/pages/inbox/report/PureReportActionItem.tsx              | 2 +-
 src/pages/inbox/report/ReportActionCompose/useEditMessage.ts | 2 +-
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index 075d2efd70c9..782adffad85c 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2938,8 +2938,8 @@ function editReportComment(
 }
 
 /** Clears drafts for all comment report action in a report. */
-function clearReportActionDrafts(reportID: string | undefined) {
-    Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`, null);
+function clearReportActionDrafts() {
+    Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {});
 }
 
 /** Saves the draft for a comment report action. This will put the comment into "edit mode" */
diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index 5b7e1ada6912..aa142cf0f78b 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -193,7 +193,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
             }
 
             return () => {
-                clearReportActionDrafts(reportIDFromRoute);
+                clearReportActionDrafts();
             };
         }, [reportIDFromRoute]),
     );
diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 9e251ce5e9bb..19a0e4413f93 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -529,7 +529,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                 return;
             }
             const editAction = () => {
-                clearReportActionDrafts(reportID);
+                clearReportActionDrafts();
 
                 if (draftMessage) {
                     return;
diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 29eb642499b9..4be24e62b4ce 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -708,7 +708,7 @@ function PureReportActionItem({
         if (draftMessage === undefined || !isDeletedAction(action)) {
             return;
         }
-        clearReportActionDrafts(reportID);
+        clearReportActionDrafts();
     }, [draftMessage, action, reportID]);
 
     // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 1a87463e27e7..d0bc1bddce10 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -70,7 +70,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
 
         setEditingState('cancelled');
 
-        clearReportActionDrafts(reportID);
+        clearReportActionDrafts();
 
         if (isActive()) {
             ReportActionComposeFocusManager.clear(true);

From 8324e3386298dd460e65f50aac1d2db23a3d3618 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 24 Mar 2026 19:25:02 +0000
Subject: [PATCH 134/233] fix: `Onyx.merge` and `Onyx.setCollection` not queued
 in the correct order

---
 src/libs/actions/Report/index.ts | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index 0d7390697347..4464b4704567 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2949,7 +2949,12 @@ function saveReportActionDraft(reportID: string | undefined, reportAction: Repor
     }
 
     const originalReportID = getOriginalReportID(reportID, reportAction, undefined);
-    Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`, {[reportAction.reportActionID]: {message: draftMessage}});
+
+    Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
+        [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`]: {
+            [reportAction.reportActionID]: {message: draftMessage},
+        },
+    });
 }
 
 function updateNotificationPreference(

From 96dd8514a8a3e9a7999cd7a5b8a18c29752ce67e Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 24 Mar 2026 19:41:37 +0000
Subject: [PATCH 135/233] refactor: other usages of Onyx draft message key

---
 .../DuplicateTransactionItem.tsx              | 15 ++---
 .../report/ReportActionItemParentAction.tsx   | 55 ++-----------------
 2 files changed, 12 insertions(+), 58 deletions(-)

diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
index 2c11ebca8d37..9a563cc59c4e 100644
--- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
+++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
@@ -8,7 +8,7 @@ import useOnyx from '@hooks/useOnyx';
 import useThemeStyles from '@hooks/useThemeStyles';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
 import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import {getOriginalReportID} from '@libs/ReportUtils';
+import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import ReportActionItem from '@pages/inbox/report/ReportActionItem';
 import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext';
 import CONST from '@src/CONST';
@@ -41,10 +41,6 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
         return IOUTransactionID === transaction?.transactionID;
     });
 
-    const originalReportID = getOriginalReportID(report?.reportID, action, reportActions);
-
-    const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`);
-
     const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action?.reportActionID}`);
 
     const [linkedTransactionRouteError] = useOnyx(
@@ -57,13 +53,14 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
     const stateValue = useMemo(() => ({shouldOpenReportInRHP: true}), []);
     const actionsValue = useMemo(() => ({onPreviewPressed}), [onPreviewPressed]);
 
+    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
+
+    const draftMessage = editingReportAction?.reportActionID === action?.reportActionID ? (editingMessage ?? undefined) : undefined;
+
     if (!action || !report) {
         return null;
     }
 
-    const reportDraftMessage = draftMessage?.[action.reportActionID];
-    const matchingDraftMessage = reportDraftMessage?.message;
-
     return (
         <View style={styles.pb2}>
             <ReportActionItemStateContext.Provider value={stateValue}>
@@ -81,7 +78,7 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
                         userWalletTierName={userWalletTierName}
                         isUserValidated={isUserValidated}
                         personalDetails={personalDetails}
-                        draftMessage={matchingDraftMessage}
+                        draftMessage={draftMessage}
                         emojiReactions={emojiReactions}
                         linkedTransactionRouteError={linkedTransactionRouteError}
                         userBillingFundID={userBillingFundID}
diff --git a/src/pages/inbox/report/ReportActionItemParentAction.tsx b/src/pages/inbox/report/ReportActionItemParentAction.tsx
index a9e4a9693ca4..4d159c56ec40 100644
--- a/src/pages/inbox/report/ReportActionItemParentAction.tsx
+++ b/src/pages/inbox/report/ReportActionItemParentAction.tsx
@@ -1,4 +1,3 @@
-import {getReportActionsForReportIDs} from '@selectors/ReportAction';
 import React, {useCallback} from 'react';
 import {View} from 'react-native';
 import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
@@ -14,16 +13,16 @@ import {getOriginalMessage, isMoneyRequestAction, isTripPreview} from '@libs/Rep
 import {
     canCurrentUserOpenReport,
     canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
-    getOriginalReportID,
     isArchivedReport,
     navigateToLinkedReportAction,
     shouldExcludeAncestorReportAction,
 } from '@libs/ReportUtils';
 import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetailsList, Report, ReportAction, ReportActionReactions, ReportActions, ReportActionsDrafts, ReportNameValuePairs, Transaction} from '@src/types/onyx';
+import type {PersonalDetailsList, Report, ReportAction, ReportActionReactions, ReportNameValuePairs, Transaction} from '@src/types/onyx';
 import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
 import RepliesDivider from './RepliesDivider';
+import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import ReportActionItem from './ReportActionItem';
 import ThreadDivider from './ThreadDivider';
 
@@ -132,43 +131,7 @@ function ReportActionItemParentAction({
         [ancestors],
     );
 
-    const ancestorReportActionsSelector = useCallback(
-        (allReportActions: OnyxCollection<ReportActions>) => {
-            const reportIDs = ancestors.map((ancestor) => ancestor.report.reportID);
-            return getReportActionsForReportIDs(allReportActions, reportIDs);
-        },
-        [ancestors],
-    );
-
-    const [ancestorsReportActions] = useOnyx(
-        ONYXKEYS.COLLECTION.REPORT_ACTIONS,
-        {
-            selector: ancestorReportActionsSelector,
-        },
-        [ancestors],
-    );
-
-    const ancestorDraftSelector = useCallback(
-        (allDrafts: OnyxCollection<ReportActionsDrafts>) => {
-            if (!allDrafts) {
-                return {};
-            }
-            const result: OnyxCollection<ReportActionsDrafts> = {};
-            for (const ancestor of ancestors) {
-                const origID = getOriginalReportID(
-                    ancestor.report.reportID,
-                    ancestor.reportAction,
-                    ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`],
-                );
-                const key = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`;
-                result[key] = allDrafts[key];
-            }
-            return result;
-        },
-        [ancestors, ancestorsReportActions],
-    );
-
-    const [ancestorDraftMessages] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {selector: ancestorDraftSelector}, [ancestors, ancestorsReportActions]);
+    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
 
     const ancestorReactionSelector = useCallback(
         (allReactions: OnyxCollection<ReportActionReactions>) => {
@@ -207,14 +170,8 @@ function ReportActionItemParentAction({
                     const shouldDisplayThreadDivider = !isTripPreview(ancestorReportAction);
                     const isAncestorReportArchived = isArchivedReport(ancestorsReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${ancestorReport.reportID}`]);
 
-                    const originalReportID = getOriginalReportID(
-                        ancestorReport.reportID,
-                        ancestorReportAction,
-                        ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`],
-                    );
-                    const reportDraftMessages = originalReportID ? ancestorDraftMessages?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`] : undefined;
-                    const matchingDraftMessage = reportDraftMessages?.[ancestorReportAction.reportActionID];
-                    const matchingDraftMessageString = matchingDraftMessage?.message;
+                    const draftMessage = editingReportAction?.reportActionID === ancestorReportAction.reportActionID ? (editingMessage ?? undefined) : undefined;
+
                     const actionEmojiReactions = ancestorReactions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${ancestorReportAction.reportActionID}`];
 
                     return (
@@ -252,7 +209,7 @@ function ReportActionItemParentAction({
                                 userWalletTierName={userWalletTierName}
                                 isUserValidated={isUserValidated}
                                 personalDetails={personalDetails}
-                                draftMessage={matchingDraftMessageString}
+                                draftMessage={draftMessage}
                                 emojiReactions={actionEmojiReactions}
                                 linkedTransactionRouteError={linkedTransactionRouteError}
                                 userBillingFundID={userBillingFundID}

From d01abbd891ed6bb10578f0735adc9f6463de4a2f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 24 Mar 2026 19:56:26 +0000
Subject: [PATCH 136/233] refactor: add back selectors for report actions and
 drafts

---
 .../report/ReportActionEditMessageContext.tsx | 62 +++++++++++++++++--
 1 file changed, 57 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index b7bfe0e5c2cd..a80b5ab10b71 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,9 +1,11 @@
 import {useIsFocused} from '@react-navigation/native';
-import React, {createContext, useContext, useState} from 'react';
+import React, {createContext, useCallback, useContext, useState} from 'react';
 import type {Dispatch, SetStateAction} from 'react';
+import type {OnyxCollection} from 'react-native-onyx';
 import type {TextSelection} from '@components/Composer/types';
 import useAncestors from '@hooks/useAncestors';
 import useOnyx from '@hooks/useOnyx';
+import {getOriginalReportID, shouldExcludeAncestorReportAction} from '@libs/ReportUtils';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 
@@ -58,8 +60,51 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
     });
-    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
-    const ancestors = useAncestors(report);
+
+    const ancestors = useAncestors(report, shouldExcludeAncestorReportAction);
+
+    const ancestorReportActionsSelector = useCallback(
+        (allReportActions: OnyxCollection<OnyxTypes.ReportActions>) => {
+            if (!allReportActions) {
+                return {};
+            }
+            const result: OnyxCollection<OnyxTypes.ReportActions> = {};
+            for (const ancestor of ancestors) {
+                const key = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`;
+                result[key] = allReportActions[key];
+            }
+            return result;
+        },
+        [ancestors],
+    );
+
+    const [ancestorsReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: ancestorReportActionsSelector}, [ancestors]);
+
+    const ancestorDraftSelector = useCallback(
+        (allDrafts: OnyxCollection<OnyxTypes.ReportActionsDrafts>) => {
+            if (!allDrafts) {
+                return {};
+            }
+            const result: OnyxCollection<OnyxTypes.ReportActionsDrafts> = {};
+            if (reportID) {
+                const currentDraftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`;
+                result[currentDraftKey] = allDrafts[currentDraftKey];
+            }
+            for (const ancestor of ancestors) {
+                const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`];
+                const origID = getOriginalReportID(ancestor.report.reportID, ancestor.reportAction, reportActionsForAncestor);
+                if (!origID) {
+                    continue;
+                }
+                const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`;
+                result[draftKey] = allDrafts[draftKey];
+            }
+            return result;
+        },
+        [ancestors, ancestorsReportActions, reportID],
+    );
+
+    const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {selector: ancestorDraftSelector}, [ancestors, ancestorsReportActions, reportID]);
 
     const [editingState, setEditingState] = useState<EditingState | null>(null);
     const [prevEditingReportActionID, setPrevEditingReportActionID] = useState<string | null>(null);
@@ -77,7 +122,12 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
             .slice()
             .reverse()
             .find(({report: ancestorReport, reportAction}) => {
-                const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorReport.reportID}`];
+                const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`];
+                const origID = getOriginalReportID(ancestorReport.reportID, reportAction, reportActionsForAncestor);
+                if (!origID) {
+                    return false;
+                }
+                const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`];
                 const ancestorDraft = ancestorDrafts?.[reportAction.reportActionID];
 
                 return ancestorDraft?.message !== undefined;
@@ -100,7 +150,9 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
         if (ancestorWithDraft) {
             const {report: ancestorReport, reportAction: ancestorReportAction} = ancestorWithDraft;
-            const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorReport.reportID}`];
+            const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`];
+            const ancestorOrigReportID = getOriginalReportID(ancestorReport.reportID, ancestorReportAction, reportActionsForAncestor);
+            const ancestorDrafts = ancestorOrigReportID ? reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorOrigReportID}`] : undefined;
             const ancestorReportActionDraft = ancestorDrafts?.[ancestorReportAction.reportActionID];
 
             editingReportID = ancestorReport.reportID;

From 4eddd233b88d778c80b17fb4557f80b6aebe5837 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 24 Mar 2026 20:19:26 +0000
Subject: [PATCH 137/233] refactor: remove unnecessary
 `clearReportActionDrafts` call

---
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 19a0e4413f93..62d2f67944ee 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -529,12 +529,6 @@ const ContextMenuActions: ContextMenuAction[] = [
                 return;
             }
             const editAction = () => {
-                clearReportActionDrafts();
-
-                if (draftMessage) {
-                    return;
-                }
-
                 saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction)));
             };
 

From c0d39090dcbc9c05de2ce461c3622e036997eef0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 09:40:58 +0000
Subject: [PATCH 138/233] refactor: make `editingState` non-nullable

---
 .../ComposerWithSuggestions.tsx               | 11 +++++-----
 .../ReportActionCompose.tsx                   |  4 ++--
 .../report/ReportActionEditMessageContext.tsx | 21 +++++++++----------
 3 files changed, 17 insertions(+), 19 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 77669c5e7d32..c753b8793fdb 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -264,7 +264,8 @@ function ComposerWithSuggestions({
     const {editingState, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
     const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
 
-    const isEditingInComposer = shouldUseNarrowLayout && editingState !== null;
+    const isEditing = editingState !== 'off';
+    const isEditingInComposer = shouldUseNarrowLayout && isEditing;
     const [value, setValue] = useState(() => {
         const initialValue = isEditingInComposer ? (editingMessage ?? draftComment) : draftComment;
 
@@ -279,7 +280,7 @@ function ComposerWithSuggestions({
 
     useDraftMessageVideoAttributeCache({
         draftMessage: value,
-        isEditing: editingState !== null,
+        isEditing,
         editingReportAction,
         updateDraftMessage: setValue,
         isEditInProgressRef: isDraftPendingSaved,
@@ -334,7 +335,7 @@ function ComposerWithSuggestions({
         [currentEditMessageSelection, updateSelectionImperatively],
     );
 
-    const wasEditing = useRef(editingState !== null);
+    const wasEditing = useRef(isEditing);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
@@ -343,9 +344,7 @@ function ComposerWithSuggestions({
             return;
         }
 
-        const isEditing = editingState === 'editing';
-
-        if (!isEditing) {
+        if (editingState !== 'editing') {
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
                 applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current});
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index ea33e9099aec..eff19e6699cd 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -169,10 +169,10 @@ function ReportActionCompose({
     const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
-    const isEditingInComposer = shouldUseNarrowLayout && editingState !== null && !didResetComposerHeight;
+    const isEditingInComposer = shouldUseNarrowLayout && editingState !== 'off' && !didResetComposerHeight;
 
     useEffect(() => {
-        if (editingState !== null || !didResetComposerHeight) {
+        if (editingState !== 'off' || !didResetComposerHeight) {
             return;
         }
 
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index a80b5ab10b71..ac5526b40bbb 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -13,7 +13,7 @@ function NOOP() {
     return null;
 }
 
-type EditingState = 'editing' | 'submitted' | 'cancelled';
+type EditingState = 'off' | 'editing' | 'submitted' | 'cancelled';
 
 type ReportActionActiveEdit = {
     editingReportID: string | null;
@@ -24,11 +24,11 @@ type ReportActionActiveEdit = {
 
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
     currentEditMessageSelection: TextSelection | null;
-    editingState: EditingState | null;
+    editingState: EditingState;
 };
 
 type ReportActionEditMessageContextActions = {
-    setEditingState: Dispatch<SetStateAction<EditingState | null>>;
+    setEditingState: Dispatch<SetStateAction<EditingState>>;
     setEditingMessage: Dispatch<SetStateAction<string | null>>;
     setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
 };
@@ -39,7 +39,7 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
     editingReportAction: null,
     editingMessage: null,
     currentEditMessageSelection: null,
-    editingState: null,
+    editingState: 'off',
 });
 
 const ReportActionEditMessageActionsContext = createContext<ReportActionEditMessageContextActions>({
@@ -106,7 +106,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
     const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {selector: ancestorDraftSelector}, [ancestors, ancestorsReportActions, reportID]);
 
-    const [editingState, setEditingState] = useState<EditingState | null>(null);
+    const [editingState, setEditingState] = useState<EditingState>('off');
     const [prevEditingReportActionID, setPrevEditingReportActionID] = useState<string | null>(null);
     const [editingMessage, setEditingMessage] = useState<string | null>(null);
     const [currentEditMessageSelection, setCurrentEditMessageSelectionState] = useState<TextSelection | null>(null);
@@ -138,9 +138,8 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
                 return;
             }
 
-            const isInitialEdit = editingMessage == null;
             const didReportActionChange = prevEditingReportActionID !== editingReportActionID;
-            if (isInitialEdit || didReportActionChange) {
+            if (didReportActionChange) {
                 setEditingMessage(nextMessage);
                 setPrevEditingReportActionID(editingReportActionID);
                 const defaultSelection: TextSelection = {start: nextMessage.length, end: nextMessage.length};
@@ -159,7 +158,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
             editingReportActionID = ancestorReportAction.reportActionID;
             editingReportAction = ancestorReportAction;
 
-            if (editingState === null) {
+            if (editingState === 'off') {
                 setEditingState('editing');
             }
 
@@ -175,7 +174,7 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
                 editingReportActionID = reportActionIDOfDraft;
                 editingReportAction = reportActions?.[reportActionIDOfDraft] ?? null;
 
-                if (editingState === null) {
+                if (editingState === 'off') {
                     setEditingState('editing');
                 }
 
@@ -185,8 +184,8 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         }
     }
 
-    if (editingReportID == null && editingState !== null) {
-        setEditingState(null);
+    if (editingReportID == null && editingState !== 'off') {
+        setEditingState('off');
         setEditingMessage(null);
         setPrevEditingReportActionID(null);
         setCurrentEditMessageSelectionState(null);

From c1683acb45da0e0199d56e5f7ed264bc38c318a0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 09:41:22 +0000
Subject: [PATCH 139/233] fix: add missing import in tests

---
 tests/ui/ReportActionComposeTest.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/ui/ReportActionComposeTest.tsx b/tests/ui/ReportActionComposeTest.tsx
index acea21dfa352..b322d2004329 100644
--- a/tests/ui/ReportActionComposeTest.tsx
+++ b/tests/ui/ReportActionComposeTest.tsx
@@ -5,6 +5,7 @@ import {forceClearInput} from '@libs/ComponentUtils';
 import {onSubmitAction} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
 import {renderReportActionCompose} from '../utils/ReportActionComposeUtils';
 import * as TestHelper from '../utils/TestHelper';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';

From f33f1a763718008d9b8e4a927c02c50dad9ad7f0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 09:41:30 +0000
Subject: [PATCH 140/233] refactor: remove unused import

---
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 62d2f67944ee..b93c0215a271 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -514,7 +514,7 @@ const ContextMenuActions: ContextMenuAction[] = [
         icon: 'Pencil',
         shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}) =>
             type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport,
-        onPress: (closePopover, {reportID, reportAction, draftMessage, moneyRequestAction, introSelected, betas}) => {
+        onPress: (closePopover, {reportID, reportAction, moneyRequestAction, introSelected, betas}) => {
             if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) {
                 const editExpense = () => {
                     const childReportID = reportAction?.childReportID;

From 730a78746fad5c5ab1c769ffd0c961d1eaae0087 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 10:06:50 +0000
Subject: [PATCH 141/233] fix: editing stops after delete message modal opens

---
 .../ReportActionCompose.tsx                   | 21 +++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index eff19e6699cd..ae798176aae8 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -450,9 +450,11 @@ function ReportActionCompose({
             return;
         }
 
-        composerRef.current?.resetHeight();
-        if (isEditingInComposer) {
-            setDidResetComposerHeight(true);
+        if (effectiveDraft !== null && effectiveDraft !== '') {
+            composerRef.current?.resetHeight();
+            if (isEditingInComposer) {
+                setDidResetComposerHeight(true);
+            }
         }
 
         scheduleOnUI(() => {
@@ -464,7 +466,18 @@ function ReportActionCompose({
 
             clearWorklet?.();
         });
-    }, [isSendDisabled, debouncedCommentMaxLengthValidation, isEditingInComposer, editingMessage, draftComment, isComposerFullSize, validateAndSubmitDraft, reportID, composerRefShared]);
+    }, [
+        isSendDisabled,
+        debouncedCommentMaxLengthValidation,
+        isComposerFullSize,
+        isEditingInComposer,
+        editingMessage,
+        draftComment,
+        effectiveDraft,
+        reportID,
+        validateAndSubmitDraft,
+        composerRefShared,
+    ]);
     onSubmitAction = submitDraftAndClearComposer;
 
     const onTriggerAttachmentPicker = useCallback(() => {

From 00cb0859197d2ebc3fd46b99b6bd6e3a27108af4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 10:07:10 +0000
Subject: [PATCH 142/233] refactor: remove `cancelled` editing state

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx  | 1 +
 .../inbox/report/ReportActionCompose/useEditMessage.ts      | 6 +++---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx   | 2 +-
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index c753b8793fdb..ff42da1178d6 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -340,6 +340,7 @@ function ComposerWithSuggestions({
     const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
     useEffect(() => {
+        // If the draft message is already being submitted, do nothing.
         if (editingState === 'submitted') {
             return;
         }
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index d0bc1bddce10..95a538e689ad 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -68,7 +68,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        setEditingState('cancelled');
+        setEditingState('off');
 
         clearReportActionDrafts();
 
@@ -101,8 +101,6 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        setEditingState('submitted');
-
         const trimmedNewDraft = draftMessage.trim();
 
         // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
@@ -112,6 +110,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
+        setEditingState('submitted');
+
         editReportComment(
             originalReport,
             reportAction,
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index ac5526b40bbb..1962c3c47677 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -13,7 +13,7 @@ function NOOP() {
     return null;
 }
 
-type EditingState = 'off' | 'editing' | 'submitted' | 'cancelled';
+type EditingState = 'off' | 'editing' | 'submitted';
 
 type ReportActionActiveEdit = {
     editingReportID: string | null;

From 0b59e757c856f89f3b16fe2a86cb46e4ec9f047f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 10:08:42 +0000
Subject: [PATCH 143/233] fix: remove unused import

---
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index b93c0215a271..4613ca445032 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -172,7 +172,6 @@ import {
 import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils';
 import {setDownload} from '@userActions/Download';
 import {
-    clearReportActionDrafts,
     explain,
     markCommentAsUnread,
     navigateToAndOpenChildReport,

From 4f1fa0889836f1ce744dfa48306522f287374d4f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 11:54:58 +0000
Subject: [PATCH 144/233] refactor: simplify `ComposerWithSuggestions` edit
 state effect by adding key

---
 .../ComposerWithSuggestions.tsx               | 22 +++++--------------
 .../ReportActionCompose.tsx                   |  2 ++
 2 files changed, 7 insertions(+), 17 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index ff42da1178d6..8691ee00da5a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -335,34 +335,22 @@ function ComposerWithSuggestions({
         [currentEditMessageSelection, updateSelectionImperatively],
     );
 
-    const wasEditing = useRef(isEditing);
+    const isFirstEditingRenderRef = useRef(isEditing);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
     useEffect(() => {
         // If the draft message is already being submitted, do nothing.
-        if (editingState === 'submitted') {
-            return;
-        }
-
         if (editingState !== 'editing') {
-            if (wasEditing.current && wasEditingInComposerRef.current) {
-                // Editing just ended in the composer – restore the draft comment and its previous selection.
-                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current});
-            }
-
-            wasEditing.current = false;
-            wasEditingInComposerRef.current = shouldUseNarrowLayout;
-            previousDraftSelectionRef.current = null;
             return;
         }
 
         // Editing just started.
-        if (!wasEditing.current) {
+        if (isFirstEditingRenderRef.current) {
+            isFirstEditingRenderRef.current = false;
+
             // Store the draft selection before switching into edit mode so we can restore it later.
             previousDraftSelectionRef.current = selection;
-
-            wasEditing.current = true;
             wasEditingInComposerRef.current = shouldUseNarrowLayout;
 
             if (!shouldUseNarrowLayout) {
@@ -392,7 +380,7 @@ function ComposerWithSuggestions({
         if (shouldUseNarrowLayout) {
             applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
+    }, [applyComposerValue, draftComment, editingMessage, editingState, isEditing, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index ae798176aae8..c40c15bb315a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -170,6 +170,7 @@ function ReportActionCompose({
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     const isEditingInComposer = shouldUseNarrowLayout && editingState !== 'off' && !didResetComposerHeight;
+    const composerEditingToggleKey = `${editingState !== 'off'}`;
 
     useEffect(() => {
         if (editingState !== 'off' || !didResetComposerHeight) {
@@ -668,6 +669,7 @@ function ReportActionCompose({
                             />
                         )}
                         <ComposerWithSuggestions
+                            key={composerEditingToggleKey}
                             ref={(ref) => {
                                 composerRef.current = ref;
                                 composerRefShared.set({

From 7e0cb256c030beed1c7ffa88950eb2afca5f26a0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 11:58:10 +0000
Subject: [PATCH 145/233] fix: composer toggle key based on report action id

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index c40c15bb315a..0b20e39f1d54 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -170,7 +170,7 @@ function ReportActionCompose({
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     const isEditingInComposer = shouldUseNarrowLayout && editingState !== 'off' && !didResetComposerHeight;
-    const composerEditingToggleKey = `${editingState !== 'off'}`;
+    const composerEditingToggleKey = editingReportActionID;
 
     useEffect(() => {
         if (editingState !== 'off' || !didResetComposerHeight) {

From ba5f1c53d8a0ce0943572ba079ca270a75080e46 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 25 Mar 2026 17:40:35 +0000
Subject: [PATCH 146/233] fix: selection issues after publishing an edit

---
 .../ComposerWithSuggestions.tsx               | 21 ++++++++-----------
 .../ReportActionCompose/useEditMessage.ts     |  6 +++---
 .../report/ReportActionEditMessageContext.tsx | 19 +++++++++++++----
 tests/unit/hooks/useEditMessage.test.ts       |  3 ++-
 4 files changed, 29 insertions(+), 20 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 8691ee00da5a..92cb1557a453 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -337,7 +337,6 @@ function ComposerWithSuggestions({
 
     const isFirstEditingRenderRef = useRef(isEditing);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
-    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
     useEffect(() => {
         // If the draft message is already being submitted, do nothing.
@@ -345,12 +344,9 @@ function ComposerWithSuggestions({
             return;
         }
 
-        // Editing just started.
+        // This is the initial render of the composer when editing is turned on
         if (isFirstEditingRenderRef.current) {
             isFirstEditingRenderRef.current = false;
-
-            // Store the draft selection before switching into edit mode so we can restore it later.
-            previousDraftSelectionRef.current = selection;
             wasEditingInComposerRef.current = shouldUseNarrowLayout;
 
             if (!shouldUseNarrowLayout) {
@@ -596,13 +592,14 @@ function ComposerWithSuggestions({
                     syncSelectionWithOnChangeTextRef.current = {position, value: newComment};
                 }
 
-                setSelection((prevSelection) => ({
-                    ...prevSelection,
-                    start: position,
-                    end: position,
-                }));
-
-                setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: position, end: position}));
+                if (!isFirstEditingRenderRef.current) {
+                    setSelection((prevSelection) => ({
+                        ...prevSelection,
+                        start: position,
+                        end: position,
+                    }));
+                    setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: position, end: position}));
+                }
             }
 
             commentRef.current = newCommentConverted;
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 95a538e689ad..7e4627753169 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -47,7 +47,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
     const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID);
     const ancestors = useAncestors(originalReport);
 
-    const {setEditingState} = useReportActionActiveEditActions();
+    const {stopEditing, submitEdit} = useReportActionActiveEditActions();
 
     useEffect(() => {
         // required for keeping last state of isFocused variable
@@ -68,7 +68,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        setEditingState('off');
+        stopEditing();
 
         clearReportActionDrafts();
 
@@ -110,7 +110,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
             return;
         }
 
-        setEditingState('submitted');
+        submitEdit();
 
         editReportComment(
             originalReport,
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 1962c3c47677..4d8878637cf2 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -28,9 +28,10 @@ type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
 };
 
 type ReportActionEditMessageContextActions = {
-    setEditingState: Dispatch<SetStateAction<EditingState>>;
     setEditingMessage: Dispatch<SetStateAction<string | null>>;
     setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
+    submitEdit: () => void;
+    stopEditing: () => void;
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
@@ -43,9 +44,10 @@ const ReportActionEditMessageContext = createContext<ReportActionEditMessageCont
 });
 
 const ReportActionEditMessageActionsContext = createContext<ReportActionEditMessageContextActions>({
-    setEditingState: NOOP,
     setEditingMessage: NOOP,
     setCurrentEditMessageSelection: NOOP,
+    submitEdit: NOOP,
+    stopEditing: NOOP,
 });
 
 type ReportActionEditMessageContextProviderProps = {
@@ -184,11 +186,19 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
         }
     }
 
-    if (editingReportID == null && editingState !== 'off') {
+    const submitEdit = () => {
+        setEditingState('submitted');
+    };
+
+    const stopEditing = () => {
         setEditingState('off');
         setEditingMessage(null);
         setPrevEditingReportActionID(null);
         setCurrentEditMessageSelectionState(null);
+    };
+
+    if (editingReportID == null && editingState !== 'off') {
+        stopEditing();
     }
 
     const setCurrentEditMessageSelection = (setSelectionStateAction: SetStateAction<TextSelection | null>) => {
@@ -209,9 +219,10 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     };
 
     const actions: ReportActionEditMessageContextActions = {
-        setEditingState,
         setEditingMessage,
         setCurrentEditMessageSelection,
+        submitEdit,
+        stopEditing,
     };
 
     return (
diff --git a/tests/unit/hooks/useEditMessage.test.ts b/tests/unit/hooks/useEditMessage.test.ts
index 607f89f05366..32f3f7c36154 100644
--- a/tests/unit/hooks/useEditMessage.test.ts
+++ b/tests/unit/hooks/useEditMessage.test.ts
@@ -19,7 +19,8 @@ jest.mock('@libs/actions/Report', () => {
 
 jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => ({
     useReportActionActiveEditActions: () => ({
-        setEditingState: jest.fn(),
+        submitEdit: jest.fn(),
+        stopEditing: jest.fn(),
     }),
 }));
 

From d71b21384981fe44c83b55c3d5bc081d65f89407 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 11:38:03 +0100
Subject: [PATCH 147/233] fix: composer not visible

---
 src/pages/inbox/report/ReportFooter.tsx | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx
index 854d54e20a3c..0af7136631ea 100644
--- a/src/pages/inbox/report/ReportFooter.tsx
+++ b/src/pages/inbox/report/ReportFooter.tsx
@@ -48,8 +48,7 @@ function ReportFooter() {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
     const {isOffline} = useNetwork();
-    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth -- isSmallScreenWidth guards composer visibility on mobile during keyboard events, shouldUseNarrowLayout would wrongly hide it in RHP
-    const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Lightbulb']);
 
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`);
@@ -86,7 +85,7 @@ function ReportFooter() {
     const chatFooterStyles = {...styles.chatFooter, minHeight: !isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0};
 
     // Happy path — user can compose
-    if (!shouldHideComposer && !isSmallScreenWidth) {
+    if (!shouldHideComposer) {
         return (
             <View style={[chatFooterStyles, isComposerFullSize && styles.chatFooterFullCompose]}>
                 <SwipeableView onSwipeDown={Keyboard.dismiss}>

From 585c3d3ef0f668bf939baeb222185bbcf6d29a87 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 12:22:15 +0100
Subject: [PATCH 148/233] fix: blur composer when the user stops editing

---
 .../ReportActionCompose.tsx                   | 36 +++++++++++++------
 1 file changed, 26 insertions(+), 10 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 6977899b7b53..52199536c0ad 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -229,6 +229,13 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
         canEvict: false,
     });
 
+    /**
+     * Updates the Highlight state of the composer
+     */
+    const [isFocused, setIsFocused] = useState(() => {
+        return shouldFocusInputOnScreenFocus && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
+    });
+
     const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
@@ -243,23 +250,32 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
         setDidResetComposerHeight(false);
     }, [didResetComposerHeight, editingState]);
 
+    // Track whether the user was editing a message before
+    const wasEditingBefore = useRef(false);
+    useEffect(() => {
+        if (editingState === 'off') {
+            return;
+        }
+        wasEditingBefore.current = true;
+    }, [editingState]);
+
+    // Reset composer focus when editing is turned off, but not on the initial chat open.
+    useEffect(() => {
+        if (editingState !== 'off' || !wasEditingBefore.current) {
+            return;
+        }
+
+        setIsFocused(false);
+    }, [editingState]);
+
     const reportActionKeys = useMemo(() => (rawReportActions ? Object.keys(rawReportActions) : []), [rawReportActions]);
     const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionKeys.at(-1), [editingReportActionID, reportActionKeys]);
 
-    const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
-
     const [targetReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${effectiveTransactionThreadReportID ?? reportID}`);
     const reportAncestors = useAncestors(report);
     const targetReportAncestors = useAncestors(targetReport);
     const {scrollOffsetRef} = useContext(ActionListContext);
 
-    /**
-     * Updates the Highlight state of the composer
-     */
-    const [isFocused, setIsFocused] = useState(() => {
-        return shouldFocusComposerOnScreenFocus && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
-    });
-
     const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize);
 
     const {isScrollLayoutTriggered, raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
@@ -784,7 +800,7 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
                                 onAddActionPressed={onAddActionPressed}
                                 onItemSelected={onItemSelected}
                                 onCanceledAttachmentPicker={() => {
-                                    if (!shouldFocusComposerOnScreenFocus) {
+                                    if (!shouldFocusInputOnScreenFocus) {
                                         return;
                                     }
                                     focus();

From 02eabb6100c7ad8da41648897cea31e1bc285ed7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 12:29:15 +0100
Subject: [PATCH 149/233] fix: only blur composer focus when no draft comment
 exists

---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 52199536c0ad..e2c7e9e6a813 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -261,7 +261,7 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
 
     // Reset composer focus when editing is turned off, but not on the initial chat open.
     useEffect(() => {
-        if (editingState !== 'off' || !wasEditingBefore.current) {
+        if (editingState !== 'off' || !!draftComment || !wasEditingBefore.current) {
             return;
         }
 

From c8df1b2ba81b8deeaf59c19abbc7dee84d2e32b8 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 12:34:10 +0100
Subject: [PATCH 150/233] Revert "refactor: simplify `ComposerWithSuggestions`
 edit state effect by adding key"

This reverts commit 4f1fa0889836f1ce744dfa48306522f287374d4f.
---
 .../ComposerWithSuggestions.tsx               | 28 ++++++++++++----
 .../ReportActionCompose.tsx                   | 32 +++++++++----------
 2 files changed, 37 insertions(+), 23 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 92cb1557a453..8107686f0b5d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -335,18 +335,34 @@ function ComposerWithSuggestions({
         [currentEditMessageSelection, updateSelectionImperatively],
     );
 
-    const isFirstEditingRenderRef = useRef(isEditing);
+    const wasEditing = useRef(isEditing);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
+    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
     useEffect(() => {
         // If the draft message is already being submitted, do nothing.
+        if (editingState === 'submitted') {
+            return;
+        }
+
         if (editingState !== 'editing') {
+            if (wasEditing.current && wasEditingInComposerRef.current) {
+                // Editing just ended in the composer – restore the draft comment and its previous selection.
+                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current});
+            }
+
+            wasEditing.current = false;
+            wasEditingInComposerRef.current = shouldUseNarrowLayout;
+            previousDraftSelectionRef.current = null;
             return;
         }
 
-        // This is the initial render of the composer when editing is turned on
-        if (isFirstEditingRenderRef.current) {
-            isFirstEditingRenderRef.current = false;
+        // Editing just started.
+        if (!wasEditing.current) {
+            // Store the draft selection before switching into edit mode so we can restore it later.
+            previousDraftSelectionRef.current = selection;
+
+            wasEditing.current = true;
             wasEditingInComposerRef.current = shouldUseNarrowLayout;
 
             if (!shouldUseNarrowLayout) {
@@ -376,7 +392,7 @@ function ComposerWithSuggestions({
         if (shouldUseNarrowLayout) {
             applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingState, isEditing, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
+    }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -592,7 +608,7 @@ function ComposerWithSuggestions({
                     syncSelectionWithOnChangeTextRef.current = {position, value: newComment};
                 }
 
-                if (!isFirstEditingRenderRef.current) {
+                if (!wasEditing.current) {
                     setSelection((prevSelection) => ({
                         ...prevSelection,
                         start: position,
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index e2c7e9e6a813..102a9ba857c8 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -240,7 +240,6 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     const isEditingInComposer = shouldUseNarrowLayout && editingState !== 'off' && !didResetComposerHeight;
-    const composerEditingToggleKey = editingReportActionID;
 
     useEffect(() => {
         if (editingState !== 'off' || !didResetComposerHeight) {
@@ -250,23 +249,23 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
         setDidResetComposerHeight(false);
     }, [didResetComposerHeight, editingState]);
 
-    // Track whether the user was editing a message before
-    const wasEditingBefore = useRef(false);
-    useEffect(() => {
-        if (editingState === 'off') {
-            return;
-        }
-        wasEditingBefore.current = true;
-    }, [editingState]);
+    // // Track whether the user was editing a message before
+    // const wasEditingBefore = useRef(false);
+    // useEffect(() => {
+    //     if (editingState === 'off') {
+    //         return;
+    //     }
+    //     wasEditingBefore.current = true;
+    // }, [editingState]);
 
-    // Reset composer focus when editing is turned off, but not on the initial chat open.
-    useEffect(() => {
-        if (editingState !== 'off' || !!draftComment || !wasEditingBefore.current) {
-            return;
-        }
+    // // Reset composer focus when editing is turned off, but not on the initial chat open.
+    // useEffect(() => {
+    //     if (editingState !== 'off' || !!draftComment || !wasEditingBefore.current) {
+    //         return;
+    //     }
 
-        setIsFocused(false);
-    }, [editingState]);
+    //     setIsFocused(false);
+    // }, [draftComment, editingState]);
 
     const reportActionKeys = useMemo(() => (rawReportActions ? Object.keys(rawReportActions) : []), [rawReportActions]);
     const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionKeys.at(-1), [editingReportActionID, reportActionKeys]);
@@ -810,7 +809,6 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
                             />
                         )}
                         <ComposerWithSuggestions
-                            key={composerEditingToggleKey}
                             ref={(ref) => {
                                 composerRef.current = ref;
                                 composerRefShared.set({

From eaca09d3f67dc9f97248dacd88aafb465809808f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 12:52:46 +0100
Subject: [PATCH 151/233] fix: natively update the text value to force
 re-layout

---
 .../ComposerWithSuggestions.tsx               | 30 ++++++++++++++-----
 1 file changed, 23 insertions(+), 7 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 8107686f0b5d..47386779d8ac 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -292,12 +292,24 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
-    const updateSelectionImperatively = useCallback((start: number, end: number) => {
+    const updateValueImperatively = useCallback((nextValue: string) => {
         if (!isIOSNative) {
             return;
         }
 
-        // ensure that selection is set imperatively after all state changes are effective
+        // Ensure that native text value is set imperatively after all state changes are effective
+        // eslint-disable-next-line @typescript-eslint/no-deprecated
+        InteractionManager.runAfterInteractions(() => {
+            composerRef.current?.setNativeProps({text: nextValue});
+        });
+    }, []);
+
+    const updateNativeSelectionValue = useCallback((start: number, end: number) => {
+        if (!isIOSNative) {
+            return;
+        }
+
+        // Ensure that native selection value is set imperatively after all state changes are effective
         // eslint-disable-next-line @typescript-eslint/no-deprecated
         InteractionManager.runAfterInteractions(() => {
             // note: this implementation is only available on non-web RN, thus the wrapping
@@ -325,14 +337,18 @@ function ComposerWithSuggestions({
             emojisPresentBefore.current = extractEmojis(nextValue);
 
             setValue(nextValue);
+            // We need to manually update the native text prop,
+            // in order to force a re-calculation of the composer height and layout.
+            updateValueImperatively(nextValue);
+
             setSelection(selectionToApply);
-            updateSelectionImperatively(selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
+            updateNativeSelectionValue(selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
 
             if (options?.isEditingInComposer) {
                 composerRef.current?.focus();
             }
         },
-        [currentEditMessageSelection, updateSelectionImperatively],
+        [currentEditMessageSelection, updateNativeSelectionValue, updateValueImperatively],
     );
 
     const wasEditing = useRef(isEditing);
@@ -392,7 +408,7 @@ function ComposerWithSuggestions({
         if (shouldUseNarrowLayout) {
             applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateSelectionImperatively]);
+    }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateNativeSelectionValue]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -778,9 +794,9 @@ function ComposerWithSuggestions({
 
             const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position;
             syncSelectionWithOnChangeTextRef.current = null;
-            updateSelectionImperatively(positionSnapshot, positionSnapshot);
+            updateNativeSelectionValue(positionSnapshot, positionSnapshot);
         },
-        [clearComposerHeight, updateComment, updateSelectionImperatively],
+        [clearComposerHeight, updateComment, updateNativeSelectionValue],
     );
 
     const onSelectionChange = useCallback(

From acfd28c3566e25deff0b5d588cbe12ac64e433f5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 12:53:19 +0100
Subject: [PATCH 152/233] fix: remove unused code

---
 .../ReportActionCompose.tsx                    | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 102a9ba857c8..191bee65d2f8 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -249,24 +249,6 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
         setDidResetComposerHeight(false);
     }, [didResetComposerHeight, editingState]);
 
-    // // Track whether the user was editing a message before
-    // const wasEditingBefore = useRef(false);
-    // useEffect(() => {
-    //     if (editingState === 'off') {
-    //         return;
-    //     }
-    //     wasEditingBefore.current = true;
-    // }, [editingState]);
-
-    // // Reset composer focus when editing is turned off, but not on the initial chat open.
-    // useEffect(() => {
-    //     if (editingState !== 'off' || !!draftComment || !wasEditingBefore.current) {
-    //         return;
-    //     }
-
-    //     setIsFocused(false);
-    // }, [draftComment, editingState]);
-
     const reportActionKeys = useMemo(() => (rawReportActions ? Object.keys(rawReportActions) : []), [rawReportActions]);
     const isEditingLastReportAction = useMemo(() => editingReportActionID === reportActionKeys.at(-1), [editingReportActionID, reportActionKeys]);
 

From 383ec9f83a5774bb9422263a3980bd72a5d78aaa Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 13:14:10 +0100
Subject: [PATCH 153/233] fix: remove unnecessary duplicate update of composer
 value

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx    | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 47386779d8ac..0f600a2dc1a7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -404,10 +404,6 @@ function ComposerWithSuggestions({
             wasEditingInComposerRef.current = false;
             applyComposerValue(draftComment ?? '');
         }
-
-        if (shouldUseNarrowLayout) {
-            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
-        }
     }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateNativeSelectionValue]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();

From 00bb4666f7dcd65bbd334e77fbd5f0944e1cb366 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 13:17:13 +0100
Subject: [PATCH 154/233] fix: composer resizing without adding react key to
 composer

---
 .../ComposerWithSuggestions.tsx               | 26 +++++++------------
 1 file changed, 9 insertions(+), 17 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 0f600a2dc1a7..e1253195dc54 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -292,18 +292,6 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
-    const updateValueImperatively = useCallback((nextValue: string) => {
-        if (!isIOSNative) {
-            return;
-        }
-
-        // Ensure that native text value is set imperatively after all state changes are effective
-        // eslint-disable-next-line @typescript-eslint/no-deprecated
-        InteractionManager.runAfterInteractions(() => {
-            composerRef.current?.setNativeProps({text: nextValue});
-        });
-    }, []);
-
     const updateNativeSelectionValue = useCallback((start: number, end: number) => {
         if (!isIOSNative) {
             return;
@@ -322,6 +310,7 @@ function ComposerWithSuggestions({
         isEditingInComposer?: boolean;
         shouldMoveSelectionToEnd?: boolean;
         selection?: TextSelection | null;
+        shouldForceNativeValueUpdate?: boolean;
     };
 
     const applyComposerValue = useCallback(
@@ -338,8 +327,11 @@ function ComposerWithSuggestions({
 
             setValue(nextValue);
             // We need to manually update the native text prop,
-            // in order to force a re-calculation of the composer height and layout.
-            updateValueImperatively(nextValue);
+            // in order to force a re-calculation of the composer height and layout,
+            // when the composer changes in or out of edit mode.
+            if (isIOSNative && options?.shouldForceNativeValueUpdate) {
+                composerRef.current?.setNativeProps({text: nextValue});
+            }
 
             setSelection(selectionToApply);
             updateNativeSelectionValue(selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
@@ -348,7 +340,7 @@ function ComposerWithSuggestions({
                 composerRef.current?.focus();
             }
         },
-        [currentEditMessageSelection, updateNativeSelectionValue, updateValueImperatively],
+        [currentEditMessageSelection, updateNativeSelectionValue],
     );
 
     const wasEditing = useRef(isEditing);
@@ -364,7 +356,7 @@ function ComposerWithSuggestions({
         if (editingState !== 'editing') {
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
-                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current});
+                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
             }
 
             wasEditing.current = false;
@@ -387,7 +379,7 @@ function ComposerWithSuggestions({
             }
             // In narrow layout we always show the message being edited.
             // When starting to edit in the composer, always place the cursor at the end of the message.
-            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldMoveSelectionToEnd: true});
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldMoveSelectionToEnd: true, shouldForceNativeValueUpdate: true});
             return;
         }
 

From 85bc5cbbfd0458eec2d417000af10907c7a37d46 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 13:20:56 +0100
Subject: [PATCH 155/233] fix: restore previous focus after editing stops

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx   | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index e1253195dc54..9b3b72b8b8b9 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -345,6 +345,7 @@ function ComposerWithSuggestions({
 
     const wasEditing = useRef(isEditing);
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
+    const wasComposerFocusedBeforeEditingRef = useRef(false);
     const previousDraftSelectionRef = useRef<TextSelection | null>(null);
 
     useEffect(() => {
@@ -357,6 +358,9 @@ function ComposerWithSuggestions({
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
                 applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
+                if (!wasComposerFocusedBeforeEditingRef.current) {
+                    composerRef.current?.blur();
+                }
             }
 
             wasEditing.current = false;
@@ -367,6 +371,7 @@ function ComposerWithSuggestions({
 
         // Editing just started.
         if (!wasEditing.current) {
+            wasComposerFocusedBeforeEditingRef.current = composerRef.current?.isFocused() ?? false;
             // Store the draft selection before switching into edit mode so we can restore it later.
             previousDraftSelectionRef.current = selection;
 

From 7f6e7577e7f76521f909b945433d1511de7badc1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 3 Apr 2026 13:27:52 +0100
Subject: [PATCH 156/233] fix: still update composer value when
 editingReportAction changes

---
 .../ReportActionCompose/ComposerWithSuggestions.tsx   | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 9b3b72b8b8b9..43d17c64ed5d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -347,6 +347,7 @@ function ComposerWithSuggestions({
     const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
     const wasComposerFocusedBeforeEditingRef = useRef(false);
     const previousDraftSelectionRef = useRef<TextSelection | null>(null);
+    const previousEditingReportActionIDRef = useRef<string | null>(null);
 
     useEffect(() => {
         // If the draft message is already being submitted, do nothing.
@@ -400,8 +401,16 @@ function ComposerWithSuggestions({
         if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
             wasEditingInComposerRef.current = false;
             applyComposerValue(draftComment ?? '');
+            return;
+        }
+
+        // The editing report action and message changed
+        if (shouldUseNarrowLayout && editingReportActionID !== previousEditingReportActionIDRef.current) {
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldForceNativeValueUpdate: true});
         }
-    }, [applyComposerValue, draftComment, editingMessage, editingState, selection, shouldUseNarrowLayout, updateNativeSelectionValue]);
+
+        previousEditingReportActionIDRef.current = editingReportActionID;
+    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout, updateNativeSelectionValue]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank

From 1c3992f6d626d4d01a48c7e1678d947b37246191 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 10 Apr 2026 19:41:38 +0100
Subject: [PATCH 157/233] Update Mobile-Expensify

---
 Mobile-Expensify | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index 3cbd49364f08..2e73c3e8065b 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 3cbd49364f08cf2a646d7a2f4beca5e11cddaf26
+Subproject commit 2e73c3e8065b85bc564c7657a33edfa18a0fa8b4

From da6c2208ff54da8de64fe5723a24e69aa22b3eaa Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 10 Apr 2026 22:36:46 +0100
Subject: [PATCH 158/233] fix: TS and ESLint errors

---
 .../report/ReportActionCompose/ComposerContext.ts      |  2 +-
 .../report/ReportActionCompose/ComposerProvider.tsx    |  5 ++---
 .../ComposerWithSuggestions.tsx                        | 10 +++++-----
 .../ComposerWithSuggestions/index.tsx                  |  3 ---
 .../report/ReportActionCompose/ReportActionCompose.tsx |  2 +-
 .../report/ReportActionCompose/useComposerFocus.ts     |  2 +-
 6 files changed, 10 insertions(+), 14 deletions(-)
 rename src/pages/inbox/report/ReportActionCompose/{ComposerWithSuggestions => }/ComposerWithSuggestions.tsx (98%)
 delete mode 100644 src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
index 3378fb68bbd0..10231c09cedf 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
@@ -4,7 +4,7 @@ import type {BlurEvent, TextInputSelectionChangeEvent, View} from 'react-native'
 import type {Emoji} from '@assets/emojis/types';
 import type {Mention} from '@components/MentionSuggestions';
 import type {FileObject} from '@src/types/utils/Attachment';
-import type {ComposerRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import type {ComposerRef} from './ComposerWithSuggestions';
 
 type SuggestionsRef = {
     resetSuggestions: () => void;
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index 64cdd5731924..125f04bcaaa4 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -15,7 +15,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type {FileObject} from '@src/types/utils/Attachment';
 import {ComposerActionsContext, ComposerMetaContext, ComposerSendActionsContext, ComposerSendStateContext, ComposerStateContext, ComposerTextContext} from './ComposerContext';
 import type {SuggestionsRef} from './ComposerContext';
-import type {ComposerRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import type {ComposerRef} from './ComposerWithSuggestions';
 import useComposerFocus from './useComposerFocus';
 
 const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
@@ -27,7 +27,6 @@ type ComposerProviderProps = {
 
 function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
-    const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT);
     const [initialModalState] = useOnyx(ONYXKEYS.MODAL);
     const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`);
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
@@ -35,7 +34,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
 
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
 
-    const initialFocused = shouldFocusComposerOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
+    const initialFocused = shouldFocusComposerOnScreenFocus && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
 
     const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize);
     const [isMenuVisible, setMenuVisibility] = useState(false);
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
similarity index 98%
rename from src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
rename to src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index ff903d77e125..34cfc7b064b7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -48,11 +48,6 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag
 import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUtils';
 import updateMultilineInputRange from '@libs/updateMultilineInputRange';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
-import getCursorPosition from '@pages/inbox/report/ReportActionCompose/getCursorPosition';
-import getScrollPosition from '@pages/inbox/report/ReportActionCompose/getScrollPosition';
-import type {SuggestionsRef} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import SilentCommentUpdater from '@pages/inbox/report/ReportActionCompose/SilentCommentUpdater';
-import Suggestions from '@pages/inbox/report/ReportActionCompose/Suggestions';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
 import {inputFocusChange} from '@userActions/InputFocus';
@@ -66,6 +61,11 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
+import getCursorPosition from './getCursorPosition';
+import getScrollPosition from './getScrollPosition';
+import type {SuggestionsRef} from './ReportActionCompose';
+import SilentCommentUpdater from './SilentCommentUpdater';
+import Suggestions from './Suggestions';
 
 type SyncSelection = {
     position: number;
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx
deleted file mode 100644
index f2aebd390ba6..000000000000
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import ComposerWithSuggestions from './ComposerWithSuggestions';
-
-export default ComposerWithSuggestions;
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 7f3600739379..964cc6e96b66 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -99,7 +99,7 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
 import ComposerWithSuggestions from './ComposerWithSuggestions';
-import type {ComposerRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import type {ComposerRef} from './ComposerWithSuggestions';
 import SendButton from './SendButton';
 import useAttachmentUploadValidation from './useAttachmentUploadValidation';
 
diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts b/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts
index 78ca74dba6e3..84dc41c5187e 100644
--- a/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts
@@ -3,7 +3,7 @@ import type {RefObject} from 'react';
 import type {BlurEvent, View} from 'react-native';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
 import type {SuggestionsRef} from './ComposerContext';
-import type {ComposerRef} from './ComposerWithSuggestions/ComposerWithSuggestions';
+import type {ComposerRef} from './ComposerWithSuggestions';
 
 const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc();
 

From e3ca84d85a0dc30c42b060b46cb054214924e1d5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 10 Apr 2026 22:36:46 +0100
Subject: [PATCH 159/233] fix: apply changes from PR also in newly added
 `ComposerProvider` and context

---
 .../ReportActionCompose/ComposerContext.ts    |  6 +--
 .../ReportActionCompose/ComposerProvider.tsx  | 46 ++++++-------------
 2 files changed, 17 insertions(+), 35 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
index 10231c09cedf..2d19c9744a11 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
@@ -4,7 +4,7 @@ import type {BlurEvent, TextInputSelectionChangeEvent, View} from 'react-native'
 import type {Emoji} from '@assets/emojis/types';
 import type {Mention} from '@components/MentionSuggestions';
 import type {FileObject} from '@src/types/utils/Attachment';
-import type {ComposerRef} from './ComposerWithSuggestions';
+import type {ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
 
 type SuggestionsRef = {
     resetSuggestions: () => void;
@@ -39,7 +39,7 @@ type ComposerActions = {
     setValue: (v: string) => void;
     setMenuVisibility: (v: boolean) => void;
     setIsFullComposerAvailable: (v: boolean) => void;
-    setComposerRef: (ref: ComposerRef | null) => void;
+    setComposerRef: (ref: ComposerWithSuggestionsRef | null) => void;
     focus: () => void;
     onBlur: (event: BlurEvent) => void;
     onFocus: () => void;
@@ -58,7 +58,7 @@ type ComposerSendActions = {
 // Frozen — stable refs, set once
 type ComposerMeta = {
     containerRef: RefObject<View | null>;
-    composerRef: RefObject<ComposerRef | null>;
+    composerRef: RefObject<ComposerWithSuggestionsRef | null>;
     suggestionsRef: RefObject<SuggestionsRef | null>;
     actionButtonRef: RefObject<View | HTMLDivElement | null>;
     isNextModalWillOpenRef: RefObject<boolean>;
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index 125f04bcaaa4..3c4ca7b09574 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -1,13 +1,11 @@
-import lodashDebounce from 'lodash/debounce';
 import React, {useRef, useState} from 'react';
 import type {View} from 'react-native';
 import {useSharedValue} from 'react-native-reanimated';
 import {scheduleOnUI} from 'react-native-worklets';
-import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength';
-import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitleLength';
 import useOnyx from '@hooks/useOnyx';
 import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
 import {chatIncludesConcierge} from '@libs/ReportUtils';
+import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import {setIsComposerFullSize} from '@userActions/Report';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';
 import CONST from '@src/CONST';
@@ -15,8 +13,9 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type {FileObject} from '@src/types/utils/Attachment';
 import {ComposerActionsContext, ComposerMetaContext, ComposerSendActionsContext, ComposerSendStateContext, ComposerStateContext, ComposerTextContext} from './ComposerContext';
 import type {SuggestionsRef} from './ComposerContext';
-import type {ComposerRef} from './ComposerWithSuggestions';
+import type {ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
 import useComposerFocus from './useComposerFocus';
+import useDebouncedCommentMaxLengthValidation from './useDebouncedCommentMaxLengthValidation';
 
 const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
 
@@ -49,38 +48,21 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const userBlockedFromConcierge = isBlockedFromConciergeUserAction(blockedFromConcierge);
     const isBlockedFromConcierge = includesConcierge && userBlockedFromConcierge;
 
-    const {hasExceededMaxCommentLength, validateCommentMaxLength, setHasExceededMaxCommentLength} = useHandleExceedMaxCommentLength();
-    const {hasExceededMaxTaskTitleLength, validateTaskTitleMaxLength, setHasExceededMaxTitleLength} = useHandleExceedMaxTaskTitleLength();
-
-    let exceededMaxLength: number | null = null;
-    if (hasExceededMaxTaskTitleLength) {
-        exceededMaxLength = CONST.TITLE_CHARACTER_LIMIT;
-    } else if (hasExceededMaxCommentLength) {
-        exceededMaxLength = CONST.MAX_COMMENT_LENGTH;
-    }
+    const {editingReportAction} = useReportActionActiveEdit();
+    const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({
+        reportID,
+        isEditing: !!editingReportAction,
+    });
 
     const isSendDisabled = isEmpty || isBlockedFromConcierge || !!exceededMaxLength;
 
-    const validateMaxLength = (v: string) => {
-        const taskCommentMatch = v?.match(CONST.REGEX.TASK_TITLE_WITH_OPTIONAL_SHORT_MENTION);
-        if (taskCommentMatch) {
-            const title = taskCommentMatch?.[3] ? taskCommentMatch[3].trim().replaceAll('\n', ' ') : '';
-            setHasExceededMaxCommentLength(false);
-            return validateTaskTitleMaxLength(title);
-        }
-        setHasExceededMaxTitleLength(false);
-        return validateCommentMaxLength(v, {reportID});
-    };
-
-    const debouncedValidate = lodashDebounce(validateMaxLength, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME, {leading: true});
-
     const containerRef = useRef<View>(null);
     const suggestionsRef = useRef<SuggestionsRef>(null);
-    const composerRef = useRef<ComposerRef | null>(null);
+    const composerRef = useRef<ComposerWithSuggestionsRef | null>(null);
     const actionButtonRef = useRef<View | HTMLDivElement | null>(null);
     const attachmentFileRef = useRef<FileObject | FileObject[] | null>(null);
 
-    const composerRefShared = useSharedValue<Partial<ComposerRef>>({});
+    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
 
     const {isFocused, onBlur, onFocus, focus, onAddActionPressed, onItemSelected, onTriggerAttachmentPicker, isNextModalWillOpenRef} = useComposerFocus({
         composerRef,
@@ -98,7 +80,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     };
 
     const handleSendMessage = () => {
-        if (isSendDisabled || !debouncedValidate.flush()) {
+        if (isSendDisabled || !debouncedCommentMaxLengthValidation.flush()) {
             return;
         }
 
@@ -118,7 +100,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         });
     };
 
-    const setComposerRef = (ref: ComposerRef | null) => {
+    const setComposerRef = (ref: ComposerWithSuggestionsRef | null) => {
         composerRef.current = ref;
         composerRefShared.set({
             clearWorklet: ref?.clearWorklet,
@@ -129,7 +111,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         if (v.length === 0 && isComposerFullSize) {
             setIsComposerFullSize(reportID, false);
         }
-        debouncedValidate(v);
+        debouncedCommentMaxLengthValidation(v);
     };
 
     const text = value;
@@ -143,7 +125,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const composerSendState = {
         isSendDisabled,
         exceededMaxLength,
-        hasExceededMaxTaskTitleLength,
+        hasExceededMaxTaskTitleLength: isTaskTitle && isExceedingMaxLength,
         isBlockedFromConcierge,
     };
 

From 99c61680cf667da542be6dcd4dcc8bc27e7a0190 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 10 Apr 2026 22:53:35 +0100
Subject: [PATCH 160/233] Update package-lock.json

---
 package-lock.json | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index d99aad2efc26..a6b83ff7c884 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34857,17 +34857,6 @@
         "react-native-blob-util": ">=0.13.7"
       }
     },
-    "node_modules/react-native-performance": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-6.0.0.tgz",
-      "integrity": "sha512-Sca75O8jhqXAnNbqvINnrw248Kv9cIwoGxToD8u2uX+BrkAxxXS+YhClEV5L3JdiOpdNCO1MJ5R9bgs2VkNpFg==",
-      "license": "MIT",
-      "optional": true,
-      "peer": true,
-      "peerDependencies": {
-        "react-native": "*"
-      }
-    },
     "node_modules/react-native-permissions": {
       "version": "5.4.0",
       "license": "MIT",

From 84af4ce9d3239888452d35b384a08fcf0a420f3b Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 10 Apr 2026 22:56:20 +0100
Subject: [PATCH 161/233] Revert "Update package-lock.json"

This reverts commit 99c61680cf667da542be6dcd4dcc8bc27e7a0190.
---
 package-lock.json | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/package-lock.json b/package-lock.json
index a6b83ff7c884..d99aad2efc26 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34857,6 +34857,17 @@
         "react-native-blob-util": ">=0.13.7"
       }
     },
+    "node_modules/react-native-performance": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-6.0.0.tgz",
+      "integrity": "sha512-Sca75O8jhqXAnNbqvINnrw248Kv9cIwoGxToD8u2uX+BrkAxxXS+YhClEV5L3JdiOpdNCO1MJ5R9bgs2VkNpFg==",
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "peerDependencies": {
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-permissions": {
       "version": "5.4.0",
       "license": "MIT",

From acc8ba577d1445201254d2c304a9de1afb39eb2a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 10 Apr 2026 23:05:29 +0100
Subject: [PATCH 162/233] fix: TS and ESLint errors

---
 .../inbox/report/ReportActionCompose/useComposerFocus.ts      | 4 ++--
 src/setup/index.ts                                            | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts b/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts
index 84dc41c5187e..1329f2aae32c 100644
--- a/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts
@@ -3,12 +3,12 @@ import type {RefObject} from 'react';
 import type {BlurEvent, View} from 'react-native';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
 import type {SuggestionsRef} from './ComposerContext';
-import type {ComposerRef} from './ComposerWithSuggestions';
+import type {ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
 
 const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc();
 
 type UseComposerFocusParams = {
-    composerRef: RefObject<ComposerRef | null>;
+    composerRef: RefObject<ComposerWithSuggestionsRef | null>;
     suggestionsRef: RefObject<SuggestionsRef | null>;
     actionButtonRef: RefObject<View | HTMLDivElement | null>;
     initialFocused: boolean;
diff --git a/src/setup/index.ts b/src/setup/index.ts
index 81b73dea4027..b1cd0a809a9d 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -48,7 +48,7 @@ export default function () {
             [ONYXKEYS.SESSION]: {loading: false},
             [ONYXKEYS.ACCOUNT]: CONST.DEFAULT_ACCOUNT_DATA,
             [ONYXKEYS.NETWORK]: CONST.DEFAULT_NETWORK_DATA,
-            [ONYXKEYS.RAM_ONLY_IS_SIDEBAR_LOADED]: false,
+            [ONYXKEYS.IS_SIDEBAR_LOADED]: false,
             [ONYXKEYS.MODAL]: {
                 isVisible: false,
                 willAlertModalBecomeVisible: false,

From a610c28ccf160c2aea94a88625d631fb066d1fe8 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 16 Apr 2026 17:20:24 +0100
Subject: [PATCH 163/233] fix: React Compiler compliance

---
 src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
index 5d7c1ec7082a..37660344d0c3 100644
--- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
+++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
@@ -55,7 +55,7 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
 
     const {editingMessage, editingReportAction} = useReportActionActiveEdit();
 
-    const draftMessage = editingReportAction?.reportActionID === action?.reportActionID ? (editingMessage ?? undefined) : undefined;
+    const draftMessage = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
 
     if (!action || !report) {
         return null;

From 810f94a4a4b1eedf2edd00c6cadfc3d275aaa57f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 11:19:04 +0100
Subject: [PATCH 164/233] fix: `ImageSVG` icon fill color not updating on
 Android

---
 src/components/ImageSVG/index.android.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/ImageSVG/index.android.tsx b/src/components/ImageSVG/index.android.tsx
index 2da157479722..f547d5a96abd 100644
--- a/src/components/ImageSVG/index.android.tsx
+++ b/src/components/ImageSVG/index.android.tsx
@@ -5,7 +5,6 @@ import getImageRecyclingKey from '@libs/getImageRecyclingKey';
 import type ImageSVGProps from './types';
 
 function ImageSVG({src, width = '100%', height = '100%', fill, contentFit = 'cover', style, onLoadEnd}: ImageSVGProps) {
-    const tintColorProp = fill ? {tintColor: fill} : {};
     const isReactComponent = typeof src === 'function';
 
     // Clear memory cache when unmounting images to avoid memory overload
@@ -60,8 +59,10 @@ function ImageSVG({src, width = '100%', height = '100%', fill, contentFit = 'cov
             source={src}
             recyclingKey={getImageRecyclingKey(src)}
             style={[{width, height}, style as ExpoImageProps['style']]}
-            // eslint-disable-next-line react/jsx-props-no-spreading
-            {...tintColorProp}
+            tintColor={fill}
+            // On android, there's an issue where the fill color of the icon does not change,
+            // unless the component is remounted. (https://github.com/Expensify/App/pull/76741#issuecomment-4245274687)
+            key={fill}
         />
     );
 }

From cfd172ddb9793500d2616128b6ef8f97aef4ab18 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 12:10:29 +0100
Subject: [PATCH 165/233] fix: `isCommentEmpty` not updated after editing
 stopped

---
 .../ComposerWithSuggestions.tsx                | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 15748f149313..b39af22b7f6e 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -177,6 +177,10 @@ type SwitchToCurrentReportProps = {
 };
 const {RNTextInputReset} = NativeModules;
 
+function getIsCommentEmpty(comment: string) {
+    return !!comment.match(/^(\s)*$/);
+}
+
 const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
 
 /**
@@ -361,6 +365,7 @@ function ComposerWithSuggestions({
             if (wasEditing.current && wasEditingInComposerRef.current) {
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
                 applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
+                setIsCommentEmpty(getIsCommentEmpty(draftComment ?? ''));
                 if (!wasComposerFocusedBeforeEditingRef.current) {
                     composerRef.current?.blur();
                 }
@@ -412,7 +417,7 @@ function ComposerWithSuggestions({
         }
 
         previousEditingReportActionIDRef.current = editingReportActionID;
-    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout, updateNativeSelectionValue]);
+    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, setIsCommentEmpty, shouldUseNarrowLayout, updateNativeSelectionValue]);
 
     const {superWideRHPRouteKeys} = useWideRHPState();
     // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
@@ -597,9 +602,6 @@ function ComposerWithSuggestions({
             const commentWithSpaceInserted = isEmojiInserted ? insertWhiteSpaceAtIndex(effectiveCommentValue, endIndex) : effectiveCommentValue;
             const {text: emojiConvertedText, emojis, cursorPosition} = replaceAndExtractEmojis(commentWithSpaceInserted, preferredSkinTone, preferredLocale);
 
-            const newComment = insertTextVSBetweenDigitAndEmoji(emojiConvertedText);
-            const textVSOffset = getTextVSCursorOffset(emojiConvertedText, cursorPosition);
-
             if (emojis.length) {
                 const newEmojis = getAddedEmojis(emojis, emojisPresentBefore.current);
                 if (newEmojis.length) {
@@ -609,9 +611,13 @@ function ComposerWithSuggestions({
                     }
                 }
             }
+
+            const newComment = insertTextVSBetweenDigitAndEmoji(emojiConvertedText);
             const newCommentConverted = convertToLTRForComposer(newComment);
-            const isNewCommentEmpty = !!newCommentConverted.match(/^(\s)*$/);
-            const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/);
+
+            const textVSOffset = getTextVSCursorOffset(emojiConvertedText, cursorPosition);
+            const isNewCommentEmpty = getIsCommentEmpty(newCommentConverted);
+            const isPrevCommentEmpty = getIsCommentEmpty(commentRef.current);
 
             /** Only update isCommentEmpty state if it's different from previous one */
             if (!isEditingInComposer && isNewCommentEmpty !== isPrevCommentEmpty) {

From 0b74044ae7cee6723676692db36074b58c66ea58 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 21:37:58 +0100
Subject: [PATCH 166/233] refactor: simplify composer focus in
 `ReportActionCompose`

---
 .../ReportActionCompose/ReportActionCompose.tsx       | 11 ++---------
 1 file changed, 2 insertions(+), 9 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 191bee65d2f8..1e4b1cb1df18 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -343,13 +343,6 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
         return translate('reportActionCompose.writeSomething');
     }, [includesConcierge, translate, userBlockedFromConcierge, isExpenseRelatedReport, canUserPerformWriteAction, isEnglishLocale]);
 
-    const focus = () => {
-        if (composerRef.current === null) {
-            return;
-        }
-        composerRef.current?.focus(true);
-    };
-
     const isKeyboardVisibleWhenShowingModalRef = useRef(false);
     const isNextModalWillOpenRef = useRef(false);
 
@@ -784,7 +777,7 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
                                     if (!shouldFocusInputOnScreenFocus) {
                                         return;
                                     }
-                                    focus();
+                                    composerRef.current?.focus(true);
                                 }}
                                 actionButtonRef={actionButtonRef}
                                 shouldDisableAttachmentItem={isExceedingMaxLength}
@@ -851,7 +844,7 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
                                     if (activeElementId === CONST.COMPOSER.NATIVE_ID || activeElementId === CONST.EMOJI_PICKER_BUTTON_NATIVE_ID) {
                                         return;
                                     }
-                                    focus();
+                                    composerRef.current?.focus(true);
                                 }}
                                 onEmojiSelected={(...args) => composerRef.current?.replaceSelectionWithText(...args)}
                                 emojiPickerID={report?.reportID}

From 489139080be2ade67d082d2579cd51188a6e2da0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 21:58:52 +0100
Subject: [PATCH 167/233] fix: allow re-focussing input on Android

---
 src/libs/focusComposerWithDelay/index.ts | 73 ++++++++++++++++++++----
 src/libs/focusComposerWithDelay/types.ts |  2 +-
 2 files changed, 63 insertions(+), 12 deletions(-)

diff --git a/src/libs/focusComposerWithDelay/index.ts b/src/libs/focusComposerWithDelay/index.ts
index 1f18a4b9d4bc..98e034ae9f20 100644
--- a/src/libs/focusComposerWithDelay/index.ts
+++ b/src/libs/focusComposerWithDelay/index.ts
@@ -1,20 +1,54 @@
+import {InteractionManager} from 'react-native';
+import {KeyboardController} from 'react-native-keyboard-controller';
 import ComposerFocusManager from '@libs/ComposerFocusManager';
+import getPlatform from '@libs/getPlatform';
 import isWindowReadyToFocus from '@libs/isWindowReadyToFocus';
 import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
 import CONST from '@src/CONST';
 import setTextInputSelection from './setTextInputSelection';
-import type {FocusComposerWithDelay, InputType} from './types';
+import type {FocusComposerWithDelay, InputType, Selection} from './types';
+
+/**
+ * When the field already has focus, RN's `focus()` often does not show the IME again.
+ * `KeyboardController.setFocusTo('current')` re-applies focus via native (`requestFocusFromJS` on Android,
+ * `reloadInputViews` + `focus` on iOS) without blurring first.
+ *
+ * @see https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#setfocusto
+ */
+function requestKeyboardForFocusedComposer(textInput: InputType, forcedSelectionRange?: Selection) {
+    const platform = getPlatform();
+    const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID;
+
+    if (!isNative) {
+        return;
+    }
+
+    requestIdleCallback(() => {
+        KeyboardController.setFocusTo('current');
+        if (forcedSelectionRange) {
+            setTextInputSelection(textInput, forcedSelectionRange);
+        }
+    });
+}
 
 /**
  * Create a function that focuses the composer.
  */
 function focusComposerWithDelay(textInput: InputType | null, delay: number = CONST.COMPOSER_FOCUS_DELAY): FocusComposerWithDelay {
+    function getIsFocused() {
+        if (textInput && 'isFocused' in textInput) {
+            return textInput.isFocused();
+        }
+        return false;
+    }
+
     /**
      * Focus the text input
      * @param [shouldDelay] Impose delay before focusing the text input
      * @param [forcedSelectionRange] Force selection range of text input
+     * @param [forceKeyboardIfAlreadyFocused] Use KeyboardController so the soft keyboard can show without blur/refocus
      */
-    return (shouldDelay = false, forcedSelectionRange = undefined) => {
+    return async (shouldDelay = false, forcedSelectionRange = undefined, forceKeyboardIfAlreadyFocused = false) => {
         // There could be other animations running while we trigger manual focus.
         // This prevents focus from making those animations janky.
         if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) {
@@ -22,23 +56,40 @@ function focusComposerWithDelay(textInput: InputType | null, delay: number = CON
         }
 
         if (!shouldDelay) {
+            if (getIsFocused()) {
+                if (forceKeyboardIfAlreadyFocused) {
+                    requestKeyboardForFocusedComposer(textInput, forcedSelectionRange);
+                }
+                return;
+            }
+
             textInput.focus();
             if (forcedSelectionRange) {
                 setTextInputSelection(textInput, forcedSelectionRange);
             }
             return;
         }
-        Promise.all([ComposerFocusManager.isReadyToFocus(), isWindowReadyToFocus()]).then(() => {
-            if (!textInput) {
+
+        await Promise.all([ComposerFocusManager.isReadyToFocus(), isWindowReadyToFocus()]);
+
+        if (!textInput) {
+            return;
+        }
+        // When the closing modal has a focused text input focus() needs a delay to properly work.
+        // Setting 150ms here is a temporary workaround for the Android HybridApp. It should be reverted once we identify the real root cause of this issue: https://github.com/Expensify/App/issues/56311.
+        setTimeout(() => {
+            if (getIsFocused()) {
+                if (forceKeyboardIfAlreadyFocused) {
+                    // Selection is applied synchronously below; only request focus
+                    requestKeyboardForFocusedComposer(textInput);
+                }
                 return;
             }
-            // When the closing modal has a focused text input focus() needs a delay to properly work.
-            // Setting 150ms here is a temporary workaround for the Android HybridApp. It should be reverted once we identify the real root cause of this issue: https://github.com/Expensify/App/issues/56311.
-            setTimeout(() => textInput.focus(), delay);
-            if (forcedSelectionRange) {
-                setTextInputSelection(textInput, forcedSelectionRange);
-            }
-        });
+            textInput.focus();
+        }, delay);
+        if (forcedSelectionRange) {
+            setTextInputSelection(textInput, forcedSelectionRange);
+        }
     };
 }
 
diff --git a/src/libs/focusComposerWithDelay/types.ts b/src/libs/focusComposerWithDelay/types.ts
index 97a1298e8c7a..1447161e88f2 100644
--- a/src/libs/focusComposerWithDelay/types.ts
+++ b/src/libs/focusComposerWithDelay/types.ts
@@ -7,7 +7,7 @@ type Selection = {
     positionY?: number;
 };
 
-type FocusComposerWithDelay = (shouldDelay?: boolean, forcedSelectionRange?: Selection) => void;
+type FocusComposerWithDelay = (shouldDelay?: boolean, forcedSelectionRange?: Selection, forceKeyboardIfAlreadyFocused?: boolean) => Promise<void>;
 
 type InputType = TextInput | HTMLTextAreaElement;
 

From 64430aa42baf966b4482dd51cc0e592c164eea7c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 21:59:45 +0100
Subject: [PATCH 168/233] refactor: use `focusComposerWithDelay` in
 `refocusComposerAfterPreventFirstResponder`

---
 ...focusComposerAfterPreventFirstResponder.ts | 22 ++++++++++++-------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/libs/refocusComposerAfterPreventFirstResponder.ts b/src/libs/refocusComposerAfterPreventFirstResponder.ts
index 6ac3fca6dcbc..94002b48e73d 100644
--- a/src/libs/refocusComposerAfterPreventFirstResponder.ts
+++ b/src/libs/refocusComposerAfterPreventFirstResponder.ts
@@ -1,15 +1,21 @@
-import isWindowReadyToFocus from './isWindowReadyToFocus';
+import type {ComposerRef} from '@components/Composer/types';
+import focusComposerWithDelay from './focusComposerWithDelay';
 import type {ComposerType} from './ReportActionComposeFocusManager';
 import ReportActionComposeFocusManager from './ReportActionComposeFocusManager';
 
 function refocusComposerAfterPreventFirstResponder(composerToRefocusOnClose: ComposerType | undefined) {
-    return isWindowReadyToFocus().then(() => {
-        if (composerToRefocusOnClose === 'main') {
-            ReportActionComposeFocusManager.composerRef.current?.focus();
-        } else if (composerToRefocusOnClose === 'edit') {
-            ReportActionComposeFocusManager.editComposerRef.current?.focus();
-        }
-    });
+    let composerRef: ComposerRef | null = null;
+    if (composerToRefocusOnClose === 'main') {
+        composerRef = ReportActionComposeFocusManager.composerRef.current;
+    } else if (composerToRefocusOnClose === 'edit') {
+        composerRef = ReportActionComposeFocusManager.editComposerRef.current;
+    }
+
+    return focusComposerWithDelay(composerRef)(true);
+
+    // return isWindowReadyToFocus().then(() => {
+    //     composerRef?.focus();
+    // });
 }
 
 export default refocusComposerAfterPreventFirstResponder;

From 17b0a317bcc58aa767be8bca6892a2af8d2914e0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 22:00:11 +0100
Subject: [PATCH 169/233] fix: composer not focussing in edit mode

---
 .../ComposerWithSuggestions.tsx               | 41 ++++++++++---------
 1 file changed, 22 insertions(+), 19 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index b39af22b7f6e..b1cca62dc4e7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -38,6 +38,7 @@ import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSp
 import convertToLTRForComposer from '@libs/convertToLTRForComposer';
 import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getTextVSCursorOffset, insertTextVSBetweenDigitAndEmoji, replaceAndExtractEmojis} from '@libs/EmojiUtils';
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
+import type {Selection} from '@libs/focusComposerWithDelay/types';
 import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
 import getPlatform from '@libs/getPlatform';
 import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener';
@@ -77,7 +78,7 @@ type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string};
 
 type ComposerWithSuggestionsRef = ComposerRef & {
     /** Focus the composer */
-    focus: (shouldDelay?: boolean) => void;
+    focus: (shouldDelay?: boolean, forcedSelectionRange?: Selection, forceKeyboardIfAlreadyFocused?: boolean) => void;
 
     /** Replace the selection with text */
     replaceSelectionWithText: OnEmojiSelected;
@@ -298,6 +299,24 @@ function ComposerWithSuggestions({
 
     const commentRef = useRef(value);
 
+    const {superWideRHPRouteKeys} = useWideRHPState();
+    // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
+    const shouldDelayAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
+    const shouldDelayAutoFocusRef = useRef(shouldDelayAutoFocus);
+    shouldDelayAutoFocusRef.current = shouldDelayAutoFocus;
+
+    /**
+     * Focus the composer text input
+     * @param [shouldDelay=false] Impose delay before focusing the composer
+     * @param [forcedSelectionRange] Optional selection to apply after focus
+     * @param [forceKeyboardIfAlreadyFocused] When already focused, use KeyboardController so the keyboard can show (e.g. edit-in-composer)
+     */
+    const focus = useCallback((shouldDelay = false, forcedSelectionRange?: Selection, forceKeyboardIfAlreadyFocused = false) => {
+        // If we're stacked above another RHP, wait for the transition to complete before focusing.
+        const delay = shouldDelayAutoFocusRef.current ? CONST.ANIMATED_TRANSITION : CONST.COMPOSER_FOCUS_DELAY;
+        focusComposerWithDelay(composerRef.current, delay)(shouldDelay, forcedSelectionRange, forceKeyboardIfAlreadyFocused).catch(() => {});
+    }, []);
+
     const updateNativeSelectionValue = useCallback((start: number, end: number) => {
         if (!isIOSNative) {
             return;
@@ -343,10 +362,10 @@ function ComposerWithSuggestions({
             updateNativeSelectionValue(selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
 
             if (options?.isEditingInComposer) {
-                composerRef.current?.focus();
+                focus(true, undefined, true);
             }
         },
-        [currentEditMessageSelection, updateNativeSelectionValue],
+        [currentEditMessageSelection, focus, updateNativeSelectionValue],
     );
 
     const wasEditing = useRef(isEditing);
@@ -419,12 +438,6 @@ function ComposerWithSuggestions({
         previousEditingReportActionIDRef.current = editingReportActionID;
     }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, setIsCommentEmpty, shouldUseNarrowLayout, updateNativeSelectionValue]);
 
-    const {superWideRHPRouteKeys} = useWideRHPState();
-    // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
-    const shouldDelayAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
-    const shouldDelayAutoFocusRef = useRef(shouldDelayAutoFocus);
-    shouldDelayAutoFocusRef.current = shouldDelayAutoFocus;
-
     const [modal] = useOnyx(ONYXKEYS.MODAL);
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
     const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED);
@@ -845,16 +858,6 @@ function ComposerWithSuggestions({
         return suggestionsRef.current.setShouldBlockSuggestionCalc(false);
     }, [suggestionsRef]);
 
-    /**
-     * Focus the composer text input
-     * @param [shouldDelay=false] Impose delay before focusing the composer
-     */
-    const focus = useCallback((shouldDelay = false) => {
-        // If we're stacked above another RHP, wait for the transition to complete before focusing.
-        const delay = shouldDelayAutoFocusRef.current ? CONST.ANIMATED_TRANSITION : CONST.COMPOSER_FOCUS_DELAY;
-        focusComposerWithDelay(composerRef.current, delay)(shouldDelay);
-    }, []);
-
     /**
      * In the stacked-RHP SearchReport case we disable the TextInput's immediate `autoFocus` to avoid jank.
      * Make sure we still trigger a (delayed) manual focus on first render for that route.

From ac41780ead1e5119fe3a39133b24d4eb1cbaa212 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 23:50:48 +0100
Subject: [PATCH 170/233] fix: remove unused import

---
 src/libs/focusComposerWithDelay/index.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/libs/focusComposerWithDelay/index.ts b/src/libs/focusComposerWithDelay/index.ts
index 98e034ae9f20..eddf2ad9f3aa 100644
--- a/src/libs/focusComposerWithDelay/index.ts
+++ b/src/libs/focusComposerWithDelay/index.ts
@@ -1,4 +1,3 @@
-import {InteractionManager} from 'react-native';
 import {KeyboardController} from 'react-native-keyboard-controller';
 import ComposerFocusManager from '@libs/ComposerFocusManager';
 import getPlatform from '@libs/getPlatform';

From 9a285ed95b084df1f097d1afaab40f96d9e20c0d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 17 Apr 2026 23:59:22 +0100
Subject: [PATCH 171/233] fix: prevent composer blur when edit mode is disabled

---
 .../ComposerWithSuggestions.tsx               |  3 +-
 .../ReportActionCompose.tsx                   |  1 -
 .../ReportActionCompose/useEditMessage.ts     | 34 ++-----------------
 .../report/ReportActionItemMessageEdit.tsx    |  1 -
 tests/unit/hooks/useEditMessage.test.ts       |  1 -
 5 files changed, 4 insertions(+), 36 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index b1cca62dc4e7..803b4967bd43 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -385,6 +385,7 @@ function ComposerWithSuggestions({
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
                 applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
                 setIsCommentEmpty(getIsCommentEmpty(draftComment ?? ''));
+
                 if (!wasComposerFocusedBeforeEditingRef.current) {
                     composerRef.current?.blur();
                 }
@@ -436,7 +437,7 @@ function ComposerWithSuggestions({
         }
 
         previousEditingReportActionIDRef.current = editingReportActionID;
-    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, selection, setIsCommentEmpty, shouldUseNarrowLayout, updateNativeSelectionValue]);
+    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, focus, selection, setIsCommentEmpty, shouldUseNarrowLayout, updateNativeSelectionValue]);
 
     const [modal] = useOnyx(ONYXKEYS.MODAL);
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 1e4b1cb1df18..662ca515f02a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -413,7 +413,6 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) {
         originalReportID,
         reportAction: editingReportAction,
         shouldScrollToLastMessage: isEditingLastReportAction,
-        isFocused,
         debouncedCommentMaxLengthValidation,
         composerRef,
     });
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 7e4627753169..7a9c6ff89632 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -1,32 +1,26 @@
 // eslint-disable-next-line lodash/import-scope
 import type {DebouncedFuncLeading} from 'lodash';
 import type React from 'react';
-import {useEffect, useRef} from 'react';
-import {InteractionManager} from 'react-native';
 import type {ComposerRef} from '@components/Composer/types';
 import useAncestors from '@hooks/useAncestors';
 import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
 import useOnyx from '@hooks/useOnyx';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useReportScrollManager from '@hooks/useReportScrollManager';
-import {isActive as isEmojiPickerActive} from '@libs/actions/EmojiPickerAction';
 import {clearReportActionDrafts, editReportComment} from '@libs/actions/Report';
 import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
-import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import {getOriginalReportID} from '@libs/ReportUtils';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
 import {useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext';
 import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
-import KeyboardUtils from '@src/utils/keyboard';
 
 type UseEditMessageProps = {
     reportID: string | undefined;
     originalReportID: string | undefined;
     reportAction: OnyxTypes.ReportAction | null | undefined;
     shouldScrollToLastMessage?: boolean;
-    isFocused: boolean;
     debouncedCommentMaxLengthValidation: DebouncedFuncLeading<(value: string) => boolean>;
     composerRef: React.RefObject<ComposerRef | null>;
 };
@@ -34,9 +28,8 @@ type UseEditMessageProps = {
 /**
  * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
  */
-function useEditMessage({reportID, originalReportID, reportAction, shouldScrollToLastMessage = false, isFocused, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) {
+function useEditMessage({reportID, originalReportID, reportAction, shouldScrollToLastMessage = false, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) {
     const reportScrollManager = useReportScrollManager();
-    const isFocusedRef = useRef(isFocused);
 
     const {email} = useCurrentUserPersonalDetails();
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`);
@@ -49,20 +42,6 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
 
     const {stopEditing, submitEdit} = useReportActionActiveEditActions();
 
-    useEffect(() => {
-        // required for keeping last state of isFocused variable
-        isFocusedRef.current = isFocused;
-    }, [isFocused]);
-
-    // We consider the report action active if it's focused, its emoji picker is open or its context menu is open
-    function isActive(): boolean {
-        if (!reportAction) {
-            return false;
-        }
-
-        return isFocusedRef.current || isEmojiPickerActive(reportAction.reportActionID) || ReportActionContextMenu.isActiveReportAction(reportAction.reportActionID);
-    }
-
     function deleteDraft(): void {
         if (!reportAction) {
             return;
@@ -72,18 +51,9 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
 
         clearReportActionDrafts();
 
-        if (isActive()) {
-            ReportActionComposeFocusManager.clear(true);
-            // Wait for report action compose re-mounting on mWeb
-            // eslint-disable-next-line @typescript-eslint/no-deprecated
-            InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus());
-        }
-
         // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
         if (shouldScrollToLastMessage) {
-            KeyboardUtils.dismiss().then(() => {
-                reportScrollManager.scrollToIndex(0, false);
-            });
+            reportScrollManager.scrollToIndex(0, false);
         }
     }
 
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 3722246f4ff8..bd0203d77f0b 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -253,7 +253,6 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
         originalReportID,
         reportAction: action,
         shouldScrollToLastMessage: index === 0,
-        isFocused,
         debouncedCommentMaxLengthValidation,
         composerRef,
     });
diff --git a/tests/unit/hooks/useEditMessage.test.ts b/tests/unit/hooks/useEditMessage.test.ts
index 32f3f7c36154..8b5cc09bf678 100644
--- a/tests/unit/hooks/useEditMessage.test.ts
+++ b/tests/unit/hooks/useEditMessage.test.ts
@@ -103,7 +103,6 @@ describe('useEditMessage', () => {
             reportID: report.reportID,
             originalReportID: report.reportID,
             reportAction,
-            isFocused: true,
             debouncedCommentMaxLengthValidation: makeDebouncedValidator({flushResult: true}),
             composerRef: {current: {blur: jest.fn()} as never},
             ...overrides,

From bc3093dedf9c22082387dc1bc3f2f19d0c989fb8 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Sat, 18 Apr 2026 00:21:57 +0100
Subject: [PATCH 172/233] refactor: extract composer toggle logic into separate
 hook

---
 .../ComposerWithSuggestions.tsx               | 154 ++++--------------
 .../ReportActionComposeUtils.ts               |  31 ++++
 .../useEditComposerToggle.ts                  | 144 ++++++++++++++++
 3 files changed, 203 insertions(+), 126 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 803b4967bd43..195c3a1541e9 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -66,8 +66,10 @@ import findNodeHandle from '@src/utils/findNodeHandle';
 import getCursorPosition from './getCursorPosition';
 import getScrollPosition from './getScrollPosition';
 import type {SuggestionsRef} from './ReportActionCompose';
+import ReportActionComposeUtils from './ReportActionComposeUtils';
 import SilentCommentUpdater from './SilentCommentUpdater';
 import Suggestions from './Suggestions';
+import useEditComposerToggle from './useEditComposerToggle';
 
 type SyncSelection = {
     position: number;
@@ -178,10 +180,6 @@ type SwitchToCurrentReportProps = {
 };
 const {RNTextInputReset} = NativeModules;
 
-function getIsCommentEmpty(comment: string) {
-    return !!comment.match(/^(\s)*$/);
-}
-
 const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
 
 /**
@@ -317,127 +315,30 @@ function ComposerWithSuggestions({
         focusComposerWithDelay(composerRef.current, delay)(shouldDelay, forcedSelectionRange, forceKeyboardIfAlreadyFocused).catch(() => {});
     }, []);
 
-    const updateNativeSelectionValue = useCallback((start: number, end: number) => {
-        if (!isIOSNative) {
-            return;
-        }
-
-        // Ensure that native selection value is set imperatively after all state changes are effective
-        // eslint-disable-next-line @typescript-eslint/no-deprecated
-        InteractionManager.runAfterInteractions(() => {
-            // note: this implementation is only available on non-web RN, thus the wrapping
-            // 'if' block contains a redundant (since the ref is only used on iOS) platform check
-            composerRef.current?.setSelection(start, end);
-        });
-    }, []);
-
-    type ApplyComposerValueOptions = {
-        isEditingInComposer?: boolean;
-        shouldMoveSelectionToEnd?: boolean;
-        selection?: TextSelection | null;
-        shouldForceNativeValueUpdate?: boolean;
-    };
-
-    const applyComposerValue = useCallback(
-        (nextValue: string, options?: ApplyComposerValueOptions) => {
-            const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
-            const shouldUseEditingSelection = options?.isEditingInComposer ?? false;
-            const shouldForceSelectionToEnd = options?.shouldMoveSelectionToEnd ?? false;
-            const explicitSelection = options?.selection ?? null;
-
-            const selectionToApply = explicitSelection ?? (shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection);
-
-            commentRef.current = nextValue;
-            emojisPresentBefore.current = extractEmojis(nextValue);
-
-            setValue(nextValue);
-            // We need to manually update the native text prop,
-            // in order to force a re-calculation of the composer height and layout,
-            // when the composer changes in or out of edit mode.
-            if (isIOSNative && options?.shouldForceNativeValueUpdate) {
-                composerRef.current?.setNativeProps({text: nextValue});
-            }
-
-            setSelection(selectionToApply);
-            updateNativeSelectionValue(selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
-
-            if (options?.isEditingInComposer) {
-                focus(true, undefined, true);
-            }
-        },
-        [currentEditMessageSelection, focus, updateNativeSelectionValue],
-    );
-
-    const wasEditing = useRef(isEditing);
-    const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
-    const wasComposerFocusedBeforeEditingRef = useRef(false);
-    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
-    const previousEditingReportActionIDRef = useRef<string | null>(null);
-
-    useEffect(() => {
-        // If the draft message is already being submitted, do nothing.
-        if (editingState === 'submitted') {
-            return;
-        }
-
-        if (editingState !== 'editing') {
-            if (wasEditing.current && wasEditingInComposerRef.current) {
-                // Editing just ended in the composer – restore the draft comment and its previous selection.
-                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
-                setIsCommentEmpty(getIsCommentEmpty(draftComment ?? ''));
-
-                if (!wasComposerFocusedBeforeEditingRef.current) {
-                    composerRef.current?.blur();
-                }
-            }
-
-            wasEditing.current = false;
-            wasEditingInComposerRef.current = shouldUseNarrowLayout;
-            previousDraftSelectionRef.current = null;
-            return;
-        }
+    const updateIsCommentEmptyOnEditEnd = useCallback(() => {
+        setIsCommentEmpty(ReportActionComposeUtils.getIsCommentEmpty(draftComment ?? ''));
+    }, [draftComment, setIsCommentEmpty]);
 
-        // Editing just started.
-        if (!wasEditing.current) {
-            wasComposerFocusedBeforeEditingRef.current = composerRef.current?.isFocused() ?? false;
-            // Store the draft selection before switching into edit mode so we can restore it later.
-            previousDraftSelectionRef.current = selection;
+    const handleEditFocus = useCallback(() => {
+        focus(true, undefined, true);
+    }, [focus]);
 
-            wasEditing.current = true;
-            wasEditingInComposerRef.current = shouldUseNarrowLayout;
+    const handleEditValueChange = useCallback((nextValue: string) => {
+        commentRef.current = nextValue;
+        emojisPresentBefore.current = extractEmojis(nextValue);
 
-            if (!shouldUseNarrowLayout) {
-                // Wide layout – another editor handles the edit, keep composer draft as-is.
-                return;
-            }
-            // In narrow layout we always show the message being edited.
-            // When starting to edit in the composer, always place the cursor at the end of the message.
-            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldMoveSelectionToEnd: true, shouldForceNativeValueUpdate: true});
-            return;
-        }
-
-        // Editing is ongoing and layout toggled from wide to narrow.
-        if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
-            wasEditingInComposerRef.current = true;
-            // We just moved from wide to narrow while editing – start editing in the composer.
-            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
-            return;
-        }
-
-        // Editing is ongoing and layout toggled from narrow to wide.
-        if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
-            wasEditingInComposerRef.current = false;
-            applyComposerValue(draftComment ?? '');
-            return;
-        }
-
-        // The editing report action and message changed
-        if (shouldUseNarrowLayout && editingReportActionID !== previousEditingReportActionIDRef.current) {
-            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldForceNativeValueUpdate: true});
-        }
+        setValue(nextValue);
+    }, []);
 
-        previousEditingReportActionIDRef.current = editingReportActionID;
-    }, [applyComposerValue, draftComment, editingMessage, editingReportActionID, editingState, focus, selection, setIsCommentEmpty, shouldUseNarrowLayout, updateNativeSelectionValue]);
+    const {wasEditingRef} = useEditComposerToggle({
+        selection,
+        draftComment,
+        composerRef,
+        onEditEnd: updateIsCommentEmptyOnEditEnd,
+        onFocus: handleEditFocus,
+        onValueChange: handleEditValueChange,
+        onSelectionChange: setSelection,
+    });
 
     const [modal] = useOnyx(ONYXKEYS.MODAL);
     const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
@@ -630,8 +531,8 @@ function ComposerWithSuggestions({
             const newCommentConverted = convertToLTRForComposer(newComment);
 
             const textVSOffset = getTextVSCursorOffset(emojiConvertedText, cursorPosition);
-            const isNewCommentEmpty = getIsCommentEmpty(newCommentConverted);
-            const isPrevCommentEmpty = getIsCommentEmpty(commentRef.current);
+            const isNewCommentEmpty = ReportActionComposeUtils.getIsCommentEmpty(newCommentConverted);
+            const isPrevCommentEmpty = ReportActionComposeUtils.getIsCommentEmpty(commentRef.current);
 
             /** Only update isCommentEmpty state if it's different from previous one */
             if (!isEditingInComposer && isNewCommentEmpty !== isPrevCommentEmpty) {
@@ -648,7 +549,7 @@ function ComposerWithSuggestions({
                     syncSelectionWithOnChangeTextRef.current = {position, value: newComment};
                 }
 
-                if (!wasEditing.current) {
+                if (!wasEditingRef.current) {
                     setSelection((prevSelection) => ({
                         ...prevSelection,
                         start: position,
@@ -701,6 +602,7 @@ function ComposerWithSuggestions({
             setIsCommentEmpty,
             shouldUseNarrowLayout,
             suggestionsRef,
+            wasEditingRef,
         ],
     );
 
@@ -818,9 +720,9 @@ function ComposerWithSuggestions({
 
             const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position;
             syncSelectionWithOnChangeTextRef.current = null;
-            updateNativeSelectionValue(positionSnapshot, positionSnapshot);
+            ReportActionComposeUtils.updateNativeSelectionValue(composerRef, positionSnapshot, positionSnapshot);
         },
-        [clearComposerHeight, updateComment, updateNativeSelectionValue],
+        [clearComposerHeight, updateComment],
     );
 
     const onSelectionChange = useCallback(
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
new file mode 100644
index 000000000000..ee9eac06c26a
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
@@ -0,0 +1,31 @@
+import {InteractionManager} from 'react-native';
+import type {ComposerRef} from '@components/Composer/types';
+import getPlatform from '@libs/getPlatform';
+import CONST from '@src/CONST';
+
+const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
+
+function getIsCommentEmpty(comment: string) {
+    return !!comment.match(/^(\s)*$/);
+}
+
+const updateNativeSelectionValue = (composerRef: React.RefObject<ComposerRef | null>, start: number, end: number) => {
+    if (!isIOSNative) {
+        return;
+    }
+
+    // Ensure that native selection value is set imperatively after all state changes are effective
+    // eslint-disable-next-line @typescript-eslint/no-deprecated
+    InteractionManager.runAfterInteractions(() => {
+        // note: this implementation is only available on non-web RN, thus the wrapping
+        // 'if' block contains a redundant (since the ref is only used on iOS) platform check
+        composerRef.current?.setSelection(start, end);
+    });
+};
+
+const ReportActionComposeUtils = {
+    getIsCommentEmpty,
+    updateNativeSelectionValue,
+};
+
+export default ReportActionComposeUtils;
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
new file mode 100644
index 000000000000..a7414819d330
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -0,0 +1,144 @@
+/* eslint-disable no-param-reassign */
+import {useCallback, useEffect, useRef} from 'react';
+import type {RefObject} from 'react';
+import type {ComposerRef, TextSelection} from '@components/Composer/types';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import getPlatform from '@libs/getPlatform';
+import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
+import CONST from '@src/CONST';
+import ReportActionComposeUtils from './ReportActionComposeUtils';
+
+const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
+type UseEditComposerToggleProps = {
+    selection: TextSelection;
+    draftComment: string;
+    composerRef: RefObject<ComposerRef | null>;
+    onEditEnd?: () => void;
+    onSelectionChange?: (selection: TextSelection) => void;
+    onFocus?: () => void;
+    onValueChange?: (value: string) => void;
+};
+
+/**
+ * useEditComposerToggle is a hook that manages the editing state of the composer.
+ * It is used to toggle the editing state of the composer and to apply the changes to the composer.
+ * It is also used to restore the draft comment and the selection when the editing state is toggled off.
+ * It is also used to focus the composer when the editing state is toggled on.
+ * It is also used to update the value of the composer when the editing state is toggled on.
+ * It is also used to update the selection of the composer when the editing state is toggled on.
+ */
+function useEditComposerToggle({selection, draftComment, composerRef, onEditEnd, onFocus, onValueChange, onSelectionChange}: UseEditComposerToggleProps) {
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+    const {editingState, editingReportActionID, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
+    const isEditing = editingState !== 'off';
+
+    const wasEditingRef = useRef(isEditing);
+    const wasEditingInComposerRef = useRef(shouldUseNarrowLayout);
+    const wasComposerFocusedBeforeEditingRef = useRef(false);
+    const previousDraftSelectionRef = useRef<TextSelection | null>(null);
+    const previousEditingReportActionIDRef = useRef<string | null>(null);
+
+    type ApplyComposerValueOptions = {
+        isEditingInComposer?: boolean;
+        shouldMoveSelectionToEnd?: boolean;
+        selection?: TextSelection | null;
+        shouldForceNativeValueUpdate?: boolean;
+    };
+
+    const applyComposerValue = useCallback(
+        (nextValue: string, options?: ApplyComposerValueOptions) => {
+            const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
+            const shouldUseEditingSelection = options?.isEditingInComposer ?? false;
+            const shouldForceSelectionToEnd = options?.shouldMoveSelectionToEnd ?? false;
+            const explicitSelection = options?.selection ?? null;
+
+            const selectionToApply = explicitSelection ?? (shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection);
+
+            onValueChange?.(nextValue);
+            // We need to manually update the native text prop,
+            // in order to force a re-calculation of the composer height and layout,
+            // when the composer changes in or out of edit mode.
+            if (isIOSNative && options?.shouldForceNativeValueUpdate) {
+                composerRef.current?.setNativeProps({text: nextValue});
+            }
+
+            onSelectionChange?.(selectionToApply);
+            ReportActionComposeUtils.updateNativeSelectionValue(composerRef, selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
+
+            if (options?.isEditingInComposer) {
+                onFocus?.();
+            }
+        },
+        [composerRef, currentEditMessageSelection, onFocus, onSelectionChange, onValueChange],
+    );
+
+    useEffect(() => {
+        // If the draft message is already being submitted, do nothing.
+        if (editingState === 'submitted') {
+            return;
+        }
+
+        if (editingState !== 'editing') {
+            if (wasEditingRef.current && wasEditingInComposerRef.current) {
+                // Editing just ended in the composer – restore the draft comment and its previous selection.
+                applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
+                onEditEnd?.();
+
+                if (!wasComposerFocusedBeforeEditingRef.current) {
+                    composerRef.current?.blur();
+                }
+            }
+
+            wasEditingRef.current = false;
+            wasEditingInComposerRef.current = shouldUseNarrowLayout;
+            previousDraftSelectionRef.current = null;
+            return;
+        }
+
+        // Editing just started.
+        if (!wasEditingRef.current) {
+            wasComposerFocusedBeforeEditingRef.current = composerRef.current?.isFocused() ?? false;
+            // Store the draft selection before switching into edit mode so we can restore it later.
+            previousDraftSelectionRef.current = selection;
+
+            wasEditingRef.current = true;
+            wasEditingInComposerRef.current = shouldUseNarrowLayout;
+
+            if (!shouldUseNarrowLayout) {
+                // Wide layout – another editor handles the edit, keep composer draft as-is.
+                return;
+            }
+            // In narrow layout we always show the message being edited.
+            // When starting to edit in the composer, always place the cursor at the end of the message.
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldMoveSelectionToEnd: true, shouldForceNativeValueUpdate: true});
+            return;
+        }
+
+        // Editing is ongoing and layout toggled from wide to narrow.
+        if (shouldUseNarrowLayout && !wasEditingInComposerRef.current) {
+            wasEditingInComposerRef.current = true;
+            // We just moved from wide to narrow while editing – start editing in the composer.
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true});
+            return;
+        }
+
+        // Editing is ongoing and layout toggled from narrow to wide.
+        if (!shouldUseNarrowLayout && wasEditingInComposerRef.current) {
+            wasEditingInComposerRef.current = false;
+            applyComposerValue(draftComment ?? '');
+            return;
+        }
+
+        // The editing report action and message changed
+        if (shouldUseNarrowLayout && editingReportActionID !== previousEditingReportActionIDRef.current) {
+            applyComposerValue(editingMessage ?? '', {isEditingInComposer: true, shouldForceNativeValueUpdate: true});
+        }
+
+        previousEditingReportActionIDRef.current = editingReportActionID;
+    }, [applyComposerValue, composerRef, draftComment, editingMessage, editingReportActionID, editingState, selection, onEditEnd, shouldUseNarrowLayout]);
+
+    return {wasEditingRef};
+}
+
+export default useEditComposerToggle;

From 70b73f3654ef07e7da94cb6c788f1cbcb4f088d7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Sat, 18 Apr 2026 00:22:50 +0100
Subject: [PATCH 173/233] docs: update JSDoc of `useEditComposerToggle`

---
 .../report/ReportActionCompose/useEditComposerToggle.ts   | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index a7414819d330..23a6419e97c6 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -22,10 +22,10 @@ type UseEditComposerToggleProps = {
 /**
  * useEditComposerToggle is a hook that manages the editing state of the composer.
  * It is used to toggle the editing state of the composer and to apply the changes to the composer.
- * It is also used to restore the draft comment and the selection when the editing state is toggled off.
- * It is also used to focus the composer when the editing state is toggled on.
- * It is also used to update the value of the composer when the editing state is toggled on.
- * It is also used to update the selection of the composer when the editing state is toggled on.
+ * Additionally, it is used to restore the draft comment and the selection when the editing state is toggled off,
+ * to focus the composer when the editing state is toggled on,
+ * to update the value of the composer when the editing state is toggled on,
+ * and to update the selection of the composer when the editing state is toggled on.
  */
 function useEditComposerToggle({selection, draftComment, composerRef, onEditEnd, onFocus, onValueChange, onSelectionChange}: UseEditComposerToggleProps) {
     const {shouldUseNarrowLayout} = useResponsiveLayout();

From 32088ca3a0dc80cd0992731918705bce15e48e56 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 21:31:30 +0100
Subject: [PATCH 174/233] refactor: move `onValueChange` to `ComposerInput`

---
 .../report/ReportActionCompose/ComposerContext.ts |  2 --
 .../report/ReportActionCompose/ComposerInput.tsx  | 15 ++++++++++++---
 .../ReportActionCompose/ComposerProvider.tsx      |  9 ---------
 3 files changed, 12 insertions(+), 14 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
index c23262631b89..06c6af13996d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
@@ -67,7 +67,6 @@ type ComposerEditActions = {
 type ComposerSendActions = {
     validateAndSubmitDraft: (draftMessage: string) => void;
     submitDraftAndClearComposer: () => void;
-    onValueChange: (value: string) => void;
 };
 
 // Frozen — stable refs, set once
@@ -134,7 +133,6 @@ const ComposerEditActionsContext = createContext<ComposerEditActions>(defaultEdi
 const defaultSendActions: ComposerSendActions = {
     validateAndSubmitDraft: noop,
     submitDraftAndClearComposer: noop,
-    onValueChange: noop,
 };
 const ComposerSendActionsContext = createContext<ComposerSendActions>(defaultSendActions);
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx
index 98020ab69ab4..dbdb6646e5d3 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx
@@ -10,6 +10,7 @@ import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
 import useParentReportAction from '@hooks/useParentReportAction';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
+import {setIsComposerFullSize} from '@libs/actions/Report';
 import FS from '@libs/Fullstory';
 import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils';
 import {getCombinedReportActions, getFilteredReportActionsForReportView, getOneTransactionThreadReportID, isMoneyRequestAction, isSentMoneyReportAction} from '@libs/ReportActionsUtils';
@@ -46,15 +47,23 @@ function ComposerInput({reportID, onPasteFile}: ComposerInputProps) {
     const {translate, preferredLocale} = useLocalize();
     const {isOffline} = useNetwork();
     const {isMenuVisible} = useComposerState();
-    const {isBlockedFromConcierge} = useComposerSendState();
-    const {setIsFullComposerAvailable, onBlur, onFocus, setComposerRef} = useComposerActions();
-    const {submitDraftAndClearComposer, onValueChange, validateAndSubmitDraft} = useComposerSendActions();
+    const {isBlockedFromConcierge, debouncedCommentMaxLengthValidation} = useComposerSendState();
+    const {setIsFullComposerAvailable, onBlur, onFocus, setComposerRef, setText} = useComposerActions();
+    const {submitDraftAndClearComposer, validateAndSubmitDraft} = useComposerSendActions();
     const {containerRef, suggestionsRef, isNextModalWillOpenRef} = useComposerMeta();
 
     const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`);
     const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
     const userBlockedFromConcierge = isBlockedFromConciergeUserAction(blockedFromConcierge);
 
+    const onValueChange = (v: string) => {
+        setText(v);
+        if (v.length === 0 && isComposerFullSize) {
+            setIsComposerFullSize(reportID, false);
+        }
+        debouncedCommentMaxLengthValidation(v);
+    };
+
     const measureContainer = (callback: MeasureInWindowOnSuccessCallback) => {
         containerRef.current?.measureInWindow(callback);
     };
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index 51610687eb03..336cc39785b9 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -314,14 +314,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         });
     };
 
-    const onValueChange = (v: string) => {
-        setText(v);
-        if (v.length === 0 && isComposerFullSize) {
-            setIsComposerFullSize(reportID, false);
-        }
-        debouncedCommentMaxLengthValidation(v);
-    };
-
     const composerState = {
         isFocused,
         isMenuVisible,
@@ -366,7 +358,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const composerSendActions = {
         validateAndSubmitDraft,
         submitDraftAndClearComposer,
-        onValueChange,
     };
 
     const composerMeta = {

From 2aa7159a9a2917c09741969055eea40a3f2133e2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 21:38:15 +0100
Subject: [PATCH 175/233] fix: expand/collapse button components

---
 .../AttachmentPickerWithMenuItems.tsx         |  4 +-
 .../ComposerExpandCollapseButton.tsx          | 67 +++-------------
 .../ExpandCollapseButton.tsx                  | 77 +++++++++++++++++++
 3 files changed, 90 insertions(+), 58 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/ExpandCollapseButton.tsx

diff --git a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
index 892dd024ce52..b65ef3c7d34f 100644
--- a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
@@ -50,7 +50,7 @@ import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'
 import type {AnchorPosition} from '@src/styles';
 import type * as OnyxTypes from '@src/types/onyx';
 import type {FileObject} from '@src/types/utils/Attachment';
-import ExpandCollapseComposerButton from './ExpandCollapseComposerButton';
+import ExpandCollapseButton from './ExpandCollapseButton';
 
 type MoneyRequestOptions = Record<
     Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.CREATE | typeof CONST.IOU.TYPE.SPLIT_EXPENSE>,
@@ -476,7 +476,7 @@ function AttachmentPickerWithMenuItems({
                                         </PressableWithFeedback>
                                     </Tooltip>
                                 </View>
-                                <ExpandCollapseComposerButton
+                                <ExpandCollapseButton
                                     isFullComposerAvailable={isFullComposerAvailable}
                                     isComposerFullSize={isComposerFullSize}
                                     reportID={reportID}
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
index d5e531893646..c45140e1c043 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
@@ -1,76 +1,31 @@
 import React from 'react';
-import {View} from 'react-native';
-import Icon from '@components/Icon';
-import {PressableWithFeedback} from '@components/Pressable';
-import Tooltip from '@components/Tooltip';
 import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
-import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
 import {setIsComposerFullSize} from '@userActions/Report';
-import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import {useComposerSendState, useComposerState} from './ComposerContext';
+import ExpandCollapseButton from './ExpandCollapseButton';
 
-type ExpandCollapseComposerButtonProps = {
+type ComposerExpandCollapseButtonProps = {
     reportID: string;
 };
 
-function ComposerExpandCollapseButton({reportID}: ExpandCollapseComposerButtonProps) {
-    const {translate} = useLocalize();
-    const theme = useTheme();
-    const styles = useThemeStyles();
-    const icons = useMemoizedLazyExpensifyIcons(['Collapse', 'Expand'] as const);
-
+function ComposerExpandCollapseButton({reportID}: ComposerExpandCollapseButtonProps) {
     const {isBlockedFromConcierge} = useComposerSendState();
     const {raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
     const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`);
     const {isFullComposerAvailable} = useComposerState();
 
-    if (!isFullComposerAvailable && !isComposerFullSize) {
-        return null;
-    }
-
-    const shouldCollapse = isComposerFullSize;
-    const tooltipText = shouldCollapse ? translate('reportActionCompose.collapse') : translate('reportActionCompose.expand');
-    const nextComposerFullSizeValue = !shouldCollapse;
-    const iconSrc = shouldCollapse ? icons.Collapse : icons.Expand;
-    const sentryLabel = shouldCollapse ? CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_COLLAPSE_BUTTON : CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_EXPAND_BUTTON;
-
-    const expandCollapseComposerButtonStyles = [styles.flexGrow1, styles.flexShrink0];
-
     return (
-        <View style={expandCollapseComposerButtonStyles}>
-            <View>
-                <Tooltip
-                    text={tooltipText}
-                    key={shouldCollapse ? 'composer-collapse' : 'composer-expand'}
-                >
-                    <PressableWithFeedback
-                        onPress={(e) => {
-                            e?.preventDefault();
-                            raiseIsScrollLayoutTriggered();
-                            setIsComposerFullSize(reportID, nextComposerFullSizeValue);
-                        }}
-                        // Keep focus on the composer when Collapse/Expand button is clicked.
-                        onMouseDown={(e) => e.preventDefault()}
-                        style={styles.composerSizeButton}
-                        disabled={isBlockedFromConcierge}
-                        role={CONST.ROLE.BUTTON}
-                        accessibilityLabel={tooltipText}
-                        sentryLabel={sentryLabel}
-                    >
-                        <Icon
-                            fill={theme.icon}
-                            src={iconSrc}
-                        />
-                    </PressableWithFeedback>
-                </Tooltip>
-            </View>
-        </View>
+        <ExpandCollapseButton
+            isFullComposerAvailable={isFullComposerAvailable}
+            isComposerFullSize={isComposerFullSize}
+            reportID={reportID}
+            raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLayoutTriggered}
+            setIsComposerFullSize={setIsComposerFullSize}
+            disabled={isBlockedFromConcierge}
+        />
     );
 }
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ExpandCollapseButton.tsx b/src/pages/inbox/report/ReportActionCompose/ExpandCollapseButton.tsx
new file mode 100644
index 000000000000..a767a03e61bc
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/ExpandCollapseButton.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {ViewProps} from 'react-native';
+import Icon from '@components/Icon';
+import {PressableWithFeedback} from '@components/Pressable';
+import Tooltip from '@components/Tooltip';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type ExpandCollapseButtonProps = ViewProps & {
+    isFullComposerAvailable: boolean;
+    isComposerFullSize: boolean;
+    reportID: string;
+    raiseIsScrollLikelyLayoutTriggered: () => void;
+    setIsComposerFullSize: (reportID: string, isFullSize: boolean) => void;
+    disabled?: boolean;
+};
+
+function ExpandCollapseButton({
+    isFullComposerAvailable,
+    isComposerFullSize,
+    reportID,
+    disabled = false,
+    raiseIsScrollLikelyLayoutTriggered,
+    setIsComposerFullSize,
+    ...restProps
+}: ExpandCollapseButtonProps) {
+    const {translate} = useLocalize();
+    const theme = useTheme();
+    const styles = useThemeStyles();
+    const icons = useMemoizedLazyExpensifyIcons(['Collapse', 'Expand'] as const);
+
+    if (!isFullComposerAvailable && !isComposerFullSize) {
+        return null;
+    }
+
+    const shouldCollapse = isComposerFullSize;
+    const tooltipText = shouldCollapse ? translate('reportActionCompose.collapse') : translate('reportActionCompose.expand');
+    const nextComposerFullSizeValue = !shouldCollapse;
+    const iconSrc = shouldCollapse ? icons.Collapse : icons.Expand;
+    const sentryLabel = shouldCollapse ? CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_COLLAPSE_BUTTON : CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_EXPAND_BUTTON;
+
+    return (
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        <View {...restProps}>
+            <Tooltip
+                text={tooltipText}
+                key={shouldCollapse ? 'composer-collapse' : 'composer-expand'}
+            >
+                <PressableWithFeedback
+                    onPress={(e) => {
+                        e?.preventDefault();
+                        raiseIsScrollLikelyLayoutTriggered();
+                        setIsComposerFullSize(reportID, nextComposerFullSizeValue);
+                    }}
+                    // Keep focus on the composer when Collapse/Expand button is clicked.
+                    onMouseDown={(e) => e.preventDefault()}
+                    style={styles.composerSizeButton}
+                    disabled={disabled}
+                    role={CONST.ROLE.BUTTON}
+                    accessibilityLabel={tooltipText}
+                    sentryLabel={sentryLabel}
+                >
+                    <Icon
+                        fill={theme.icon}
+                        src={iconSrc}
+                    />
+                </PressableWithFeedback>
+            </Tooltip>
+        </View>
+    );
+}
+
+export default ExpandCollapseButton;

From 4de296f4002a42e0bca3f0e79c5a2e1879c88498 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 21:52:55 +0100
Subject: [PATCH 176/233] fix: missing import of React

---
 .../inbox/report/ReportActionCompose/ComposerEditingButtons.tsx  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
index c3d47b0eb493..99d3c7e7562a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
 import {View} from 'react-native';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {useComposerEditActions} from './ComposerContext';

From 6c9d19f05c8e46df79eb7ea88def047a42310658 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 22:04:13 +0100
Subject: [PATCH 177/233] fix: add back `KeyboardStateProvider` to
 `ReportScreenProviders` for tests

---
 tests/utils/ReportActionComposeUtils.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index e7b7ec2e8e25..d2f631464919 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -3,6 +3,7 @@ import type {PropsWithChildren} from 'react';
 import ComposeProviders from '@components/ComposeProviders';
 import {LocaleContextProvider} from '@components/LocaleContextProvider';
 import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
 import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
@@ -17,7 +18,7 @@ function ReportActionEditMessageContextProviderForReport({children}: PropsWithCh
 }
 
 function ReportScreenProviders({children}: PropsWithChildren) {
-    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
 }
 
 const defaultReportActionComposeProps: ReportActionComposeProps = {

From 5f08a7f889ea24e629ed8278b709443c3eae3118 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 22:05:34 +0100
Subject: [PATCH 178/233] fix: add missing React import

---
 tests/utils/ReportActionComposeUtils.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index d2f631464919..b3481350d638 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -1,5 +1,6 @@
 import {render} from '@testing-library/react-native';
 import type {PropsWithChildren} from 'react';
+import React from 'react';
 import ComposeProviders from '@components/ComposeProviders';
 import {LocaleContextProvider} from '@components/LocaleContextProvider';
 import OnyxListItemProvider from '@components/OnyxListItemProvider';

From e43a49362a6f9bdac1ab778b276b7ab9a065fc25 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 23:25:43 +0100
Subject: [PATCH 179/233] fix: use extracted message edit test components

---
 tests/ui/ReportActionItemMessageEditTest.tsx | 36 +++-----------------
 tests/utils/ReportActionComposeUtils.tsx     | 27 ++-------------
 2 files changed, 7 insertions(+), 56 deletions(-)

diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index 4290637c2d30..d385258cbb0b 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -1,19 +1,13 @@
 import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, render, screen} from '@testing-library/react-native';
-import React from 'react';
+import {act, fireEvent, screen} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
-import ComposeProviders from '@components/ComposeProviders';
-import {LocaleContextProvider} from '@components/LocaleContextProvider';
-import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {renderReportActionItemMessageEdit, reportActionComposeTestReportAction} from 'tests/utils/ReportActionComposeUtils';
 import {editReportComment} from '@libs/actions/Report';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
-import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
-import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
 import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Message} from '@src/types/onyx/ReportAction';
-import * as LHNTestUtils from '../utils/LHNTestUtils';
 import * as TestHelper from '../utils/TestHelper';
 
 const mockEditReportComment = jest.mocked(editReportComment);
@@ -62,28 +56,6 @@ jest.mock('@react-navigation/native', () => ({
 
 TestHelper.setupGlobalFetchMock();
 
-const defaultReport = LHNTestUtils.getFakeReport();
-const defaultProps: ReportActionItemMessageEditProps = {
-    action: LHNTestUtils.getFakeReportAction(),
-    reportID: defaultReport.reportID,
-    originalReportID: defaultReport.reportID,
-    index: 0,
-    isGroupPolicyReport: false,
-};
-
-const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
-    return render(
-        <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
-            <ReportActionItemMessageEdit
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-            />
-        </ComposeProviders>,
-    );
-};
-
 describe('ReportActionCompose Integration Tests', () => {
     beforeAll(() => {
         Onyx.init({
@@ -158,11 +130,11 @@ describe('ReportActionCompose Integration Tests', () => {
             const videoSource = 'https://example.com/video.mp4';
             const videoHtml = `<video src="${videoSource}" data-expensify-source="${videoSource}" data-name="video.mp4" data-expensify-height="100" data-expensify-width="200">video.mp4</video>`;
 
-            const messages = defaultProps.action.message as Message[];
+            const messages = reportActionComposeTestReportAction.message as Message[];
 
             renderReportActionItemMessageEdit({
                 action: {
-                    ...defaultProps.action,
+                    ...reportActionComposeTestReportAction,
                     message: [
                         {
                             ...messages.at(0),
diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
index b3481350d638..d7f69c529484 100644
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ b/tests/utils/ReportActionComposeUtils.tsx
@@ -13,6 +13,7 @@ import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMes
 import * as LHNTestUtils from './LHNTestUtils';
 
 const reportActionComposeTestReport = LHNTestUtils.getFakeReport();
+const reportActionComposeTestReportAction = LHNTestUtils.getFakeReportAction();
 
 function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
     return <ReportActionEditMessageContextProvider reportID={reportActionComposeTestReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
@@ -45,7 +46,7 @@ const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) =>
 };
 
 const defaultReportActionItemMessageEditProps: ReportActionItemMessageEditProps = {
-    action: LHNTestUtils.getFakeReportAction(),
+    action: reportActionComposeTestReportAction,
     reportID: reportActionComposeTestReport.reportID,
     originalReportID: reportActionComposeTestReport.reportID,
     index: 0,
@@ -65,26 +66,4 @@ const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessa
     );
 };
 
-const renderReportActionMessageEditComponents = (
-    reportActionComposeProps?: Partial<ReportActionComposeProps>,
-    reportActionItemMessageEditProps?: Partial<ReportActionItemMessageEditProps>,
-) => {
-    return render(
-        <ReportScreenProviders>
-            <ReportActionCompose
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultReportActionComposeProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...reportActionComposeProps}
-            />
-            <ReportActionItemMessageEdit
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultReportActionItemMessageEditProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...reportActionItemMessageEditProps}
-            />
-        </ReportScreenProviders>,
-    );
-};
-
-export {ReportActionComposeWrapper, renderReportActionCompose, renderReportActionItemMessageEdit, renderReportActionMessageEditComponents, reportActionComposeTestReport};
+export {ReportActionComposeWrapper, renderReportActionCompose, renderReportActionItemMessageEdit, reportActionComposeTestReport, reportActionComposeTestReportAction};

From 43311490e21392fd31ef97f04e5733babff68dc4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 20 Apr 2026 23:56:20 +0100
Subject: [PATCH 180/233] fix: tests

---
 tests/ui/ReportActionComposeTest.tsx         | 43 ++++++++++--
 tests/ui/ReportActionItemMessageEditTest.tsx | 62 ++++++++++++++++--
 tests/utils/ReportActionComposeUtils.tsx     | 69 --------------------
 3 files changed, 97 insertions(+), 77 deletions(-)
 delete mode 100644 tests/utils/ReportActionComposeUtils.tsx

diff --git a/tests/ui/ReportActionComposeTest.tsx b/tests/ui/ReportActionComposeTest.tsx
index 9f0ec04e570e..b4d1aa3cacd0 100644
--- a/tests/ui/ReportActionComposeTest.tsx
+++ b/tests/ui/ReportActionComposeTest.tsx
@@ -1,10 +1,18 @@
 import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, screen, waitFor} from '@testing-library/react-native';
+import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
+import React from 'react';
+import type {PropsWithChildren} from 'react';
 import Onyx from 'react-native-onyx';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
+import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
-import {renderReportActionCompose, reportActionComposeTestReport} from '../utils/ReportActionComposeUtils';
 import * as TestHelper from '../utils/TestHelper';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
 
@@ -46,6 +54,33 @@ jest.mock('@react-navigation/native', () => ({
 
 TestHelper.setupGlobalFetchMock();
 
+const defaultReport = LHNTestUtils.getFakeReport();
+const defaultProps: ReportActionComposeProps = {
+    reportID: defaultReport.reportID,
+};
+
+function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
+    return <ReportActionEditMessageContextProvider reportID={defaultReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
+}
+
+function ReportScreenProviders({children}: PropsWithChildren) {
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+}
+
+const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
+    // eslint-disable-next-line react/jsx-props-no-spreading
+    return render(
+        <ReportScreenProviders>
+            <ReportActionCompose
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...props}
+            />
+        </ReportScreenProviders>,
+    );
+};
+
 // Helper function to simulate text selection
 const simulateSelection = (composer: ReturnType<typeof screen.getByTestId>, start: number, end: number) => {
     fireEvent(composer, 'selectionChange', {
@@ -63,7 +98,7 @@ describe('ReportActionCompose Integration Tests', () => {
 
     beforeEach(async () => {
         await act(async () => {
-            await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportActionComposeTestReport.reportID}`, reportActionComposeTestReport);
+            await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
         });
     });
 
@@ -391,7 +426,7 @@ describe('ReportActionCompose Integration Tests', () => {
             await waitFor(
                 () => {
                     // And the task-title-specific error should be displayed
-                    expect(screen.getByText('composer.commentExceededMaxLength')).toBeOnTheScreen();
+                    expect(screen.getByText('composer.taskTitleExceededMaxLength')).toBeOnTheScreen();
                 },
                 {timeout: CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME + 500},
             );
diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index d385258cbb0b..5b7a9734f16c 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -1,13 +1,21 @@
 import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, screen} from '@testing-library/react-native';
+import {act, fireEvent, render, screen} from '@testing-library/react-native';
+import type {PropsWithChildren} from 'react';
 import Onyx from 'react-native-onyx';
-import {renderReportActionItemMessageEdit, reportActionComposeTestReportAction} from 'tests/utils/ReportActionComposeUtils';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
 import {editReportComment} from '@libs/actions/Report';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
+import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
+import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
+import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
 import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Message} from '@src/types/onyx/ReportAction';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
 import * as TestHelper from '../utils/TestHelper';
 
 const mockEditReportComment = jest.mocked(editReportComment);
@@ -56,6 +64,52 @@ jest.mock('@react-navigation/native', () => ({
 
 TestHelper.setupGlobalFetchMock();
 
+const defaultReport = LHNTestUtils.getFakeReport();
+const defaultReportAction = LHNTestUtils.getFakeReportAction();
+const defaultProps: ReportActionItemMessageEditProps = {
+    action: defaultReportAction,
+    reportID: defaultReport.reportID,
+    originalReportID: defaultReport.reportID,
+    index: 0,
+    isGroupPolicyReport: false,
+};
+
+function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
+    return <ReportActionEditMessageContextProvider reportID={defaultReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
+}
+
+function ReportScreenProviders({children}: PropsWithChildren) {
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+}
+
+const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
+    return render(
+        <ReportScreenProviders>
+            <ReportActionItemMessageEdit
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultProps}
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...props}
+            />
+        </ReportScreenProviders>,
+    );
+};
+
+// const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
+//     return render(
+//         <ReportScreenProviders>
+//             {/* <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}> */}
+//             <ReportActionItemMessageEdit
+//                 // eslint-disable-next-line react/jsx-props-no-spreading
+//                 {...defaultProps}
+//                 // eslint-disable-next-line react/jsx-props-no-spreading
+//                 {...props}
+//             />
+//             {/* </ComposeProviders>, */}
+//         </ReportScreenProviders>,
+//     );
+// };
+
 describe('ReportActionCompose Integration Tests', () => {
     beforeAll(() => {
         Onyx.init({
@@ -130,11 +184,11 @@ describe('ReportActionCompose Integration Tests', () => {
             const videoSource = 'https://example.com/video.mp4';
             const videoHtml = `<video src="${videoSource}" data-expensify-source="${videoSource}" data-name="video.mp4" data-expensify-height="100" data-expensify-width="200">video.mp4</video>`;
 
-            const messages = reportActionComposeTestReportAction.message as Message[];
+            const messages = defaultReportAction.message as Message[];
 
             renderReportActionItemMessageEdit({
                 action: {
-                    ...reportActionComposeTestReportAction,
+                    ...defaultReportAction,
                     message: [
                         {
                             ...messages.at(0),
diff --git a/tests/utils/ReportActionComposeUtils.tsx b/tests/utils/ReportActionComposeUtils.tsx
deleted file mode 100644
index d7f69c529484..000000000000
--- a/tests/utils/ReportActionComposeUtils.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import {render} from '@testing-library/react-native';
-import type {PropsWithChildren} from 'react';
-import React from 'react';
-import ComposeProviders from '@components/ComposeProviders';
-import {LocaleContextProvider} from '@components/LocaleContextProvider';
-import OnyxListItemProvider from '@components/OnyxListItemProvider';
-import {KeyboardStateProvider} from '@components/withKeyboardState';
-import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
-import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
-import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
-import * as LHNTestUtils from './LHNTestUtils';
-
-const reportActionComposeTestReport = LHNTestUtils.getFakeReport();
-const reportActionComposeTestReportAction = LHNTestUtils.getFakeReportAction();
-
-function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
-    return <ReportActionEditMessageContextProvider reportID={reportActionComposeTestReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
-}
-
-function ReportScreenProviders({children}: PropsWithChildren) {
-    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
-}
-
-const defaultReportActionComposeProps: ReportActionComposeProps = {
-    reportID: reportActionComposeTestReport.reportID,
-};
-
-function ReportActionComposeWrapper(props?: Partial<ReportActionComposeProps>) {
-    return (
-        <ReportScreenProviders>
-            <ReportActionCompose
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultReportActionComposeProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-            />
-        </ReportScreenProviders>
-    );
-}
-
-const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
-    // eslint-disable-next-line react/jsx-props-no-spreading
-    return render(<ReportActionComposeWrapper {...props} />);
-};
-
-const defaultReportActionItemMessageEditProps: ReportActionItemMessageEditProps = {
-    action: reportActionComposeTestReportAction,
-    reportID: reportActionComposeTestReport.reportID,
-    originalReportID: reportActionComposeTestReport.reportID,
-    index: 0,
-    isGroupPolicyReport: false,
-};
-
-const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
-    return render(
-        <ReportScreenProviders>
-            <ReportActionItemMessageEdit
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultReportActionItemMessageEditProps}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-            />
-        </ReportScreenProviders>,
-    );
-};
-
-export {ReportActionComposeWrapper, renderReportActionCompose, renderReportActionItemMessageEdit, reportActionComposeTestReport, reportActionComposeTestReportAction};

From 337cccc36ffc12c8fa9a981f91a27b82d941a7f7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 11:03:47 +0100
Subject: [PATCH 181/233] revert: reassure test changes

---
 tests/perf-test/ReportActionCompose.perf-test.tsx | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx
index bf981fb7b482..28306dda484c 100644
--- a/tests/perf-test/ReportActionCompose.perf-test.tsx
+++ b/tests/perf-test/ReportActionCompose.perf-test.tsx
@@ -3,13 +3,17 @@ import React from 'react';
 import Onyx from 'react-native-onyx';
 import type Animated from 'react-native-reanimated';
 import {measureRenders} from 'reassure';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
 import type {EmojiPickerRef} from '@libs/actions/EmojiPickerAction';
 import type Navigation from '@libs/Navigation/Navigation';
 import {setHasRadio} from '@libs/NetworkState';
+import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Report} from '@src/types/onyx';
-import {ReportActionComposeWrapper} from '../utils/ReportActionComposeUtils';
 import {translateLocal} from '../utils/TestHelper';
 import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
 
@@ -84,6 +88,14 @@ beforeEach(() => {
 
 const mockEvent = {preventDefault: jest.fn()};
 
+function ReportActionComposeWrapper() {
+    return (
+        <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider]}>
+            <ReportActionCompose reportID="1" />
+        </ComposeProviders>
+    );
+}
+
 test('[ReportActionCompose] should render Composer with text input interactions', async () => {
     const scenario = async () => {
         // Query for the composer

From 0f59b45cf51346c99fda9c2ab8ba7d9e83f7fab6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 17:58:09 +0100
Subject: [PATCH 182/233] fix: don't remove edit state when screen is not
 focused

---
 .../report/ReportActionEditMessageContext.tsx | 107 +++++++++---------
 1 file changed, 51 insertions(+), 56 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 4d8878637cf2..5f745ff1e458 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,3 @@
-import {useIsFocused} from '@react-navigation/native';
 import React, {createContext, useCallback, useContext, useState} from 'react';
 import type {Dispatch, SetStateAction} from 'react';
 import type {OnyxCollection} from 'react-native-onyx';
@@ -56,8 +55,6 @@ type ReportActionEditMessageContextProviderProps = {
 };
 
 function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
-    const isFocused = useIsFocused();
-
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
         canEvict: false,
@@ -119,70 +116,68 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
     let editingReportActionID: string | null = null;
     let editingReportAction: OnyxTypes.ReportAction | null = null;
 
-    if (isFocused) {
-        const ancestorWithDraft = [...ancestors]
-            .slice()
-            .reverse()
-            .find(({report: ancestorReport, reportAction}) => {
-                const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`];
-                const origID = getOriginalReportID(ancestorReport.reportID, reportAction, reportActionsForAncestor);
-                if (!origID) {
-                    return false;
-                }
-                const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`];
-                const ancestorDraft = ancestorDrafts?.[reportAction.reportActionID];
-
-                return ancestorDraft?.message !== undefined;
-            });
-
-        const updateMessage = (nextMessage: string | null) => {
-            if (nextMessage == null) {
-                return;
+    const ancestorWithDraft = [...ancestors]
+        .slice()
+        .reverse()
+        .find(({report: ancestorReport, reportAction}) => {
+            const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`];
+            const origID = getOriginalReportID(ancestorReport.reportID, reportAction, reportActionsForAncestor);
+            if (!origID) {
+                return false;
             }
+            const ancestorDrafts = reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`];
+            const ancestorDraft = ancestorDrafts?.[reportAction.reportActionID];
 
-            const didReportActionChange = prevEditingReportActionID !== editingReportActionID;
-            if (didReportActionChange) {
-                setEditingMessage(nextMessage);
-                setPrevEditingReportActionID(editingReportActionID);
-                const defaultSelection: TextSelection = {start: nextMessage.length, end: nextMessage.length};
-                setCurrentEditMessageSelectionState(defaultSelection);
-            }
-        };
+            return ancestorDraft?.message !== undefined;
+        });
 
-        if (ancestorWithDraft) {
-            const {report: ancestorReport, reportAction: ancestorReportAction} = ancestorWithDraft;
-            const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`];
-            const ancestorOrigReportID = getOriginalReportID(ancestorReport.reportID, ancestorReportAction, reportActionsForAncestor);
-            const ancestorDrafts = ancestorOrigReportID ? reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorOrigReportID}`] : undefined;
-            const ancestorReportActionDraft = ancestorDrafts?.[ancestorReportAction.reportActionID];
+    const updateMessage = (nextMessage: string | null) => {
+        if (nextMessage == null) {
+            return;
+        }
 
-            editingReportID = ancestorReport.reportID;
-            editingReportActionID = ancestorReportAction.reportActionID;
-            editingReportAction = ancestorReportAction;
+        const didReportActionChange = prevEditingReportActionID !== editingReportActionID;
+        if (didReportActionChange) {
+            setEditingMessage(nextMessage);
+            setPrevEditingReportActionID(editingReportActionID);
+            const defaultSelection: TextSelection = {start: nextMessage.length, end: nextMessage.length};
+            setCurrentEditMessageSelectionState(defaultSelection);
+        }
+    };
 
-            if (editingState === 'off') {
-                setEditingState('editing');
-            }
+    if (ancestorWithDraft) {
+        const {report: ancestorReport, reportAction: ancestorReportAction} = ancestorWithDraft;
+        const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`];
+        const ancestorOrigReportID = getOriginalReportID(ancestorReport.reportID, ancestorReportAction, reportActionsForAncestor);
+        const ancestorDrafts = ancestorOrigReportID ? reportActionDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${ancestorOrigReportID}`] : undefined;
+        const ancestorReportActionDraft = ancestorDrafts?.[ancestorReportAction.reportActionID];
 
-            const nextMessage = ancestorReportActionDraft?.message ?? null;
-            updateMessage(nextMessage);
-        } else if (reportDrafts) {
-            const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message !== undefined);
+        editingReportID = ancestorReport.reportID;
+        editingReportActionID = ancestorReportAction.reportActionID;
+        editingReportAction = ancestorReportAction;
 
-            if (reportDraftEntry) {
-                const [reportActionIDOfDraft, reportActionDraft] = reportDraftEntry;
+        if (editingState === 'off') {
+            setEditingState('editing');
+        }
 
-                editingReportID = reportID ?? null;
-                editingReportActionID = reportActionIDOfDraft;
-                editingReportAction = reportActions?.[reportActionIDOfDraft] ?? null;
+        const nextMessage = ancestorReportActionDraft?.message ?? null;
+        updateMessage(nextMessage);
+    } else if (reportDrafts) {
+        const reportDraftEntry = Object.entries(reportDrafts).find(([, draft]) => draft?.message !== undefined);
 
-                if (editingState === 'off') {
-                    setEditingState('editing');
-                }
+        if (reportDraftEntry) {
+            const [reportActionIDOfDraft, reportActionDraft] = reportDraftEntry;
 
-                const nextMessage = reportActionDraft?.message ?? null;
-                updateMessage(nextMessage);
+            editingReportID = reportID ?? null;
+            editingReportActionID = reportActionIDOfDraft;
+            editingReportAction = reportActions?.[reportActionIDOfDraft] ?? null;
+
+            if (editingState === 'off') {
+                setEditingState('editing');
             }
+
+            const nextMessage = reportActionDraft?.message ?? null;
+            updateMessage(nextMessage);
         }
     }
 

From 05a45afe6a4a2903b5032caa9c1eb7c2e4aaa8b4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 18:00:56 +0100
Subject: [PATCH 183/233] fix: clear report action drafts on mount and unmount

---
 src/pages/inbox/ReportScreen.tsx | 19 +++++++------------
 1 file changed, 7 insertions(+), 12 deletions(-)

diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index e6b6d55a13fc..a7e012c95d4a 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -1,6 +1,5 @@
 import {PortalHost} from '@gorhom/portal';
-import {useFocusEffect} from '@react-navigation/native';
-import React, {useCallback} from 'react';
+import React, {useEffect} from 'react';
 import type {ViewStyle} from 'react-native';
 import {View} from 'react-native';
 import ScreenWrapper from '@components/ScreenWrapper';
@@ -51,17 +50,13 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
     const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
 
     // When the report screen is navigated away from, clear all report action edit drafts
-    useFocusEffect(
-        useCallback(() => {
-            if (!reportIDFromRoute) {
-                return;
-            }
+    useEffect(() => {
+        clearReportActionDrafts();
 
-            return () => {
-                clearReportActionDrafts();
-            };
-        }, [reportIDFromRoute]),
-    );
+        return () => {
+            clearReportActionDrafts();
+        };
+    }, []);
 
     useSubmitToDestinationVisible(
         [CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT, CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY],

From 26338f0f970d1dc27ac724533e2bcffbe936dca6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 18:23:08 +0100
Subject: [PATCH 184/233] fix: context menu opening in edit mode

---
 src/pages/inbox/report/PureReportActionItem.tsx | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 9565dc81a718..f39736c6f403 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -633,6 +633,11 @@ function PureReportActionItem({
 
     const disabledActions = useMemo(() => (!canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []), [report]);
 
+    const hasErrors = !isEmptyValueObject(action.errors);
+    const isContextMenuDisabled = useMemo(() => {
+        return draftMessage !== undefined || hasErrors || !shouldDisplayContextMenuValue;
+    }, [draftMessage, hasErrors, shouldDisplayContextMenuValue]);
+
     /**
      * Show the ReportActionContextMenu modal popover.
      *
@@ -641,7 +646,7 @@ function PureReportActionItem({
     const showPopover = useCallback(
         (event: GestureResponderEvent | MouseEvent) => {
             // Block menu on the message being Edited or if the report action item has errors
-            if (draftMessage !== undefined || !isEmptyValueObject(action.errors) || !shouldDisplayContextMenuValue) {
+            if (isContextMenuDisabled) {
                 return;
             }
 
@@ -674,17 +679,15 @@ function PureReportActionItem({
             });
         },
         [
-            draftMessage,
-            action.errors,
             action.reportActionID,
             reportID,
             toggleContextMenuFromActiveReportAction,
             originalReportID,
-            shouldDisplayContextMenuValue,
             disabledActions,
             isArchivedRoom,
             isChronosReport,
             handleShowContextMenu,
+            isContextMenuDisabled,
             isThreadReportParentAction,
         ],
     );
@@ -1354,7 +1357,7 @@ function PureReportActionItem({
                 onPressIn={() => shouldUseNarrowLayout && canUseTouchScreen() && ControlSelection.block()}
                 onPressOut={() => ControlSelection.unblock()}
                 onSecondaryInteraction={showPopover}
-                preventDefaultContextMenu={draftMessage === undefined && !hasErrors}
+                preventDefaultContextMenu={!isContextMenuDisabled}
                 withoutFocusOnSecondaryInteraction
                 accessibilityLabel={accessibilityLabel}
                 accessibilityHint={translate('accessibilityHints.chatMessage')}

From 4c01dfff5d060342cddb90433466c9b860b4cd71 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 18:23:50 +0100
Subject: [PATCH 185/233] refactor: move draftMessage derivation to
 `PureReportActionItem`

---
 .../SearchList/ListItem/ChatListItem.tsx      |  1 -
 .../DuplicateTransactionItem.tsx              |  6 ----
 .../BaseReportActionContextMenu.tsx           |  6 ----
 .../report/ContextMenu/ContextMenuActions.tsx |  3 +-
 .../PopoverReportActionContextMenu.tsx        |  7 +---
 .../ContextMenu/ReportActionContextMenu.ts    |  2 --
 .../inbox/report/PureReportActionItem.tsx     | 34 ++++++++-----------
 src/pages/inbox/report/ReportActionItem.tsx   |  8 -----
 .../report/ReportActionItemParentAction.tsx   |  6 ----
 .../report/ReportActionsListItemRenderer.tsx  |  5 ---
 10 files changed, 16 insertions(+), 62 deletions(-)

diff --git a/src/components/Search/SearchList/ListItem/ChatListItem.tsx b/src/components/Search/SearchList/ListItem/ChatListItem.tsx
index e3c17257038f..74f67ea26463 100644
--- a/src/components/Search/SearchList/ListItem/ChatListItem.tsx
+++ b/src/components/Search/SearchList/ListItem/ChatListItem.tsx
@@ -82,7 +82,6 @@ function ChatListItem<TItem extends ListItem>({
                 index={item.index ?? 0}
                 isFirstVisibleReportAction={false}
                 shouldDisplayContextMenu={false}
-                shouldShowDraftMessage={false}
                 shouldShowBorder
                 userWalletTierName={userWalletTierName}
                 isUserValidated={isUserValidated}
diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
index 37660344d0c3..d21d7dab3892 100644
--- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
+++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
@@ -8,7 +8,6 @@ import useOnyx from '@hooks/useOnyx';
 import useThemeStyles from '@hooks/useThemeStyles';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
 import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import ReportActionItem from '@pages/inbox/report/ReportActionItem';
 import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext';
 import CONST from '@src/CONST';
@@ -53,10 +52,6 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
     const stateValue = useMemo(() => ({shouldOpenReportInRHP: true}), []);
     const actionsValue = useMemo(() => ({onPreviewPressed}), [onPreviewPressed]);
 
-    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
-
-    const draftMessage = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
-
     if (!action || !report) {
         return null;
     }
@@ -77,7 +72,6 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
                         userWalletTierName={userWalletTierName}
                         isUserValidated={isUserValidated}
                         personalDetails={personalDetails}
-                        draftMessage={draftMessage}
                         emojiReactions={emojiReactions}
                         linkedTransactionRouteError={linkedTransactionRouteError}
                         userBillingFundID={userBillingFundID}
diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
index 969ff12cdb6b..8219548e27d2 100755
--- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -71,9 +71,6 @@ type BaseReportActionContextMenuProps = {
     /** The copy selection. */
     selection?: string;
 
-    /** Draft message - if this is set the comment is in 'edit' mode */
-    draftMessage?: string;
-
     /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices  */
     type?: ContextMenuType;
 
@@ -123,7 +120,6 @@ function BaseReportActionContextMenu({
     isUnreadChat = false,
     isThreadReportParentAction = false,
     selection = '',
-    draftMessage = '',
     reportActionID,
     reportID,
     originalReportID,
@@ -349,7 +345,6 @@ function BaseReportActionContextMenu({
             },
             reportAction: {
                 reportActionID: reportAction?.reportActionID,
-                draftMessage,
                 isThreadReportParentAction,
             },
             callbacks: {
@@ -384,7 +379,6 @@ function BaseReportActionContextMenu({
                             reportID,
                             originalReportID,
                             report,
-                            draftMessage,
                             selection,
                             close: () => setShouldKeepOpen(false),
                             transitionActionSheetState,
diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 280223f27075..7209a58b96c4 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -274,7 +274,6 @@ type ContextMenuActionPayload = {
     currentUserAccountID: number;
     report: OnyxEntry<ReportType>;
     policy?: OnyxEntry<Policy>;
-    draftMessage: string;
     selection: string;
     close: () => void;
     transitionActionSheetState: (params: {type: string; payload?: Record<string, unknown>}) => void;
@@ -311,7 +310,7 @@ type ContextMenuActionPayload = {
     conciergeReportID: string | undefined;
 };
 
-type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void;
+type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string) => void;
 
 type RenderContent = (closePopover: boolean, payload: ContextMenuActionPayload) => React.ReactElement;
 
diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx
index c918824154a3..f123270cac55 100644
--- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -54,7 +54,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
     const reportActionIDRef = useRef<string | undefined>(undefined);
     const originalReportIDRef = useRef<string | undefined>(undefined);
     const selectionRef = useRef('');
-    const reportActionDraftMessageRef = useRef<string | undefined>(undefined);
     const isReportArchived = useReportIsArchived(reportIDRef.current);
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDRef.current}`);
     const reportActionsRef = useRef(reportActions);
@@ -174,7 +173,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
      * @param reportID - Active Report Id
      * @param reportActionID - ReportAction for ContextMenu
      * @param originalReportID - The current Report Id of the reportAction
-     * @param draftMessage - ReportAction draft message
      * @param [onShow] - Run a callback when Menu is shown
      * @param [onHide] - Run a callback when Menu is hidden
      * @param isArchivedRoom - Whether the provided report is an archived room
@@ -203,7 +201,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
         }
 
         const {reportID, originalReportID, isArchivedRoom = false, isChronos = false, isPinnedChat = false, isUnreadChat = false} = currentReport;
-        const {reportActionID, draftMessage, isThreadReportParentAction: isThreadReportParentActionParam = false} = reportAction;
+        const {reportActionID, isThreadReportParentAction: isThreadReportParentActionParam = false} = reportAction;
         const {onShow = () => {}, onHide = () => {}, setIsEmojiPickerActive = () => {}} = callbacks;
         setIsContextMenuOpening(true);
         setIsWithoutOverlay(withoutOverlay);
@@ -249,7 +247,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
             originalReportIDRef.current = originalReportID || undefined;
             selectionRef.current = selection;
             setIsPopoverVisible(true);
-            reportActionDraftMessageRef.current = draftMessage;
             setIsRoomArchived(isArchivedRoom);
             setIsChronosReportEnabled(isChronos);
             setIsChatPinned(isPinnedChat);
@@ -303,7 +300,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
         }
 
         selectionRef.current = '';
-        reportActionDraftMessageRef.current = undefined;
         setIsPopoverVisible(false);
 
         transitionActionSheetState({
@@ -482,7 +478,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
                     type={typeRef.current}
                     reportID={reportIDRef.current}
                     reportActionID={reportActionIDRef.current}
-                    draftMessage={reportActionDraftMessageRef.current}
                     selection={selectionRef.current}
                     isArchivedRoom={isRoomArchived}
                     isChronosReport={isChronosReportEnabled}
diff --git a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts
index 2da4ac2bbffc..28135387132b 100644
--- a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts
+++ b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts
@@ -32,7 +32,6 @@ type ShowContextMenuParams = {
     };
     reportAction?: {
         reportActionID?: string;
-        draftMessage?: string;
         isThreadReportParentAction?: boolean;
     };
     callbacks?: {
@@ -118,7 +117,6 @@ function hideContextMenu(shouldDelay?: boolean, onHideCallback = () => {}, param
  * @param reportID - Active Report Id
  * @param reportActionID - ReportActionID for ContextMenu
  * @param originalReportID - The current Report Id of the reportAction
- * @param draftMessage - ReportAction draft message
  * @param [onShow=() => {}] - Run a callback when Menu is shown
  * @param [onHide=() => {}] - Run a callback when Menu is hidden
  * @param isArchivedRoom - Whether the provided report is an archived room
diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index f39736c6f403..556a000d2a05 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -158,6 +158,7 @@ import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMe
 import type {ContextMenuAnchor} from './ContextMenu/ReportActionContextMenu';
 import {hideContextMenu, hideDeleteModal, isActiveReportAction, showContextMenu} from './ContextMenu/ReportActionContextMenu';
 import LinkPreviewer from './LinkPreviewer';
+import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import ReportActionItemBasicMessage from './ReportActionItemBasicMessage';
 import ReportActionItemContentCreated from './ReportActionItemContentCreated';
 import ReportActionItemDraft from './ReportActionItemDraft';
@@ -227,9 +228,6 @@ type PureReportActionItemProps = {
     /** Whether context menu should be displayed */
     shouldDisplayContextMenu?: boolean;
 
-    /** ReportAction draft message */
-    draftMessage?: string;
-
     /** The IOU/Expense report we are paying */
     iouReport?: OnyxTypes.Report;
 
@@ -377,7 +375,6 @@ function PureReportActionItem({
     shouldUseThreadDividerLine = false,
     shouldDisplayContextMenu = true,
     parentReportActionForTransactionThread,
-    draftMessage,
     iouReport,
     taskReport,
     linkedReport,
@@ -436,10 +433,14 @@ function PureReportActionItem({
     const composerTextInputRef = useRef<ComposerRef | null>(null);
     const popoverAnchorRef = useRef<Exclude<ContextMenuAnchor, TextInput>>(null);
     const downloadedPreviews = useRef<string[]>([]);
-    const prevDraftMessage = usePrevious(draftMessage);
     const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID;
     const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked);
     const isReportArchived = useReportIsArchived(reportID);
+
+    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
+    const draftMessage = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
+
+    const prevDraftMessage = usePrevious(draftMessage);
     const isEditingInline = !shouldUseNarrowLayout && draftMessage !== undefined;
 
     const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID);
@@ -666,7 +667,6 @@ function PureReportActionItem({
                     },
                     reportAction: {
                         reportActionID: action.reportActionID,
-                        draftMessage,
                         isThreadReportParentAction,
                     },
                     callbacks: {
@@ -680,15 +680,15 @@ function PureReportActionItem({
         },
         [
             action.reportActionID,
-            reportID,
-            toggleContextMenuFromActiveReportAction,
-            originalReportID,
             disabledActions,
+            handleShowContextMenu,
             isArchivedRoom,
             isChronosReport,
-            handleShowContextMenu,
             isContextMenuDisabled,
             isThreadReportParentAction,
+            originalReportID,
+            reportID,
+            toggleContextMenuFromActiveReportAction,
         ],
     );
 
@@ -725,10 +725,9 @@ function PureReportActionItem({
      * Get the content of ReportActionItem
      * @param hovered whether the ReportActionItem is hovered
      * @param isWhisper whether the report action is a whisper
-     * @param hasErrors whether the report action has any errors
      * @returns child component(s)
      */
-    const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => {
+    const renderItemContent = (hovered = false, isWhisper = false): React.JSX.Element => {
         let children;
         const moneyRequestOriginalMessage = isMoneyRequestAction(action) ? getOriginalMessage(action) : undefined;
         const moneyRequestActionType = moneyRequestOriginalMessage?.type;
@@ -1204,12 +1203,11 @@ function PureReportActionItem({
      * Get ReportActionItem with a proper wrapper
      * @param hovered whether the ReportActionItem is hovered
      * @param isWhisper whether the ReportActionItem is a whisper
-     * @param hasErrors whether the report action has any errors
      * @returns report action item
      */
 
-    const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => {
-        const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors);
+    const renderReportActionItem = (hovered: boolean, isWhisper: boolean): React.JSX.Element => {
+        const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper);
 
         if (isEmptyHTML(content) || (!shouldRenderViewBasedOnAction && !isClosedExpenseReportWithNoExpenses)) {
             return emptyHTML;
@@ -1293,8 +1291,6 @@ function PureReportActionItem({
     if (isWhisperActionTargetedToOthers(action)) {
         return null;
     }
-
-    const hasErrors = !isEmptyValueObject(action.errors);
     const whisperedTo = getWhisperedTo(action);
     const isMultipleParticipant = whisperedTo.length > 1;
 
@@ -1389,7 +1385,6 @@ function PureReportActionItem({
                                     disabledActions={disabledActions}
                                     isVisible={hovered && draftMessage === undefined && !hasErrors}
                                     isThreadReportParentAction={isThreadReportParentAction}
-                                    draftMessage={draftMessage}
                                     isChronosReport={isChronosReport}
                                     checkIfContextMenuActive={toggleContextMenuFromActiveReportAction}
                                     setIsEmojiPickerActive={setIsEmojiPickerActive}
@@ -1439,7 +1434,7 @@ function PureReportActionItem({
                                                     />
                                                 </View>
                                             )}
-                                            {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)}
+                                            {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper)}
                                         </>,
                                     )}
                                 </OfflineWithFeedback>
@@ -1488,7 +1483,6 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => {
         deepEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) &&
         deepEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) &&
         deepEqual(prevParentReportAction, nextParentReportAction) &&
-        prevProps.draftMessage === nextProps.draftMessage &&
         prevProps.iouReport?.reportID === nextProps.iouReport?.reportID &&
         deepEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
         deepEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) &&
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index 396169cbb4af..86270ff68dbd 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -25,12 +25,6 @@ import type {PureReportActionItemProps} from './PureReportActionItem';
 import PureReportActionItem from './PureReportActionItem';
 
 type ReportActionItemProps = Omit<PureReportActionItemProps, 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'betas'> & {
-    /** Whether to show the draft message or not */
-    shouldShowDraftMessage?: boolean;
-
-    /** Draft message for the report action */
-    draftMessage?: string;
-
     /** Emoji reactions for the report action */
     emojiReactions?: OnyxEntry<ReportActionReactions>;
 
@@ -53,7 +47,6 @@ type ReportActionItemProps = Omit<PureReportActionItemProps, 'taskReport' | 'lin
 function ReportActionItem({
     action,
     report,
-    draftMessage,
     emojiReactions,
     userWalletTierName,
     isUserValidated,
@@ -112,7 +105,6 @@ function ReportActionItem({
             report={report}
             policy={policy}
             currentUserAccountID={currentUserAccountID}
-            draftMessage={draftMessage}
             iouReport={iouReport}
             taskReport={taskReport}
             cardList={cardList}
diff --git a/src/pages/inbox/report/ReportActionItemParentAction.tsx b/src/pages/inbox/report/ReportActionItemParentAction.tsx
index e65601cc6a63..1b5981d67eed 100644
--- a/src/pages/inbox/report/ReportActionItemParentAction.tsx
+++ b/src/pages/inbox/report/ReportActionItemParentAction.tsx
@@ -23,7 +23,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type {PersonalDetailsList, Report, ReportAction, ReportActionReactions, ReportNameValuePairs, Transaction} from '@src/types/onyx';
 import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
 import RepliesDivider from './RepliesDivider';
-import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import ReportActionItem from './ReportActionItem';
 import ThreadDivider from './ThreadDivider';
 
@@ -132,8 +131,6 @@ function ReportActionItemParentAction({
         [ancestors],
     );
 
-    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
-
     const ancestorReactionSelector = useCallback(
         (allReactions: OnyxCollection<ReportActionReactions>) => {
             if (!allReactions) {
@@ -172,8 +169,6 @@ function ReportActionItemParentAction({
                     const shouldDisplayThreadDivider = !isTripPreview(ancestorReportAction);
                     const isAncestorReportArchived = isArchivedReport(ancestorsReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${ancestorReport.reportID}`]);
 
-                    const draftMessage = editingReportAction?.reportActionID === ancestorReportAction.reportActionID ? (editingMessage ?? undefined) : undefined;
-
                     const actionEmojiReactions = ancestorReactions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${ancestorReportAction.reportActionID}`];
 
                     return (
@@ -212,7 +207,6 @@ function ReportActionItemParentAction({
                                 userWalletTierName={userWalletTierName}
                                 isUserValidated={isUserValidated}
                                 personalDetails={personalDetails}
-                                draftMessage={draftMessage}
                                 emojiReactions={actionEmojiReactions}
                                 linkedTransactionRouteError={linkedTransactionRouteError}
                                 userBillingFundID={userBillingFundID}
diff --git a/src/pages/inbox/report/ReportActionsListItemRenderer.tsx b/src/pages/inbox/report/ReportActionsListItemRenderer.tsx
index e4af3490dfe3..c2374c2b4faf 100644
--- a/src/pages/inbox/report/ReportActionsListItemRenderer.tsx
+++ b/src/pages/inbox/report/ReportActionsListItemRenderer.tsx
@@ -6,7 +6,6 @@ import {isChatThread} from '@libs/ReportUtils';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx';
-import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import ReportActionItem from './ReportActionItem';
 import ReportActionItemParentAction from './ReportActionItemParentAction';
 
@@ -101,9 +100,6 @@ function ReportActionsListItemRenderer({
     reportNameValuePairsOrigin,
     reportNameValuePairsOriginalID,
 }: ReportActionsListItemRendererProps) {
-    const {editingReportActionID, editingMessage} = useReportActionActiveEdit();
-    const draftMessage = !!editingReportActionID && editingReportActionID === reportAction.reportActionID ? (editingMessage ?? undefined) : undefined;
-
     const originalMessage = useMemo(() => getOriginalMessage(reportAction), [reportAction]);
 
     const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportAction.reportActionID}`);
@@ -222,7 +218,6 @@ function ReportActionsListItemRenderer({
             userWalletTierName={userWalletTierName}
             isUserValidated={isUserValidated}
             personalDetails={personalDetails}
-            draftMessage={draftMessage}
             emojiReactions={emojiReactions}
             userBillingFundID={userBillingFundID}
             isTryNewDotNVPDismissed={isTryNewDotNVPDismissed}

From 7e1db3793879925f3798d14aec38f05eeadd54c6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 20:17:53 +0100
Subject: [PATCH 186/233] fix: cursor not updating when emoji is picked

---
 .../ComposerWithSuggestions.tsx                | 18 ++++++++----------
 .../useEditComposerToggle.ts                   |  2 --
 2 files changed, 8 insertions(+), 12 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 69e66b2dd96f..8c1eb7694e83 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -319,7 +319,7 @@ function ComposerWithSuggestions({
         setValue(nextValue);
     }, []);
 
-    const {wasEditingRef} = useEditComposerToggle({
+    useEditComposerToggle({
         selection,
         draftComment,
         composerRef,
@@ -530,14 +530,13 @@ function ComposerWithSuggestions({
                     syncSelectionWithOnChangeTextRef.current = {position, value: newComment};
                 }
 
-                if (!wasEditingRef.current) {
-                    setSelection((prevSelection) => ({
-                        ...prevSelection,
-                        start: position,
-                        end: position,
-                    }));
-                    setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: position, end: position}));
-                }
+                // Keep selection in sync after emoji conversion / insertion while editing (e.g. emoji picker on web);
+                setSelection((prevSelection) => ({
+                    ...prevSelection,
+                    start: position,
+                    end: position,
+                }));
+                setCurrentEditMessageSelection((prevSelection) => ({...prevSelection, start: position, end: position}));
             }
 
             commentRef.current = newCommentConverted;
@@ -581,7 +580,6 @@ function ComposerWithSuggestions({
             setCurrentEditMessageSelection,
             setEditingMessage,
             shouldUseNarrowLayout,
-            wasEditingRef,
         ],
     );
 
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index 9ca961dc772f..ffcad92cdfe5 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -135,8 +135,6 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
 
         previousEditingReportActionIDRef.current = editingReportActionID;
     }, [applyComposerValue, composerRef, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout]);
-
-    return {wasEditingRef};
 }
 
 export default useEditComposerToggle;

From ec639efc3ed863740fe6c6e669f33471504e4219 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 20:36:41 +0100
Subject: [PATCH 187/233] refactor: remove commented out code

---
 .../refocusComposerAfterPreventFirstResponder.ts  |  4 ----
 .../inbox/report/ReportActionItemMessageEdit.tsx  |  2 --
 tests/ui/ReportActionItemMessageEditTest.tsx      | 15 ---------------
 3 files changed, 21 deletions(-)

diff --git a/src/libs/refocusComposerAfterPreventFirstResponder.ts b/src/libs/refocusComposerAfterPreventFirstResponder.ts
index 94002b48e73d..e9fe1bdb1eaa 100644
--- a/src/libs/refocusComposerAfterPreventFirstResponder.ts
+++ b/src/libs/refocusComposerAfterPreventFirstResponder.ts
@@ -12,10 +12,6 @@ function refocusComposerAfterPreventFirstResponder(composerToRefocusOnClose: Com
     }
 
     return focusComposerWithDelay(composerRef)(true);
-
-    // return isWindowReadyToFocus().then(() => {
-    //     composerRef?.focus();
-    // });
 }
 
 export default refocusComposerAfterPreventFirstResponder;
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index bd0203d77f0b..975927a3fb42 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -446,8 +446,6 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
                                 if (isMobileChrome() && reportScrollManager.ref?.current) {
                                     reportScrollManager.ref.current.scrollToIndex({index, animated: false});
                                 }
-                                // The last composer that had focus should re-gain focus
-                                // setUpComposeFocusManager();
 
                                 // Clear active report action when another action gets focused
                                 if (!isEmojiPickerActive(action.reportActionID)) {
diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx
index 5b7a9734f16c..bca8a6d81e7a 100644
--- a/tests/ui/ReportActionItemMessageEditTest.tsx
+++ b/tests/ui/ReportActionItemMessageEditTest.tsx
@@ -95,21 +95,6 @@ const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessa
     );
 };
 
-// const renderReportActionItemMessageEdit = (props?: Partial<ReportActionItemMessageEditProps>) => {
-//     return render(
-//         <ReportScreenProviders>
-//             {/* <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}> */}
-//             <ReportActionItemMessageEdit
-//                 // eslint-disable-next-line react/jsx-props-no-spreading
-//                 {...defaultProps}
-//                 // eslint-disable-next-line react/jsx-props-no-spreading
-//                 {...props}
-//             />
-//             {/* </ComposeProviders>, */}
-//         </ReportScreenProviders>,
-//     );
-// };
-
 describe('ReportActionCompose Integration Tests', () => {
     beforeAll(() => {
         Onyx.init({

From 88fa1590f0b3e1b1b30ddc5536e4a8f01555ca5f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 23:14:49 +0100
Subject: [PATCH 188/233] refactor: Composer web implementation improvements

---
 .../Composer/implementation/index.tsx         | 40 +++++++++++++------
 1 file changed, 27 insertions(+), 13 deletions(-)

diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 239b1756d15b..4533d0200760 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -96,9 +96,16 @@ function Composer({
     /**
      *  Adds the cursor position to the selection change event.
      */
-    const addCursorPositionToSelectionChange = (event: TextInputSelectionChangeEvent) => {
-        const sel = window.getSelection();
-        if (shouldCalculateCaretPosition && isRendered && sel) {
+    const addCursorPositionToSelectionChange = useCallback(
+        (event: TextInputSelectionChangeEvent) => {
+            const sel = window.getSelection();
+            const canCalculateCaretPosition = shouldCalculateCaretPosition && isRendered && sel;
+            if (!canCalculateCaretPosition) {
+                onSelectionChange(event);
+                setSelection(event.nativeEvent.selection);
+                return;
+            }
+
             const range = sel.getRangeAt(0).cloneRange();
             range.collapse(true);
             const rect = range.getClientRects()[0] || range.startContainer.parentElement?.getClientRects()[0];
@@ -126,11 +133,12 @@ function Composer({
                 },
             });
             setSelection(selectionValue);
-        } else {
+
             onSelectionChange(event);
             setSelection(event.nativeEvent.selection);
-        }
-    };
+        },
+        [isRendered, onSelectionChange, shouldCalculateCaretPosition],
+    );
 
     /**
      * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file,
@@ -213,16 +221,20 @@ function Composer({
         if (!textInputRef.current) {
             return;
         }
+
+        const inputRef = textInputRef.current;
+
         const debouncedSetPrevScroll = lodashDebounce(() => {
-            if (!textInputRef.current) {
+            if (!inputRef) {
                 return;
             }
-            setPrevScroll(textInputRef.current.scrollTop);
+            setPrevScroll(inputRef.scrollTop);
         }, 100);
 
-        textInputRef.current.addEventListener('scroll', debouncedSetPrevScroll);
+        inputRef.addEventListener('scroll', debouncedSetPrevScroll);
+
         return () => {
-            textInputRef.current?.removeEventListener('scroll', debouncedSetPrevScroll);
+            inputRef?.removeEventListener('scroll', debouncedSetPrevScroll);
         };
     }, []);
 
@@ -235,6 +247,8 @@ function Composer({
     }, []);
 
     useEffect(() => {
+        const inputRef = textInputRef.current;
+
         const handleWheel = (e: MouseEvent) => {
             if (isReportFlatListScrolling.current) {
                 e.preventDefault();
@@ -243,15 +257,15 @@ function Composer({
 
             // When the composer has no scrollable content, the stopPropagation will prevent the inverted wheel event handler on the Chat body
             // which defaults to the browser wheel behavior. This causes the chat body to scroll in the opposite direction creating jerky behavior.
-            if (textInputRef.current && textInputRef.current.scrollHeight <= textInputRef.current.clientHeight) {
+            if (inputRef && inputRef.scrollHeight <= inputRef.clientHeight) {
                 return;
             }
             e.stopPropagation();
         };
-        textInputRef.current?.addEventListener('wheel', handleWheel, {passive: false});
+        inputRef?.addEventListener('wheel', handleWheel, {passive: false});
 
         return () => {
-            textInputRef.current?.removeEventListener('wheel', handleWheel);
+            inputRef?.removeEventListener('wheel', handleWheel);
         };
     }, []);
 

From 33929df885ea200d035490a195ce8e45c5b6961f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 23:17:13 +0100
Subject: [PATCH 189/233] refactor: `ComposerProvider` improvements

---
 .../ReportActionCompose/ComposerProvider.tsx  | 30 ++++++++-----------
 1 file changed, 12 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index 0ac8a8894b81..c094b6045428 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -42,38 +42,33 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`);
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
     const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`);
-
     const [rawReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
         canEvict: false,
     });
 
     const reportActionKeys = rawReportActions ? Object.keys(rawReportActions) : [];
-
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
-
     const initialFocused = shouldFocusComposerOnScreenFocus && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
 
-    const containerRef = useRef<View>(null);
-    const suggestionsRef = useRef<SuggestionsRef>(null);
-    const composerRef = useRef<ComposerWithSuggestionsRef | null>(null);
-    const actionButtonRef = useRef<View | HTMLDivElement | null>(null);
-    const attachmentFileRef = useRef<FileObject | FileObject[] | null>(null);
-
-    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
+    const includesConcierge = chatIncludesConcierge({participants: report?.participants});
+    const userBlockedFromConcierge = isBlockedFromConciergeUserAction(blockedFromConcierge);
+    const isBlockedFromConcierge = includesConcierge && userBlockedFromConcierge;
 
     const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize);
     const [isMenuVisible, setMenuVisibility] = useState(false);
-
     const [text, setText] = useState(() => {
         return draftComment ?? '';
     });
 
-    const includesConcierge = chatIncludesConcierge({participants: report?.participants});
-    const userBlockedFromConcierge = isBlockedFromConciergeUserAction(blockedFromConcierge);
-    const isBlockedFromConcierge = includesConcierge && userBlockedFromConcierge;
+    const containerRef = useRef<View>(null);
+    const suggestionsRef = useRef<SuggestionsRef>(null);
+    const composerRef = useRef<ComposerWithSuggestionsRef | null>(null);
+    const actionButtonRef = useRef<View | HTMLDivElement | null>(null);
+    const attachmentFileRef = useRef<FileObject | FileObject[] | null>(null);
 
-    const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
+    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
 
+    const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
     const isEditingLastReportAction = editingReportActionID === reportActionKeys.at(-1);
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
@@ -86,9 +81,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     }, [didResetComposerHeight, editingState]);
 
     const isEditingInComposer = shouldUseNarrowLayout && editingState !== 'off' && !didResetComposerHeight;
-
     const effectiveDraft = isEditingInComposer ? editingMessage : draftComment;
-    const isEmpty = !isEditingInComposer && (!effectiveDraft || !!text.match(CONST.REGEX.EMPTY_COMMENT));
 
     const {debouncedCommentMaxLengthValidation, exceededMaxLength, isExceedingMaxLength, isTaskTitle} = useDebouncedCommentMaxLengthValidation({
         reportID,
@@ -106,7 +99,8 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         composerRef,
     });
 
-    const isSubmittingDraftCommentDisabled = isBlockedFromConcierge || isExceedingMaxLength || isEmpty;
+    const isDraftCommentEmpty = !text || !!text.match(CONST.REGEX.EMPTY_COMMENT);
+    const isSubmittingDraftCommentDisabled = isBlockedFromConcierge || isExceedingMaxLength || isDraftCommentEmpty;
     const isSendDisabled = !isEditingInComposer && isSubmittingDraftCommentDisabled;
 
     const {isFocused, onBlur, onFocus, onAddActionPressed, onItemSelected, onTriggerAttachmentPicker, isNextModalWillOpenRef} = useComposerFocus({

From ebfda2a9274dff8420885323ef123c9345563f43 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 21 Apr 2026 23:41:43 +0100
Subject: [PATCH 190/233] fix: duplicate selection update

---
 src/components/Composer/implementation/index.tsx | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 4533d0200760..424b1bd5ec69 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -133,9 +133,6 @@ function Composer({
                 },
             });
             setSelection(selectionValue);
-
-            onSelectionChange(event);
-            setSelection(event.nativeEvent.selection);
         },
         [isRendered, onSelectionChange, shouldCalculateCaretPosition],
     );

From e5d0a57c6daa61f4feb51a7c56b96fe7e84b7c85 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 00:49:39 +0100
Subject: [PATCH 191/233] fix: inifinite setState loop due to missing
 memoization in `PureReportActionItem`

---
 .../inbox/report/PureReportActionItem.tsx     | 21 ++++++++++---------
 src/pages/inbox/report/ReportActionItem.tsx   |  5 +++++
 2 files changed, 16 insertions(+), 10 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 556a000d2a05..29c451347110 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -158,7 +158,6 @@ import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMe
 import type {ContextMenuAnchor} from './ContextMenu/ReportActionContextMenu';
 import {hideContextMenu, hideDeleteModal, isActiveReportAction, showContextMenu} from './ContextMenu/ReportActionContextMenu';
 import LinkPreviewer from './LinkPreviewer';
-import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 import ReportActionItemBasicMessage from './ReportActionItemBasicMessage';
 import ReportActionItemContentCreated from './ReportActionItemContentCreated';
 import ReportActionItemDraft from './ReportActionItemDraft';
@@ -228,6 +227,9 @@ type PureReportActionItemProps = {
     /** Whether context menu should be displayed */
     shouldDisplayContextMenu?: boolean;
 
+    /** ReportAction draft message */
+    draftMessage?: string;
+
     /** The IOU/Expense report we are paying */
     iouReport?: OnyxTypes.Report;
 
@@ -375,6 +377,7 @@ function PureReportActionItem({
     shouldUseThreadDividerLine = false,
     shouldDisplayContextMenu = true,
     parentReportActionForTransactionThread,
+    draftMessage,
     iouReport,
     taskReport,
     linkedReport,
@@ -433,14 +436,10 @@ function PureReportActionItem({
     const composerTextInputRef = useRef<ComposerRef | null>(null);
     const popoverAnchorRef = useRef<Exclude<ContextMenuAnchor, TextInput>>(null);
     const downloadedPreviews = useRef<string[]>([]);
+    const prevDraftMessage = usePrevious(draftMessage);
     const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID;
     const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked);
     const isReportArchived = useReportIsArchived(reportID);
-
-    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
-    const draftMessage = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
-
-    const prevDraftMessage = usePrevious(draftMessage);
     const isEditingInline = !shouldUseNarrowLayout && draftMessage !== undefined;
 
     const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID);
@@ -680,15 +679,15 @@ function PureReportActionItem({
         },
         [
             action.reportActionID,
+            reportID,
+            toggleContextMenuFromActiveReportAction,
+            originalReportID,
             disabledActions,
-            handleShowContextMenu,
             isArchivedRoom,
             isChronosReport,
+            handleShowContextMenu,
             isContextMenuDisabled,
             isThreadReportParentAction,
-            originalReportID,
-            reportID,
-            toggleContextMenuFromActiveReportAction,
         ],
     );
 
@@ -1291,6 +1290,7 @@ function PureReportActionItem({
     if (isWhisperActionTargetedToOthers(action)) {
         return null;
     }
+
     const whisperedTo = getWhisperedTo(action);
     const isMultipleParticipant = whisperedTo.length > 1;
 
@@ -1483,6 +1483,7 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => {
         deepEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) &&
         deepEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) &&
         deepEqual(prevParentReportAction, nextParentReportAction) &&
+        prevProps.draftMessage === nextProps.draftMessage &&
         prevProps.iouReport?.reportID === nextProps.iouReport?.reportID &&
         deepEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
         deepEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) &&
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index 86270ff68dbd..18ceb0619865 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -23,6 +23,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type {PersonalDetailsList, ReportActionReactions, Transaction} from '@src/types/onyx';
 import type {PureReportActionItemProps} from './PureReportActionItem';
 import PureReportActionItem from './PureReportActionItem';
+import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 
 type ReportActionItemProps = Omit<PureReportActionItemProps, 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'betas'> & {
     /** Emoji reactions for the report action */
@@ -94,6 +95,9 @@ function ReportActionItem({
     const targetReport = isChatThread(report) ? parentReport : report;
     const missingPaymentMethod = getIndicatedMissingPaymentMethod(userWalletTierName, targetReport?.reportID, action, bankAccountList);
 
+    const {editingMessage, editingReportAction} = useReportActionActiveEdit();
+    const draftMessage = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
+
     return (
         <PureReportActionItem
             // eslint-disable-next-line react/jsx-props-no-spreading
@@ -105,6 +109,7 @@ function ReportActionItem({
             report={report}
             policy={policy}
             currentUserAccountID={currentUserAccountID}
+            draftMessage={draftMessage}
             iouReport={iouReport}
             taskReport={taskReport}
             cardList={cardList}

From bf651d897c1ec6439f27f2bbf2116c0f3cec11a0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 11:16:28 +0100
Subject: [PATCH 192/233] fix: add back draft message prop for
 `DuplicateTransactionItem`

---
 .../TransactionDuplicate/DuplicateTransactionItem.tsx    | 9 +++++++++
 src/pages/inbox/report/ReportActionItem.tsx              | 7 ++++++-
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
index d21d7dab3892..73521e291054 100644
--- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
+++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
@@ -8,6 +8,7 @@ import useOnyx from '@hooks/useOnyx';
 import useThemeStyles from '@hooks/useThemeStyles';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
 import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getOriginalReportID} from '@libs/ReportUtils';
 import ReportActionItem from '@pages/inbox/report/ReportActionItem';
 import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext';
 import CONST from '@src/CONST';
@@ -40,6 +41,10 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
         return IOUTransactionID === transaction?.transactionID;
     });
 
+    const originalReportID = getOriginalReportID(report?.reportID, action, reportActions);
+
+    const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`);
+
     const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action?.reportActionID}`);
 
     const [linkedTransactionRouteError] = useOnyx(
@@ -56,6 +61,9 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
         return null;
     }
 
+    const reportDraftMessage = draftMessage?.[action.reportActionID];
+    const matchingDraftMessage = reportDraftMessage?.message;
+
     return (
         <View style={styles.pb2}>
             <ReportActionItemStateContext.Provider value={stateValue}>
@@ -72,6 +80,7 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
                         userWalletTierName={userWalletTierName}
                         isUserValidated={isUserValidated}
                         personalDetails={personalDetails}
+                        draftMessage={matchingDraftMessage}
                         emojiReactions={emojiReactions}
                         linkedTransactionRouteError={linkedTransactionRouteError}
                         userBillingFundID={userBillingFundID}
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index 18ceb0619865..76966807aa2e 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -26,6 +26,9 @@ import PureReportActionItem from './PureReportActionItem';
 import {useReportActionActiveEdit} from './ReportActionEditMessageContext';
 
 type ReportActionItemProps = Omit<PureReportActionItemProps, 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'betas'> & {
+    /** Draft message for the report action */
+    draftMessage?: string;
+
     /** Emoji reactions for the report action */
     emojiReactions?: OnyxEntry<ReportActionReactions>;
 
@@ -48,6 +51,7 @@ type ReportActionItemProps = Omit<PureReportActionItemProps, 'taskReport' | 'lin
 function ReportActionItem({
     action,
     report,
+    draftMessage: draftMessageProp,
     emojiReactions,
     userWalletTierName,
     isUserValidated,
@@ -96,7 +100,8 @@ function ReportActionItem({
     const missingPaymentMethod = getIndicatedMissingPaymentMethod(userWalletTierName, targetReport?.reportID, action, bankAccountList);
 
     const {editingMessage, editingReportAction} = useReportActionActiveEdit();
-    const draftMessage = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
+    const draftMessageFromEditingContext = editingReportAction && action && editingReportAction.reportActionID === action.reportActionID ? (editingMessage ?? undefined) : undefined;
+    const draftMessage = draftMessageProp ?? draftMessageFromEditingContext;
 
     return (
         <PureReportActionItem

From 5a191fce8cd2baa50f6326374aaf68ea42eb5165 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 11:19:38 +0100
Subject: [PATCH 193/233] refactor: update `clearAllReportActionDrafts`
 function name and JSDoc

---
 src/libs/actions/Report/index.ts                            | 6 +++---
 src/pages/inbox/ReportScreen.tsx                            | 6 +++---
 src/pages/inbox/report/PureReportActionItem.tsx             | 4 ++--
 .../inbox/report/ReportActionCompose/useEditMessage.ts      | 4 ++--
 4 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index 5750ea5da5ef..b8fb7b9afa02 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -2947,8 +2947,8 @@ function editReportComment(
     );
 }
 
-/** Clears drafts for all comment report action in a report. */
-function clearReportActionDrafts() {
+/** Clears drafts for all comment report action across all reports */
+function clearAllReportActionDrafts() {
     Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {});
 }
 
@@ -7380,7 +7380,7 @@ export {
     extractRHPVariantFromResponse,
     createNewReport,
     deleteReport,
-    clearReportActionDrafts,
+    clearAllReportActionDrafts,
     deleteReportComment,
     deleteReportField,
     dismissTrackExpenseActionableWhisper,
diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index 226bec9950dc..e4376240d003 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -11,7 +11,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useSubmitToDestinationVisible from '@hooks/useSubmitToDestinationVisible';
 import useThemeStyles from '@hooks/useThemeStyles';
 import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
-import {clearReportActionDrafts} from '@libs/actions/Report';
+import {clearAllReportActionDrafts} from '@libs/actions/Report';
 import {flushDeferredWrite, hasDeferredWrite} from '@libs/deferredLayoutWrite';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -56,10 +56,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
 
     // When the report screen is navigated away from, clear all report action edit drafts
     useEffect(() => {
-        clearReportActionDrafts();
+        clearAllReportActionDrafts();
 
         return () => {
-            clearReportActionDrafts();
+            clearAllReportActionDrafts();
         };
     }, []);
 
diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 29c451347110..7fa9b60c0cd2 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -131,7 +131,7 @@ import variables from '@styles/variables';
 import {openPersonalBankAccountSetupView} from '@userActions/BankAccounts';
 import type {IgnoreDirection} from '@userActions/ClearReportActionErrors';
 import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction';
-import {clearReportActionDrafts, createTransactionThreadReport, expandURLPreview} from '@userActions/Report';
+import {clearAllReportActionDrafts, createTransactionThreadReport, expandURLPreview} from '@userActions/Report';
 import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -578,7 +578,7 @@ function PureReportActionItem({
         if (draftMessage === undefined || !isDeletedAction(action)) {
             return;
         }
-        clearReportActionDrafts();
+        clearAllReportActionDrafts();
     }, [draftMessage, action, reportID]);
 
     // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 7a9c6ff89632..8a26a4ac97d3 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -7,7 +7,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
 import useOnyx from '@hooks/useOnyx';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useReportScrollManager from '@hooks/useReportScrollManager';
-import {clearReportActionDrafts, editReportComment} from '@libs/actions/Report';
+import {clearAllReportActionDrafts, editReportComment} from '@libs/actions/Report';
 import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
 import {getOriginalReportID} from '@libs/ReportUtils';
 import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
@@ -49,7 +49,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT
 
         stopEditing();
 
-        clearReportActionDrafts();
+        clearAllReportActionDrafts();
 
         // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
         if (shouldScrollToLastMessage) {

From 061ae7e02cf56ba5e8ba6062eee4ec5cb1e01800 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 11:28:41 +0100
Subject: [PATCH 194/233] refactor: extract `requestKeyboardForFocusedComposer`
 from `focusComposerWithDelay` into platform-specific files

---
 src/libs/focusComposerWithDelay/index.ts      | 28 ++-----------------
 .../index.native.ts                           | 21 ++++++++++++++
 .../index.ts                                  |  7 +++++
 .../types.ts                                  |  5 ++++
 4 files changed, 35 insertions(+), 26 deletions(-)
 create mode 100644 src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.native.ts
 create mode 100644 src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.ts
 create mode 100644 src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/types.ts

diff --git a/src/libs/focusComposerWithDelay/index.ts b/src/libs/focusComposerWithDelay/index.ts
index eddf2ad9f3aa..29371f921935 100644
--- a/src/libs/focusComposerWithDelay/index.ts
+++ b/src/libs/focusComposerWithDelay/index.ts
@@ -1,34 +1,10 @@
-import {KeyboardController} from 'react-native-keyboard-controller';
 import ComposerFocusManager from '@libs/ComposerFocusManager';
-import getPlatform from '@libs/getPlatform';
 import isWindowReadyToFocus from '@libs/isWindowReadyToFocus';
 import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
 import CONST from '@src/CONST';
+import requestKeyboardForFocusedComposer from './requestKeyboardForFocusedComposer';
 import setTextInputSelection from './setTextInputSelection';
-import type {FocusComposerWithDelay, InputType, Selection} from './types';
-
-/**
- * When the field already has focus, RN's `focus()` often does not show the IME again.
- * `KeyboardController.setFocusTo('current')` re-applies focus via native (`requestFocusFromJS` on Android,
- * `reloadInputViews` + `focus` on iOS) without blurring first.
- *
- * @see https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#setfocusto
- */
-function requestKeyboardForFocusedComposer(textInput: InputType, forcedSelectionRange?: Selection) {
-    const platform = getPlatform();
-    const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID;
-
-    if (!isNative) {
-        return;
-    }
-
-    requestIdleCallback(() => {
-        KeyboardController.setFocusTo('current');
-        if (forcedSelectionRange) {
-            setTextInputSelection(textInput, forcedSelectionRange);
-        }
-    });
-}
+import type {FocusComposerWithDelay, InputType} from './types';
 
 /**
  * Create a function that focuses the composer.
diff --git a/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.native.ts b/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.native.ts
new file mode 100644
index 000000000000..723e73257307
--- /dev/null
+++ b/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.native.ts
@@ -0,0 +1,21 @@
+import {KeyboardController} from 'react-native-keyboard-controller';
+import setTextInputSelection from '@libs/focusComposerWithDelay/setTextInputSelection';
+import type RequestKeyboardForFocusedComposer from './types';
+
+/**
+ * When the field already has focus, RN's `focus()` often does not show the IME again.
+ * `KeyboardController.setFocusTo('current')` re-applies focus via native (`requestFocusFromJS` on Android,
+ * `reloadInputViews` + `focus` on iOS) without blurring first.
+ *
+ * @see https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#setfocusto
+ */
+const requestKeyboardForFocusedComposer: RequestKeyboardForFocusedComposer = (textInput, forcedSelectionRange) => {
+    requestIdleCallback(() => {
+        KeyboardController.setFocusTo('current');
+        if (forcedSelectionRange) {
+            setTextInputSelection(textInput, forcedSelectionRange);
+        }
+    });
+};
+
+export default requestKeyboardForFocusedComposer;
diff --git a/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.ts b/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.ts
new file mode 100644
index 000000000000..8470a7ea6846
--- /dev/null
+++ b/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/index.ts
@@ -0,0 +1,7 @@
+import type RequestKeyboardForFocusedComposer from './types';
+
+const NOOP = () => {};
+
+const requestKeyboardForFocusedComposer: RequestKeyboardForFocusedComposer = NOOP;
+
+export default requestKeyboardForFocusedComposer;
diff --git a/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/types.ts b/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/types.ts
new file mode 100644
index 000000000000..5464de49d563
--- /dev/null
+++ b/src/libs/focusComposerWithDelay/requestKeyboardForFocusedComposer/types.ts
@@ -0,0 +1,5 @@
+import type {InputType, Selection} from '@libs/focusComposerWithDelay/types';
+
+type RequestKeyboardForFocusedComposer = (textInput: InputType, forcedSelectionRange?: Selection) => void;
+
+export default RequestKeyboardForFocusedComposer;

From 02ffbc3a401cea34c5398b18c25ef05ab56c7abf Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 11:39:55 +0100
Subject: [PATCH 195/233] fix: disable `shouldScrollToLastMessage` for composer
 editing

---
 .../inbox/report/ReportActionCompose/ComposerProvider.tsx  | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index c094b6045428..b7f2c5cc1d57 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -42,11 +42,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`);
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
     const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`);
-    const [rawReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
-        canEvict: false,
-    });
 
-    const reportActionKeys = rawReportActions ? Object.keys(rawReportActions) : [];
     const shouldFocusComposerOnScreenFocus = shouldFocusInputOnScreenFocus || !!draftComment;
     const initialFocused = shouldFocusComposerOnScreenFocus && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible;
 
@@ -69,7 +65,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
 
     const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
-    const isEditingLastReportAction = editingReportActionID === reportActionKeys.at(-1);
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     useEffect(() => {
@@ -94,7 +89,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         reportID: editingReportID ?? undefined,
         originalReportID,
         reportAction: editingReportAction,
-        shouldScrollToLastMessage: isEditingLastReportAction,
+        shouldScrollToLastMessage: false,
         debouncedCommentMaxLengthValidation,
         composerRef,
     });

From 75eec230255245d05b3be63336bff5f792b2e84e Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 11:49:58 +0100
Subject: [PATCH 196/233] fix: also run `SilentCommentUpdater` on narrow layout

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 8c1eb7694e83..aff10c6d0e4f 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -1115,7 +1115,7 @@ function ComposerWithSuggestions({
                 resetKeyboardInput={resetKeyboardInput}
             />
 
-            {isValidReportIDFromPath(reportID) && !shouldUseNarrowLayout && (
+            {isValidReportIDFromPath(reportID) && (
                 <SilentCommentUpdater
                     reportID={reportID}
                     value={value}

From 1f068a10ea5d8064cf41e16e39699301582138c4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 11:54:45 +0100
Subject: [PATCH 197/233] fix: only apply selection after focus in
 `focusComposerWithDelay`

---
 src/libs/focusComposerWithDelay/index.ts | 27 +++++++++---------------
 1 file changed, 10 insertions(+), 17 deletions(-)

diff --git a/src/libs/focusComposerWithDelay/index.ts b/src/libs/focusComposerWithDelay/index.ts
index 29371f921935..dbbcfae1d965 100644
--- a/src/libs/focusComposerWithDelay/index.ts
+++ b/src/libs/focusComposerWithDelay/index.ts
@@ -30,18 +30,22 @@ function focusComposerWithDelay(textInput: InputType | null, delay: number = CON
             return;
         }
 
-        if (!shouldDelay) {
+        function focusAndUpdateSelection(input: InputType) {
             if (getIsFocused()) {
                 if (forceKeyboardIfAlreadyFocused) {
-                    requestKeyboardForFocusedComposer(textInput, forcedSelectionRange);
+                    requestKeyboardForFocusedComposer(input, forcedSelectionRange);
                 }
                 return;
             }
 
-            textInput.focus();
+            input.focus();
             if (forcedSelectionRange) {
-                setTextInputSelection(textInput, forcedSelectionRange);
+                setTextInputSelection(input, forcedSelectionRange);
             }
+        }
+
+        if (!shouldDelay) {
+            focusAndUpdateSelection(textInput);
             return;
         }
 
@@ -50,21 +54,10 @@ function focusComposerWithDelay(textInput: InputType | null, delay: number = CON
         if (!textInput) {
             return;
         }
+
         // When the closing modal has a focused text input focus() needs a delay to properly work.
         // Setting 150ms here is a temporary workaround for the Android HybridApp. It should be reverted once we identify the real root cause of this issue: https://github.com/Expensify/App/issues/56311.
-        setTimeout(() => {
-            if (getIsFocused()) {
-                if (forceKeyboardIfAlreadyFocused) {
-                    // Selection is applied synchronously below; only request focus
-                    requestKeyboardForFocusedComposer(textInput);
-                }
-                return;
-            }
-            textInput.focus();
-        }, delay);
-        if (forcedSelectionRange) {
-            setTextInputSelection(textInput, forcedSelectionRange);
-        }
+        setTimeout(() => focusAndUpdateSelection(textInput), delay);
     };
 }
 

From 99232e6006fdcf7ad9f36065392af512c9f7a10d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 15:14:07 +0100
Subject: [PATCH 198/233] test: add unit and ui tests for new editing modes

---
 ...portActionComposeNarrowMessageEditTest.tsx | 219 +++++++++++
 tests/unit/hooks/useEditComposerToggleTest.ts | 366 ++++++++++++++++++
 2 files changed, 585 insertions(+)
 create mode 100644 tests/ui/ReportActionComposeNarrowMessageEditTest.tsx
 create mode 100644 tests/unit/hooks/useEditComposerToggleTest.ts

diff --git a/tests/ui/ReportActionComposeNarrowMessageEditTest.tsx b/tests/ui/ReportActionComposeNarrowMessageEditTest.tsx
new file mode 100644
index 000000000000..98b388aee342
--- /dev/null
+++ b/tests/ui/ReportActionComposeNarrowMessageEditTest.tsx
@@ -0,0 +1,219 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import type * as NativeNavigation from '@react-navigation/native';
+import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
+import React from 'react';
+import type {PropsWithChildren} from 'react';
+import Onyx from 'react-native-onyx';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
+import * as Report from '@libs/actions/Report';
+import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
+import * as TestHelper from '../utils/TestHelper';
+import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
+
+// Narrow layout: message edits happen in the main composer (ComposerWithSuggestions + useEditComposerToggle),
+// and ReportActionCompose swaps the attachment action row for edit chrome (ComposerEditingButtons).
+
+jest.mock('@hooks/useResponsiveLayout', () => ({
+    __esModule: true,
+    default: () => ({
+        shouldUseNarrowLayout: true,
+        isSmallScreenWidth: true,
+        isInNarrowPaneModal: false,
+        isExtraSmallScreenHeight: false,
+        isExtraSmallScreenWidth: false,
+        isMediumScreenWidth: false,
+        onboardingIsMediumOrLargerScreenWidth: false,
+        isLargeScreenWidth: false,
+        isSmallScreen: true,
+    }),
+}));
+
+jest.mock('@libs/getPlatform', () => ({
+    __esModule: true,
+    default: () => 'web',
+}));
+
+jest.mock('@libs/ComponentUtils', () => ({
+    forceClearInput: jest.fn(),
+}));
+
+jest.mock('@hooks/useLocalize', () =>
+    jest.fn(() => ({
+        translate: jest.fn((key: string) => key),
+        numberFormat: jest.fn((num: number) => num.toString()),
+    })),
+);
+
+jest.mock('@hooks/usePaginatedReportActions', () => jest.fn(() => ({reportActions: [], hasNewerActions: false, hasOlderActions: false})));
+jest.mock('@hooks/useParentReportAction', () => jest.fn(() => null));
+jest.mock('@hooks/useReportTransactionsCollection', () => jest.fn(() => ({})));
+jest.mock('@hooks/useShortMentionsList', () => jest.fn(() => ({availableLoginsList: []})));
+jest.mock('@hooks/useSidePanelState', () => jest.fn(() => ({sessionStartTime: null})));
+
+jest.mock('@components/DropZone/DualDropZone', () => {
+    const RN = jest.requireActual<Record<string, React.ComponentType<{testID?: string; children?: React.ReactNode}>>>('react-native');
+    return ({shouldAcceptSingleReceipt}: {shouldAcceptSingleReceipt?: boolean}) => (
+        <RN.Text testID="dual-drop-zone">{shouldAcceptSingleReceipt ? 'receipt-editable' : 'receipt-not-editable'}</RN.Text>
+    );
+});
+
+const mockRouteReportID = {current: '1'};
+
+jest.mock('@react-navigation/native', () => ({
+    ...((): typeof NativeNavigation => {
+        return jest.requireActual('@react-navigation/native');
+    })(),
+    useNavigation: jest.fn(() => ({
+        navigate: jest.fn(),
+        addListener: jest.fn(() => jest.fn()),
+    })),
+    useIsFocused: jest.fn(() => true),
+    useRoute: jest.fn(() => ({key: '', name: '', params: {reportID: mockRouteReportID.current}})),
+}));
+
+TestHelper.setupGlobalFetchMock();
+
+const defaultReport = LHNTestUtils.getFakeReport();
+mockRouteReportID.current = defaultReport.reportID;
+
+const defaultProps: ReportActionComposeProps = {
+    reportID: defaultReport.reportID,
+};
+
+const commentAction = {
+    ...LHNTestUtils.getFakeReportAction(),
+    actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
+};
+
+function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
+    return <ReportActionEditMessageContextProvider reportID={defaultReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
+}
+
+function ReportScreenProviders({children}: PropsWithChildren) {
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+}
+
+const saveReportActionDraftSpy = jest.spyOn(Report, 'saveReportActionDraft');
+
+function renderNarrowReportActionCompose() {
+    return render(
+        <ReportScreenProviders>
+            <ReportActionCompose
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultProps}
+            />
+        </ReportScreenProviders>,
+    );
+}
+
+describe('ReportActionCompose — narrow layout message edit flow', () => {
+    beforeAll(() => {
+        Onyx.init({
+            keys: ONYXKEYS,
+            evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+        });
+    });
+
+    beforeEach(() => {
+        jest.useFakeTimers();
+    });
+
+    afterEach(async () => {
+        jest.useRealTimers();
+        await act(async () => {
+            await Onyx.clear();
+        });
+        saveReportActionDraftSpy.mockClear();
+    });
+
+    it('shows edit chrome (close) instead of the create control while a report action draft is open', async () => {
+        await act(async () => {
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
+                [commentAction.reportActionID]: commentAction,
+            });
+            await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
+                [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
+                    [commentAction.reportActionID]: {message: 'Edit me in the composer'},
+                },
+            });
+        });
+        await waitForBatchedUpdatesWithAct();
+
+        renderNarrowReportActionCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.getByLabelText('common.close')).toBeOnTheScreen();
+        expect(screen.queryByLabelText('common.create')).toBeNull();
+    });
+
+    it('loads the draft into the composer and debounces saveReportActionDraft while typing in edit mode', async () => {
+        await act(async () => {
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
+                [commentAction.reportActionID]: commentAction,
+            });
+            await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
+                [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
+                    [commentAction.reportActionID]: {message: 'Start'},
+                },
+            });
+        });
+        await waitForBatchedUpdatesWithAct();
+
+        renderNarrowReportActionCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        const composer = screen.getByTestId('composer');
+        await waitFor(() => {
+            expect(composer.props.value).toBe('Start');
+        });
+
+        fireEvent.changeText(composer, 'Start, edited');
+
+        act(() => {
+            jest.advanceTimersByTime(1100);
+        });
+        await waitForBatchedUpdatesWithAct();
+
+        expect(saveReportActionDraftSpy).toHaveBeenCalled();
+        const lastCall = saveReportActionDraftSpy.mock.calls.at(-1);
+        expect(lastCall?.[0]).toBe(defaultReport.reportID);
+        expect(lastCall?.[2]).toBe('Start, edited');
+    });
+
+    it('leaves edit mode and restores default composer actions when the user cancels', async () => {
+        await act(async () => {
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
+                [commentAction.reportActionID]: commentAction,
+            });
+            await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
+                [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
+                    [commentAction.reportActionID]: {message: 'To cancel'},
+                },
+            });
+        });
+        await waitForBatchedUpdatesWithAct();
+
+        renderNarrowReportActionCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        const cancelButton = screen.getByLabelText('common.close');
+        fireEvent.press(cancelButton);
+        await waitForBatchedUpdatesWithAct();
+
+        await waitFor(() => {
+            expect(screen.getByLabelText('common.create')).toBeOnTheScreen();
+        });
+        expect(screen.queryByLabelText('common.close')).toBeNull();
+    });
+});
diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
new file mode 100644
index 000000000000..19ef9ab9eeca
--- /dev/null
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -0,0 +1,366 @@
+import {act, renderHook} from '@testing-library/react-native';
+import type {ComposerRef, TextSelection} from '@components/Composer/types';
+import type {RefObject} from 'react';
+import ReportActionComposeUtils from '@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils';
+import useEditComposerToggle from '@pages/inbox/report/ReportActionCompose/useEditComposerToggle';
+import type {ReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
+import * as ReportActionEditMessageContext from '@pages/inbox/report/ReportActionEditMessageContext';
+
+jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@pages/inbox/report/ReportActionEditMessageContext');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        useReportActionActiveEdit: jest.fn(),
+    };
+});
+
+jest.mock('@hooks/useResponsiveLayout', () => ({
+    __esModule: true,
+    default: jest.fn(() => ({
+        shouldUseNarrowLayout: true,
+        isSmallScreenWidth: true,
+        isInNarrowPaneModal: false,
+        isExtraSmallScreenHeight: false,
+        isExtraSmallScreenWidth: false,
+        isMediumScreenWidth: false,
+        onboardingIsMediumOrLargerScreenWidth: false,
+        isLargeScreenWidth: false,
+        isSmallScreen: true,
+    })),
+}));
+
+jest.mock('@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils', () => ({
+    __esModule: true,
+    default: {
+        updateNativeSelectionValue: jest.fn(),
+    },
+}));
+
+jest.mock('@libs/getPlatform', () => ({
+    __esModule: true,
+    default: () => 'web',
+}));
+
+const mockUseReportActionActiveEdit = jest.mocked(ReportActionEditMessageContext.useReportActionActiveEdit);
+const mockUseResponsiveLayout = jest.requireMock('@hooks/useResponsiveLayout').default as jest.Mock;
+const mockUpdateNativeSelectionValue = jest.mocked(ReportActionComposeUtils.updateNativeSelectionValue);
+
+type ActiveEdit = ReportActionActiveEdit & {editingState: 'off' | 'editing' | 'submitted'};
+
+function makeComposerRef(overrides?: Partial<ComposerRef>): RefObject<ComposerRef | null> {
+    return {
+        current: {
+            blur: jest.fn(),
+            isFocused: jest.fn(() => false),
+            setNativeProps: jest.fn(),
+            setSelection: jest.fn(),
+            focus: jest.fn(),
+            ...overrides,
+        } as unknown as ComposerRef,
+    };
+}
+
+function defaultActiveEdit(overrides?: Partial<ActiveEdit>): ActiveEdit {
+    return {
+        editingReportID: null,
+        editingReportActionID: null,
+        editingReportAction: null,
+        editingMessage: null,
+        currentEditMessageSelection: null,
+        editingState: 'off',
+        ...overrides,
+    };
+}
+
+function wideLayoutResult() {
+    return {
+        shouldUseNarrowLayout: false,
+        isSmallScreenWidth: false,
+        isInNarrowPaneModal: false,
+        isExtraSmallScreenHeight: false,
+        isExtraSmallScreenWidth: false,
+        isMediumScreenWidth: false,
+        onboardingIsMediumOrLargerScreenWidth: true,
+        isLargeScreenWidth: true,
+        isSmallScreen: false,
+    };
+}
+
+function narrowLayoutResult() {
+    return {
+        shouldUseNarrowLayout: true,
+        isSmallScreenWidth: true,
+        isInNarrowPaneModal: false,
+        isExtraSmallScreenHeight: false,
+        isExtraSmallScreenWidth: false,
+        isMediumScreenWidth: false,
+        onboardingIsMediumOrLargerScreenWidth: false,
+        isLargeScreenWidth: false,
+        isSmallScreen: true,
+    };
+}
+
+describe('useEditComposerToggle', () => {
+    const activeEditRef = {current: defaultActiveEdit()};
+
+    beforeEach(() => {
+        jest.clearAllMocks();
+        activeEditRef.current = defaultActiveEdit();
+        mockUseReportActionActiveEdit.mockImplementation(() => activeEditRef.current);
+        mockUseResponsiveLayout.mockReturnValue(narrowLayoutResult());
+    });
+
+    it('does not run apply logic while editingState is submitted', () => {
+        const onValueChange = jest.fn();
+        const composerRef = makeComposerRef();
+        activeEditRef.current = defaultActiveEdit({editingState: 'submitted', editingMessage: 'hello'});
+
+        renderHook(() =>
+            useEditComposerToggle({
+                selection: {start: 0, end: 0},
+                draftComment: 'draft',
+                composerRef,
+                onValueChange,
+            }),
+        );
+
+        expect(onValueChange).not.toHaveBeenCalled();
+    });
+
+    it('on narrow layout, when editing starts, applies editing message, selection at end, and focus', () => {
+        const onValueChange = jest.fn();
+        const onSelectionChange = jest.fn();
+        const onFocus = jest.fn();
+        const composerRef = makeComposerRef();
+        const priorSelection: TextSelection = {start: 0, end: 0};
+
+        const {rerender} = renderHook(
+            (props: {selection: TextSelection; draft: string}) =>
+                useEditComposerToggle({
+                    selection: props.selection,
+                    draftComment: props.draft,
+                    composerRef,
+                    onValueChange,
+                    onSelectionChange,
+                    onFocus,
+                }),
+            {initialProps: {selection: priorSelection, draft: 'keep my draft'}},
+        );
+
+        act(() => {
+            activeEditRef.current = defaultActiveEdit({
+                editingState: 'editing',
+                editingMessage: 'edited body',
+                editingReportActionID: '100',
+                currentEditMessageSelection: {start: 1, end: 2},
+            });
+            rerender({selection: priorSelection, draft: 'keep my draft'});
+        });
+
+        const expectedEnd = 'edited body'.length;
+        expect(onValueChange).toHaveBeenCalledWith('edited body');
+        expect(onSelectionChange).toHaveBeenCalledWith({start: expectedEnd, end: expectedEnd});
+        expect(onFocus).toHaveBeenCalled();
+    });
+
+    it('on wide layout, when editing starts, leaves composer value unchanged (inline editor handles edit)', () => {
+        mockUseResponsiveLayout.mockReturnValue(wideLayoutResult());
+
+        const onValueChange = jest.fn();
+        const composerRef = makeComposerRef();
+
+        const {rerender} = renderHook(() =>
+            useEditComposerToggle({
+                selection: {start: 0, end: 0},
+                draftComment: 'draft in composer',
+                composerRef,
+                onValueChange,
+            }),
+        );
+
+        act(() => {
+            activeEditRef.current = defaultActiveEdit({
+                editingState: 'editing',
+                editingMessage: 'from thread',
+            });
+            rerender();
+        });
+
+        expect(onValueChange).not.toHaveBeenCalled();
+    });
+
+    it('on narrow, when edit ends, restores prior draft and selection; blurs if composer was not focused before', () => {
+        const onValueChange = jest.fn();
+        const onSelectionChange = jest.fn();
+        const composerRef = makeComposerRef();
+        const priorSelection: TextSelection = {start: 2, end: 5};
+
+        // Start with edit off so wasEditingRef is false; then turn editing on to capture previousDraftSelectionRef.
+        const {rerender} = renderHook(
+            (props: {selection: TextSelection; draft: string; editing: boolean}) => {
+                activeEditRef.current = defaultActiveEdit(
+                    props.editing
+                        ? {editingState: 'editing', editingMessage: 'e', editingReportActionID: '1'}
+                        : {editingState: 'off'},
+                );
+                return useEditComposerToggle({
+                    selection: props.selection,
+                    draftComment: props.draft,
+                    composerRef,
+                    onValueChange,
+                    onSelectionChange,
+                });
+            },
+            {initialProps: {selection: priorSelection, draft: 'restored', editing: false}},
+        );
+
+        act(() => {
+            rerender({selection: priorSelection, draft: 'restored', editing: true});
+        });
+
+        onValueChange.mockClear();
+        onSelectionChange.mockClear();
+
+        act(() => {
+            rerender({selection: priorSelection, draft: 'restored', editing: false});
+        });
+
+        expect(onValueChange).toHaveBeenCalledWith('restored');
+        expect(onSelectionChange).toHaveBeenCalledWith(priorSelection);
+        expect(composerRef.current?.blur).toHaveBeenCalled();
+    });
+
+    it('on narrow, when switching the message being edited, applies the new message', () => {
+        const onValueChange = jest.fn();
+        const onFocus = jest.fn();
+        const composerRef = makeComposerRef();
+
+        activeEditRef.current = defaultActiveEdit({
+            editingState: 'editing',
+            editingMessage: 'first',
+            editingReportActionID: 'a',
+        });
+
+        const {rerender} = renderHook(
+            (id: string) => {
+                activeEditRef.current = defaultActiveEdit({
+                    editingState: 'editing',
+                    editingMessage: id === 'a' ? 'first' : 'second',
+                    editingReportActionID: id,
+                });
+                return useEditComposerToggle({
+                    selection: {start: 0, end: 0},
+                    draftComment: 'x',
+                    composerRef,
+                    onValueChange,
+                    onFocus,
+                });
+            },
+            {initialProps: 'a'},
+        );
+
+        onValueChange.mockClear();
+        onFocus.mockClear();
+
+        act(() => {
+            rerender('b');
+        });
+
+        expect(onValueChange).toHaveBeenCalledWith('second');
+        expect(onFocus).toHaveBeenCalled();
+    });
+
+    it('when layout goes from wide to narrow while editing, loads editing message into the composer', () => {
+        mockUseResponsiveLayout.mockReturnValue(wideLayoutResult());
+
+        const onValueChange = jest.fn();
+        const onFocus = jest.fn();
+        const composerRef = makeComposerRef();
+
+        activeEditRef.current = defaultActiveEdit({
+            editingState: 'editing',
+            editingMessage: 'wide first',
+        });
+
+        const {rerender} = renderHook(() =>
+            useEditComposerToggle({
+                selection: {start: 0, end: 0},
+                draftComment: 'composer draft',
+                composerRef,
+                onValueChange,
+                onFocus,
+            }),
+        );
+
+        onValueChange.mockClear();
+
+        act(() => {
+            mockUseResponsiveLayout.mockReturnValue(narrowLayoutResult());
+            rerender();
+        });
+
+        expect(onValueChange).toHaveBeenCalledWith('wide first');
+        expect(onFocus).toHaveBeenCalled();
+    });
+
+    it('when layout goes from narrow to wide while editing, restores the normal draft in the composer', () => {
+        const onValueChange = jest.fn();
+        const composerRef = makeComposerRef();
+
+        activeEditRef.current = defaultActiveEdit({
+            editingState: 'editing',
+            editingMessage: 'editing in narrow',
+        });
+
+        const {rerender} = renderHook(
+            (narrow: boolean) => {
+                mockUseResponsiveLayout.mockReturnValue(narrow ? narrowLayoutResult() : wideLayoutResult());
+                return useEditComposerToggle({
+                    selection: {start: 0, end: 0},
+                    draftComment: 'plain draft for wide',
+                    composerRef,
+                    onValueChange,
+                });
+            },
+            {initialProps: true},
+        );
+
+        onValueChange.mockClear();
+
+        act(() => {
+            rerender(false);
+        });
+
+        expect(onValueChange).toHaveBeenCalledWith('plain draft for wide');
+    });
+
+    it('passes selection through to ReportActionComposeUtils when toggling (non-iOS / web mock)', () => {
+        const onValueChange = jest.fn();
+        const onSelectionChange = jest.fn();
+        const composerRef = makeComposerRef();
+
+        const {rerender} = renderHook(
+            (editing: boolean) => {
+                activeEditRef.current = defaultActiveEdit(
+                    editing ? {editingState: 'editing', editingMessage: 'hi', editingReportActionID: '1'} : {editingState: 'off'},
+                );
+                return useEditComposerToggle({
+                    selection: {start: 0, end: 0},
+                    draftComment: 'd',
+                    composerRef,
+                    onValueChange,
+                    onSelectionChange,
+                });
+            },
+            {initialProps: false},
+        );
+
+        act(() => {
+            rerender(true);
+        });
+
+        expect(mockUpdateNativeSelectionValue).toHaveBeenCalled();
+    });
+});

From e919d28a749bf974762b4d4f0e388801a5ec268a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 15:34:23 +0100
Subject: [PATCH 199/233] chore: add test ids to editing components

---
 src/CONST/index.ts                                   | 12 ++++++++++++
 .../AttachmentPickerWithMenuItems.tsx                |  8 +++++++-
 .../ReportActionCompose/ComposerActionMenu.tsx       |  2 ++
 .../ReportActionCompose/ComposerEditingButtons.tsx   | 11 +++++++++--
 .../ReportActionCompose/MessageEditCancelButton.tsx  |  1 +
 .../ReportActionCompose/ReportActionCompose.tsx      |  5 ++++-
 .../inbox/report/ReportActionItemMessageEdit.tsx     |  2 ++
 7 files changed, 37 insertions(+), 4 deletions(-)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 0f882fbc2f12..cac19437aaad 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1833,6 +1833,18 @@ const CONST = {
         MAX_LINES_FULL: -1,
         // The minimum height needed to enable the full screen composer
         FULL_COMPOSER_MIN_HEIGHT: 60,
+        /**
+         * TestIDs for the main report composer vs inline message editor (E2E / integration tests only).
+         * See tests/ui/ReportActionMessageEditLayoutTest.tsx
+         */
+        TEST_ID: {
+            REPORT_ACTION_COMPOSE: 'reportActionCompose',
+            DRAFT_MESSAGE_ACTION_ROW: 'reportActionCompose_draftMessageActionRow',
+            EDITING_MESSAGE_ACTION_ROW: 'reportActionCompose_editingMessageActionRow',
+            REPORT_ACTION_ITEM_MESSAGE_EDIT: 'reportActionItemMessageEdit',
+            MESSAGE_EDIT_CANCEL_MAIN_COMPOSER: 'messageEditCancel_mainComposer',
+            MESSAGE_EDIT_CANCEL_INLINE: 'messageEditCancel_inlineMessageEdit',
+        },
     },
     MODAL: {
         MODAL_TYPE: {
diff --git a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
index b65ef3c7d34f..51e6bbf3ad9c 100644
--- a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
@@ -110,6 +110,8 @@ type AttachmentPickerWithMenuItemsProps = {
     reportParticipantIDs?: number[];
 
     shouldDisableAttachmentItem?: boolean;
+
+    testID?: string;
 };
 
 /**
@@ -135,6 +137,7 @@ function AttachmentPickerWithMenuItems({
     actionButtonRef,
     raiseIsScrollLikelyLayoutTriggered,
     shouldDisableAttachmentItem,
+    testID,
 }: AttachmentPickerWithMenuItemsProps) {
     const icons = useMemoizedLazyExpensifyIcons([
         'Cash',
@@ -446,7 +449,10 @@ function AttachmentPickerWithMenuItems({
                 ];
                 return (
                     <>
-                        <View style={outerContainerStyles}>
+                        <View
+                            testID={testID}
+                            style={outerContainerStyles}
+                        >
                             <View style={innerContainerStyles}>
                                 <View style={createButtonContainerStyles}>
                                     <Tooltip text={translate('common.create')}>
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
index 004e12a733ca..b5a9db6d205a 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
@@ -5,6 +5,7 @@ import useOnyx from '@hooks/useOnyx';
 import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
 import {chatIncludesConcierge} from '@libs/ReportUtils';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';
+import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {FileObject} from '@src/types/utils/Attachment';
 import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems';
@@ -39,6 +40,7 @@ function ComposerActionMenu({reportID, onAttachmentPicked}: ComposerActionMenuPr
 
     return (
         <AttachmentPickerWithMenuItems
+            testID={CONST.COMPOSER.TEST_ID.DRAFT_MESSAGE_ACTION_ROW}
             onAttachmentPicked={onAttachmentPicked}
             reportID={reportID}
             report={report}
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
index 99d3c7e7562a..b5060f393c42 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import {View} from 'react-native';
 import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
 import {useComposerEditActions} from './ComposerContext';
 import ComposerExpandCollapseButton from './ComposerExpandCollapseButton';
 import MessageEditCancelButton from './MessageEditCancelButton';
@@ -22,9 +23,15 @@ function ComposerEditingButtons({reportID}: ComposerEditingButtonsProps) {
         {paddingVertical: styles.composerSizeButton.marginHorizontal},
     ];
     return (
-        <View style={editingButtonsContainerStyles}>
+        <View
+            testID={CONST.COMPOSER.TEST_ID.EDITING_MESSAGE_ACTION_ROW}
+            style={editingButtonsContainerStyles}
+        >
             <ComposerExpandCollapseButton reportID={reportID} />
-            <MessageEditCancelButton onCancel={deleteDraft} />
+            <MessageEditCancelButton
+                onCancel={deleteDraft}
+                testID={CONST.COMPOSER.TEST_ID.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER}
+            />
         </View>
     );
 }
diff --git a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
index 92f6ed61f1fe..356e1eea4590 100644
--- a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
@@ -12,6 +12,7 @@ import CONST from '@src/CONST';
 
 type MessageEditCancelButtonProps = ViewProps & {
     onCancel: () => void;
+    testID?: string;
 };
 
 function MessageEditCancelButton({onCancel, ...restProps}: MessageEditCancelButtonProps) {
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index e2b2ea61fea0..e4e1730469cd 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -165,7 +165,10 @@ function ReportActionComposeInner({reportID}: ReportActionComposeProps) {
     }
 
     return (
-        <View style={[isComposerFullSize && styles.chatItemFullComposeRow]}>
+        <View
+            testID={CONST.COMPOSER.TEST_ID.REPORT_ACTION_COMPOSE}
+            style={[isComposerFullSize && styles.chatItemFullComposeRow]}
+        >
             <ComposerLocalTime reportID={reportID} />
             <View style={isComposerFullSize ? styles.flex1 : {}}>
                 <ComposerBox reportID={reportID}>
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 975927a3fb42..3fd9f0a6866d 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -396,6 +396,7 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
     return (
         <>
             <View
+                testID={CONST.COMPOSER.TEST_ID.REPORT_ACTION_ITEM_MESSAGE_EDIT}
                 style={[styles.chatItemMessage, styles.flexRow]}
                 ref={containerRef}
             >
@@ -409,6 +410,7 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
                     ]}
                 >
                     <MessageEditCancelButton
+                        testID={CONST.COMPOSER.TEST_ID.MESSAGE_EDIT_CANCEL_INLINE}
                         onCancel={deleteDraft}
                         style={[styles.justifyContentEnd, styles.mb1]}
                     />

From 62ffcc3079a990d49692dcc04c9af133e69ba3a2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 15:35:35 +0100
Subject: [PATCH 200/233] test: remove redundant `act()` and fix ESLint errors

---
 tests/unit/hooks/useEditComposerToggleTest.ts | 69 +++++++------------
 1 file changed, 24 insertions(+), 45 deletions(-)

diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
index 19ef9ab9eeca..170552fe8ff7 100644
--- a/tests/unit/hooks/useEditComposerToggleTest.ts
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -1,9 +1,10 @@
-import {act, renderHook} from '@testing-library/react-native';
-import type {ComposerRef, TextSelection} from '@components/Composer/types';
+/* eslint-disable @typescript-eslint/naming-convention */
+import {renderHook} from '@testing-library/react-native';
 import type {RefObject} from 'react';
+import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import ReportActionComposeUtils from '@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils';
 import useEditComposerToggle from '@pages/inbox/report/ReportActionCompose/useEditComposerToggle';
-import type {ReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
+import type {ReportActionEditMessageContextValue} from '@pages/inbox/report/ReportActionEditMessageContext';
 import * as ReportActionEditMessageContext from '@pages/inbox/report/ReportActionEditMessageContext';
 
 jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => {
@@ -47,7 +48,7 @@ const mockUseReportActionActiveEdit = jest.mocked(ReportActionEditMessageContext
 const mockUseResponsiveLayout = jest.requireMock('@hooks/useResponsiveLayout').default as jest.Mock;
 const mockUpdateNativeSelectionValue = jest.mocked(ReportActionComposeUtils.updateNativeSelectionValue);
 
-type ActiveEdit = ReportActionActiveEdit & {editingState: 'off' | 'editing' | 'submitted'};
+type ActiveEdit = ReportActionEditMessageContextValue & {editingState: 'off' | 'editing' | 'submitted'};
 
 function makeComposerRef(overrides?: Partial<ComposerRef>): RefObject<ComposerRef | null> {
     return {
@@ -149,15 +150,13 @@ describe('useEditComposerToggle', () => {
             {initialProps: {selection: priorSelection, draft: 'keep my draft'}},
         );
 
-        act(() => {
-            activeEditRef.current = defaultActiveEdit({
-                editingState: 'editing',
-                editingMessage: 'edited body',
-                editingReportActionID: '100',
-                currentEditMessageSelection: {start: 1, end: 2},
-            });
-            rerender({selection: priorSelection, draft: 'keep my draft'});
+        activeEditRef.current = defaultActiveEdit({
+            editingState: 'editing',
+            editingMessage: 'edited body',
+            editingReportActionID: '100',
+            currentEditMessageSelection: {start: 1, end: 2},
         });
+        rerender({selection: priorSelection, draft: 'keep my draft'});
 
         const expectedEnd = 'edited body'.length;
         expect(onValueChange).toHaveBeenCalledWith('edited body');
@@ -180,13 +179,11 @@ describe('useEditComposerToggle', () => {
             }),
         );
 
-        act(() => {
-            activeEditRef.current = defaultActiveEdit({
-                editingState: 'editing',
-                editingMessage: 'from thread',
-            });
-            rerender();
+        activeEditRef.current = defaultActiveEdit({
+            editingState: 'editing',
+            editingMessage: 'from thread',
         });
+        rerender({});
 
         expect(onValueChange).not.toHaveBeenCalled();
     });
@@ -200,11 +197,7 @@ describe('useEditComposerToggle', () => {
         // Start with edit off so wasEditingRef is false; then turn editing on to capture previousDraftSelectionRef.
         const {rerender} = renderHook(
             (props: {selection: TextSelection; draft: string; editing: boolean}) => {
-                activeEditRef.current = defaultActiveEdit(
-                    props.editing
-                        ? {editingState: 'editing', editingMessage: 'e', editingReportActionID: '1'}
-                        : {editingState: 'off'},
-                );
+                activeEditRef.current = defaultActiveEdit(props.editing ? {editingState: 'editing', editingMessage: 'e', editingReportActionID: '1'} : {editingState: 'off'});
                 return useEditComposerToggle({
                     selection: props.selection,
                     draftComment: props.draft,
@@ -216,16 +209,12 @@ describe('useEditComposerToggle', () => {
             {initialProps: {selection: priorSelection, draft: 'restored', editing: false}},
         );
 
-        act(() => {
-            rerender({selection: priorSelection, draft: 'restored', editing: true});
-        });
+        rerender({selection: priorSelection, draft: 'restored', editing: true});
 
         onValueChange.mockClear();
         onSelectionChange.mockClear();
 
-        act(() => {
-            rerender({selection: priorSelection, draft: 'restored', editing: false});
-        });
+        rerender({selection: priorSelection, draft: 'restored', editing: false});
 
         expect(onValueChange).toHaveBeenCalledWith('restored');
         expect(onSelectionChange).toHaveBeenCalledWith(priorSelection);
@@ -264,9 +253,7 @@ describe('useEditComposerToggle', () => {
         onValueChange.mockClear();
         onFocus.mockClear();
 
-        act(() => {
-            rerender('b');
-        });
+        rerender('b');
 
         expect(onValueChange).toHaveBeenCalledWith('second');
         expect(onFocus).toHaveBeenCalled();
@@ -296,10 +283,8 @@ describe('useEditComposerToggle', () => {
 
         onValueChange.mockClear();
 
-        act(() => {
-            mockUseResponsiveLayout.mockReturnValue(narrowLayoutResult());
-            rerender();
-        });
+        mockUseResponsiveLayout.mockReturnValue(narrowLayoutResult());
+        rerender({});
 
         expect(onValueChange).toHaveBeenCalledWith('wide first');
         expect(onFocus).toHaveBeenCalled();
@@ -329,9 +314,7 @@ describe('useEditComposerToggle', () => {
 
         onValueChange.mockClear();
 
-        act(() => {
-            rerender(false);
-        });
+        rerender(false);
 
         expect(onValueChange).toHaveBeenCalledWith('plain draft for wide');
     });
@@ -343,9 +326,7 @@ describe('useEditComposerToggle', () => {
 
         const {rerender} = renderHook(
             (editing: boolean) => {
-                activeEditRef.current = defaultActiveEdit(
-                    editing ? {editingState: 'editing', editingMessage: 'hi', editingReportActionID: '1'} : {editingState: 'off'},
-                );
+                activeEditRef.current = defaultActiveEdit(editing ? {editingState: 'editing', editingMessage: 'hi', editingReportActionID: '1'} : {editingState: 'off'});
                 return useEditComposerToggle({
                     selection: {start: 0, end: 0},
                     draftComment: 'd',
@@ -357,9 +338,7 @@ describe('useEditComposerToggle', () => {
             {initialProps: false},
         );
 
-        act(() => {
-            rerender(true);
-        });
+        rerender(true);
 
         expect(mockUpdateNativeSelectionValue).toHaveBeenCalled();
     });

From 90f57156d067f8a93e63fe3b0f4469850c1f1e60 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 15:35:48 +0100
Subject: [PATCH 201/233] test: improve `ReportActionMessageEditLayoutTest`
 suite and handle more cases

---
 ...portActionComposeNarrowMessageEditTest.tsx | 219 ----------
 .../ui/ReportActionMessageEditLayoutTest.tsx  | 401 ++++++++++++++++++
 2 files changed, 401 insertions(+), 219 deletions(-)
 delete mode 100644 tests/ui/ReportActionComposeNarrowMessageEditTest.tsx
 create mode 100644 tests/ui/ReportActionMessageEditLayoutTest.tsx

diff --git a/tests/ui/ReportActionComposeNarrowMessageEditTest.tsx b/tests/ui/ReportActionComposeNarrowMessageEditTest.tsx
deleted file mode 100644
index 98b388aee342..000000000000
--- a/tests/ui/ReportActionComposeNarrowMessageEditTest.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-/* eslint-disable @typescript-eslint/naming-convention */
-import type * as NativeNavigation from '@react-navigation/native';
-import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
-import React from 'react';
-import type {PropsWithChildren} from 'react';
-import Onyx from 'react-native-onyx';
-import ComposeProviders from '@components/ComposeProviders';
-import {LocaleContextProvider} from '@components/LocaleContextProvider';
-import OnyxListItemProvider from '@components/OnyxListItemProvider';
-import {KeyboardStateProvider} from '@components/withKeyboardState';
-import * as Report from '@libs/actions/Report';
-import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
-import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import * as LHNTestUtils from '../utils/LHNTestUtils';
-import * as TestHelper from '../utils/TestHelper';
-import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
-
-// Narrow layout: message edits happen in the main composer (ComposerWithSuggestions + useEditComposerToggle),
-// and ReportActionCompose swaps the attachment action row for edit chrome (ComposerEditingButtons).
-
-jest.mock('@hooks/useResponsiveLayout', () => ({
-    __esModule: true,
-    default: () => ({
-        shouldUseNarrowLayout: true,
-        isSmallScreenWidth: true,
-        isInNarrowPaneModal: false,
-        isExtraSmallScreenHeight: false,
-        isExtraSmallScreenWidth: false,
-        isMediumScreenWidth: false,
-        onboardingIsMediumOrLargerScreenWidth: false,
-        isLargeScreenWidth: false,
-        isSmallScreen: true,
-    }),
-}));
-
-jest.mock('@libs/getPlatform', () => ({
-    __esModule: true,
-    default: () => 'web',
-}));
-
-jest.mock('@libs/ComponentUtils', () => ({
-    forceClearInput: jest.fn(),
-}));
-
-jest.mock('@hooks/useLocalize', () =>
-    jest.fn(() => ({
-        translate: jest.fn((key: string) => key),
-        numberFormat: jest.fn((num: number) => num.toString()),
-    })),
-);
-
-jest.mock('@hooks/usePaginatedReportActions', () => jest.fn(() => ({reportActions: [], hasNewerActions: false, hasOlderActions: false})));
-jest.mock('@hooks/useParentReportAction', () => jest.fn(() => null));
-jest.mock('@hooks/useReportTransactionsCollection', () => jest.fn(() => ({})));
-jest.mock('@hooks/useShortMentionsList', () => jest.fn(() => ({availableLoginsList: []})));
-jest.mock('@hooks/useSidePanelState', () => jest.fn(() => ({sessionStartTime: null})));
-
-jest.mock('@components/DropZone/DualDropZone', () => {
-    const RN = jest.requireActual<Record<string, React.ComponentType<{testID?: string; children?: React.ReactNode}>>>('react-native');
-    return ({shouldAcceptSingleReceipt}: {shouldAcceptSingleReceipt?: boolean}) => (
-        <RN.Text testID="dual-drop-zone">{shouldAcceptSingleReceipt ? 'receipt-editable' : 'receipt-not-editable'}</RN.Text>
-    );
-});
-
-const mockRouteReportID = {current: '1'};
-
-jest.mock('@react-navigation/native', () => ({
-    ...((): typeof NativeNavigation => {
-        return jest.requireActual('@react-navigation/native');
-    })(),
-    useNavigation: jest.fn(() => ({
-        navigate: jest.fn(),
-        addListener: jest.fn(() => jest.fn()),
-    })),
-    useIsFocused: jest.fn(() => true),
-    useRoute: jest.fn(() => ({key: '', name: '', params: {reportID: mockRouteReportID.current}})),
-}));
-
-TestHelper.setupGlobalFetchMock();
-
-const defaultReport = LHNTestUtils.getFakeReport();
-mockRouteReportID.current = defaultReport.reportID;
-
-const defaultProps: ReportActionComposeProps = {
-    reportID: defaultReport.reportID,
-};
-
-const commentAction = {
-    ...LHNTestUtils.getFakeReportAction(),
-    actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
-};
-
-function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
-    return <ReportActionEditMessageContextProvider reportID={defaultReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
-}
-
-function ReportScreenProviders({children}: PropsWithChildren) {
-    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
-}
-
-const saveReportActionDraftSpy = jest.spyOn(Report, 'saveReportActionDraft');
-
-function renderNarrowReportActionCompose() {
-    return render(
-        <ReportScreenProviders>
-            <ReportActionCompose
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...defaultProps}
-            />
-        </ReportScreenProviders>,
-    );
-}
-
-describe('ReportActionCompose — narrow layout message edit flow', () => {
-    beforeAll(() => {
-        Onyx.init({
-            keys: ONYXKEYS,
-            evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
-        });
-    });
-
-    beforeEach(() => {
-        jest.useFakeTimers();
-    });
-
-    afterEach(async () => {
-        jest.useRealTimers();
-        await act(async () => {
-            await Onyx.clear();
-        });
-        saveReportActionDraftSpy.mockClear();
-    });
-
-    it('shows edit chrome (close) instead of the create control while a report action draft is open', async () => {
-        await act(async () => {
-            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
-            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
-                [commentAction.reportActionID]: commentAction,
-            });
-            await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
-                [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
-                    [commentAction.reportActionID]: {message: 'Edit me in the composer'},
-                },
-            });
-        });
-        await waitForBatchedUpdatesWithAct();
-
-        renderNarrowReportActionCompose();
-        await waitForBatchedUpdatesWithAct();
-
-        expect(screen.getByLabelText('common.close')).toBeOnTheScreen();
-        expect(screen.queryByLabelText('common.create')).toBeNull();
-    });
-
-    it('loads the draft into the composer and debounces saveReportActionDraft while typing in edit mode', async () => {
-        await act(async () => {
-            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
-            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
-                [commentAction.reportActionID]: commentAction,
-            });
-            await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
-                [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
-                    [commentAction.reportActionID]: {message: 'Start'},
-                },
-            });
-        });
-        await waitForBatchedUpdatesWithAct();
-
-        renderNarrowReportActionCompose();
-        await waitForBatchedUpdatesWithAct();
-
-        const composer = screen.getByTestId('composer');
-        await waitFor(() => {
-            expect(composer.props.value).toBe('Start');
-        });
-
-        fireEvent.changeText(composer, 'Start, edited');
-
-        act(() => {
-            jest.advanceTimersByTime(1100);
-        });
-        await waitForBatchedUpdatesWithAct();
-
-        expect(saveReportActionDraftSpy).toHaveBeenCalled();
-        const lastCall = saveReportActionDraftSpy.mock.calls.at(-1);
-        expect(lastCall?.[0]).toBe(defaultReport.reportID);
-        expect(lastCall?.[2]).toBe('Start, edited');
-    });
-
-    it('leaves edit mode and restores default composer actions when the user cancels', async () => {
-        await act(async () => {
-            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
-            await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
-                [commentAction.reportActionID]: commentAction,
-            });
-            await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
-                [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
-                    [commentAction.reportActionID]: {message: 'To cancel'},
-                },
-            });
-        });
-        await waitForBatchedUpdatesWithAct();
-
-        renderNarrowReportActionCompose();
-        await waitForBatchedUpdatesWithAct();
-
-        const cancelButton = screen.getByLabelText('common.close');
-        fireEvent.press(cancelButton);
-        await waitForBatchedUpdatesWithAct();
-
-        await waitFor(() => {
-            expect(screen.getByLabelText('common.create')).toBeOnTheScreen();
-        });
-        expect(screen.queryByLabelText('common.close')).toBeNull();
-    });
-});
diff --git a/tests/ui/ReportActionMessageEditLayoutTest.tsx b/tests/ui/ReportActionMessageEditLayoutTest.tsx
new file mode 100644
index 000000000000..6466fde55607
--- /dev/null
+++ b/tests/ui/ReportActionMessageEditLayoutTest.tsx
@@ -0,0 +1,401 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import type * as NativeNavigation from '@react-navigation/native';
+import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react-native';
+import React from 'react';
+import type {PropsWithChildren} from 'react';
+import Onyx from 'react-native-onyx';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import type {ReportActionComposeProps} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import ReportActionCompose from '@pages/inbox/report/ReportActionCompose/ReportActionCompose';
+import {ReportActionEditMessageContextProvider} from '@pages/inbox/report/ReportActionEditMessageContext';
+import type {ReportActionItemMessageEditProps} from '@pages/inbox/report/ReportActionItemMessageEdit';
+import ReportActionItemMessageEdit from '@pages/inbox/report/ReportActionItemMessageEdit';
+import {draftMessageVideoAttributeCache} from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
+import * as TestHelper from '../utils/TestHelper';
+import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
+
+/**
+ * Exercises where message edit appears on narrow (main @ReportActionCompose) vs wide (@ReportActionItemMessageEdit),
+ * draft set/unset, and layout switching. TestIDs: CONST.COMPOSER.TEST_ID.*
+ */
+
+jest.mock('@hooks/useResponsiveLayout', () => ({
+    __esModule: true,
+    default: jest.fn(),
+}));
+
+const narrowLayout: ReturnType<typeof useResponsiveLayout> = {
+    shouldUseNarrowLayout: true,
+    isSmallScreenWidth: true,
+    isInNarrowPaneModal: false,
+    isExtraSmallScreenHeight: false,
+    isExtraSmallScreenWidth: false,
+    isMediumScreenWidth: false,
+    onboardingIsMediumOrLargerScreenWidth: false,
+    isLargeScreenWidth: false,
+    isSmallScreen: true,
+} as ReturnType<typeof useResponsiveLayout>;
+
+const wideLayout: ReturnType<typeof useResponsiveLayout> = {
+    shouldUseNarrowLayout: false,
+    isSmallScreenWidth: false,
+    isInNarrowPaneModal: false,
+    isExtraSmallScreenHeight: false,
+    isExtraSmallScreenWidth: false,
+    isMediumScreenWidth: false,
+    onboardingIsMediumOrLargerScreenWidth: true,
+    isLargeScreenWidth: true,
+    isSmallScreen: false,
+} as ReturnType<typeof useResponsiveLayout>;
+
+jest.mock('@libs/getPlatform', () => ({
+    __esModule: true,
+    default: () => 'web',
+}));
+
+jest.mock('@libs/ComponentUtils', () => ({
+    forceClearInput: jest.fn(),
+}));
+
+jest.mock('@hooks/useLocalize', () =>
+    jest.fn(() => ({
+        translate: jest.fn((key: string) => key),
+        numberFormat: jest.fn((num: number) => num.toString()),
+    })),
+);
+
+jest.mock('@hooks/usePaginatedReportActions', () => jest.fn(() => ({reportActions: [], hasNewerActions: false, hasOlderActions: false})));
+jest.mock('@hooks/useParentReportAction', () => jest.fn(() => null));
+jest.mock('@hooks/useReportTransactionsCollection', () => jest.fn(() => ({})));
+jest.mock('@hooks/useShortMentionsList', () => jest.fn(() => ({availableLoginsList: []})));
+jest.mock('@hooks/useSidePanelState', () => jest.fn(() => ({sessionStartTime: null})));
+jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}})));
+
+jest.mock('@libs/actions/Report', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@libs/actions/Report');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        editReportComment: jest.fn(),
+    };
+});
+
+jest.mock('@pages/inbox/report/ContextMenu/ReportActionContextMenu', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@pages/inbox/report/ContextMenu/ReportActionContextMenu');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        showDeleteModal: jest.fn(),
+    };
+});
+
+jest.mock('@components/DropZone/DualDropZone', () => {
+    const RN = jest.requireActual<Record<string, React.ComponentType<{testID?: string; children?: React.ReactNode}>>>('react-native');
+    return ({shouldAcceptSingleReceipt}: {shouldAcceptSingleReceipt?: boolean}) => (
+        <RN.Text testID="dual-drop-zone">{shouldAcceptSingleReceipt ? 'receipt-editable' : 'receipt-not-editable'}</RN.Text>
+    );
+});
+
+const mockRouteReportID = {current: '1'};
+
+jest.mock('@react-navigation/native', () => ({
+    ...((): typeof NativeNavigation => {
+        return jest.requireActual('@react-navigation/native');
+    })(),
+    useNavigation: jest.fn(() => ({
+        navigate: jest.fn(),
+        addListener: jest.fn(() => jest.fn()),
+    })),
+    useIsFocused: jest.fn(() => true),
+    useRoute: jest.fn(() => ({key: '', name: '', params: {reportID: mockRouteReportID.current}})),
+}));
+
+TestHelper.setupGlobalFetchMock();
+
+const mockUseResponsiveLayout = jest.mocked(useResponsiveLayout);
+
+const defaultReport = LHNTestUtils.getFakeReport();
+mockRouteReportID.current = defaultReport.reportID;
+
+const defaultProps: ReportActionComposeProps = {
+    reportID: defaultReport.reportID,
+};
+
+const commentAction: ReportActionItemMessageEditProps['action'] = {
+    ...LHNTestUtils.getFakeReportAction(),
+    actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
+};
+
+const testIds = CONST.COMPOSER.TEST_ID;
+
+function ReportActionEditMessageContextProviderForReport({children}: PropsWithChildren) {
+    return <ReportActionEditMessageContextProvider reportID={defaultReport.reportID}>{children}</ReportActionEditMessageContextProvider>;
+}
+
+function ReportScreenProviders({children}: PropsWithChildren) {
+    return <ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, KeyboardStateProvider, ReportActionEditMessageContextProviderForReport]}>{children}</ComposeProviders>;
+}
+
+/**
+ * Simulates the product split: on wide, inline @ReportActionItemMessageEdit is mounted (isEditingInline in PureReportActionItem);
+ * on narrow it is not and edit happens in the main composer.
+ */
+type LayoutMode = 'narrow' | 'wide';
+function MessageEditLayoutHost({layout}: {layout: LayoutMode}) {
+    const isWide = layout === 'wide';
+    return (
+        <ReportScreenProviders>
+            <ReportActionCompose
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultProps}
+            />
+            {isWide && (
+                <ReportActionItemMessageEdit
+                    action={commentAction}
+                    reportID={defaultReport.reportID}
+                    originalReportID={defaultReport.reportID}
+                    index={0}
+                    isGroupPolicyReport={false}
+                />
+            )}
+        </ReportScreenProviders>
+    );
+}
+
+async function seedReportAndActions() {
+    await act(async () => {
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${defaultReport.reportID}`, defaultReport);
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${defaultReport.reportID}`, {
+            [commentAction.reportActionID]: commentAction,
+        });
+    });
+}
+
+async function setReportActionDraftWithMessage(message: string) {
+    await act(async () => {
+        await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {
+            [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`]: {
+                [commentAction.reportActionID]: {message},
+            },
+        });
+    });
+}
+
+async function clearReportActionDraftsForReport() {
+    await act(async () => {
+        await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${defaultReport.reportID}`, {});
+    });
+}
+
+function renderNarrowMessageCompose() {
+    mockUseResponsiveLayout.mockReturnValue(narrowLayout);
+    return render(
+        <ReportScreenProviders>
+            <ReportActionCompose
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...defaultProps}
+            />
+        </ReportScreenProviders>,
+    );
+}
+
+describe('ReportActionMessageEdit layout and draft (narrow vs wide)', () => {
+    beforeAll(() => {
+        Onyx.init({
+            keys: ONYXKEYS,
+            evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+        });
+    });
+
+    beforeEach(() => {
+        mockUseResponsiveLayout.mockReturnValue(narrowLayout);
+        jest.useFakeTimers();
+    });
+
+    afterEach(async () => {
+        jest.useRealTimers();
+        await act(async () => {
+            await Onyx.clear();
+        });
+        draftMessageVideoAttributeCache.clear();
+    });
+
+    it('with no report-action draft, main composer is in normal draft message mode (not message-edit action row)', async () => {
+        await seedReportAndActions();
+        await waitForBatchedUpdatesWithAct();
+
+        renderNarrowMessageCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.getByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(screen.queryByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeNull();
+    });
+
+    it('when a report-action draft is set on narrow, main composer enters message edit mode and edit-mode test IDs are used', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Narrow body');
+        await waitForBatchedUpdatesWithAct();
+
+        renderNarrowMessageCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.getByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(screen.queryByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeNull();
+        expect(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER)).toBeOnTheScreen();
+        const mainCompose = screen.getByTestId(testIds.REPORT_ACTION_COMPOSE);
+        expect(within(mainCompose).getByTestId(CONST.COMPOSER.NATIVE_ID).props.value).toBe('Narrow body');
+    });
+
+    it('when the draft is cleared, message edit mode ends and normal draft action row returns', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Then remove');
+        await waitForBatchedUpdatesWithAct();
+
+        const {unmount} = renderNarrowMessageCompose();
+        await waitForBatchedUpdatesWithAct();
+        expect(screen.getByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+
+        await clearReportActionDraftsForReport();
+        await waitForBatchedUpdatesWithAct();
+
+        unmount();
+        renderNarrowMessageCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.getByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(screen.queryByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeNull();
+    });
+
+    it('on wide, main composer stays in normal action row while the inline @ReportActionItemMessageEdit is used', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Wide inline');
+        await waitForBatchedUpdatesWithAct();
+
+        mockUseResponsiveLayout.mockReturnValue(wideLayout);
+        render(<MessageEditLayoutHost layout="wide" />);
+        await waitForBatchedUpdatesWithAct();
+
+        const mainRoot = screen.getByTestId(testIds.REPORT_ACTION_COMPOSE);
+        expect(within(mainRoot).getByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(within(mainRoot).queryByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeNull();
+
+        expect(screen.getByTestId(testIds.REPORT_ACTION_ITEM_MESSAGE_EDIT)).toBeOnTheScreen();
+        expect(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_INLINE)).toBeOnTheScreen();
+        expect(screen.queryByTestId(testIds.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER)).toBeNull();
+    });
+
+    it('switches the editing surface from inline (wide) to main composer (narrow) when layout becomes narrow', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Shared draft');
+        await waitForBatchedUpdatesWithAct();
+
+        mockUseResponsiveLayout.mockReturnValue(wideLayout);
+        const {unmount} = render(
+            <MessageEditLayoutHost
+                key="msg-edit-layout-1"
+                layout="wide"
+            />,
+        );
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.getByTestId(testIds.REPORT_ACTION_ITEM_MESSAGE_EDIT)).toBeOnTheScreen();
+        expect(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_INLINE)).toBeOnTheScreen();
+        const mainWide = screen.getByTestId(testIds.REPORT_ACTION_COMPOSE);
+        expect(within(mainWide).getByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+
+        unmount();
+        mockUseResponsiveLayout.mockReturnValue(narrowLayout);
+        render(
+            <MessageEditLayoutHost
+                key="msg-edit-layout-2"
+                layout="narrow"
+            />,
+        );
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.queryByTestId(testIds.REPORT_ACTION_ITEM_MESSAGE_EDIT)).toBeNull();
+        expect(screen.queryByTestId(testIds.MESSAGE_EDIT_CANCEL_INLINE)).toBeNull();
+        expect(screen.getByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER)).toBeOnTheScreen();
+    });
+
+    it('switches the editing surface from main composer (narrow) to inline (wide) when layout becomes wide', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Back to wide');
+        await waitForBatchedUpdatesWithAct();
+
+        mockUseResponsiveLayout.mockReturnValue(narrowLayout);
+        const {unmount} = render(
+            <MessageEditLayoutHost
+                key="msg-edit-layout-3"
+                layout="narrow"
+            />,
+        );
+        await waitForBatchedUpdatesWithAct();
+
+        expect(screen.getByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER)).toBeOnTheScreen();
+        expect(screen.queryByTestId(testIds.REPORT_ACTION_ITEM_MESSAGE_EDIT)).toBeNull();
+
+        unmount();
+        mockUseResponsiveLayout.mockReturnValue(wideLayout);
+        render(
+            <MessageEditLayoutHost
+                key="msg-edit-layout-4"
+                layout="wide"
+            />,
+        );
+        await waitForBatchedUpdatesWithAct();
+
+        const main = screen.getByTestId(testIds.REPORT_ACTION_COMPOSE);
+        expect(within(main).getByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        expect(within(main).queryByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeNull();
+        expect(screen.getByTestId(testIds.REPORT_ACTION_ITEM_MESSAGE_EDIT)).toBeOnTheScreen();
+        expect(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_INLINE)).toBeOnTheScreen();
+    });
+
+    it('in narrow message-edit-in-composer mode, updateComment keeps the main composer value in sync (editingState + shouldUseNarrowLayout branch in ComposerWithSuggestions)', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Start');
+        await waitForBatchedUpdatesWithAct();
+
+        mockUseResponsiveLayout.mockReturnValue(narrowLayout);
+        renderNarrowMessageCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        const mainRoot = screen.getByTestId(testIds.REPORT_ACTION_COMPOSE);
+        const composer = within(mainRoot).getByTestId(CONST.COMPOSER.NATIVE_ID);
+        expect(screen.getByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+
+        fireEvent.changeText(composer, 'Start, edited');
+        await waitFor(() => {
+            expect(within(mainRoot).getByTestId(CONST.COMPOSER.NATIVE_ID).props.value).toBe('Start, edited');
+        });
+    });
+
+    it('cancel in narrow main composer returns to normal draft action row', async () => {
+        await seedReportAndActions();
+        await setReportActionDraftWithMessage('Cancel me');
+        await waitForBatchedUpdatesWithAct();
+
+        renderNarrowMessageCompose();
+        await waitForBatchedUpdatesWithAct();
+
+        fireEvent.press(screen.getByTestId(testIds.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER));
+        await waitForBatchedUpdatesWithAct();
+
+        await waitFor(() => {
+            expect(screen.getByTestId(testIds.DRAFT_MESSAGE_ACTION_ROW)).toBeOnTheScreen();
+        });
+        expect(screen.queryByTestId(testIds.EDITING_MESSAGE_ACTION_ROW)).toBeNull();
+    });
+});

From 108f842747aeb58c3ac2f4ac4a5df387b0d49d9e Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 15:38:36 +0100
Subject: [PATCH 202/233] fix: move testID to pressable

---
 .../report/ReportActionCompose/MessageEditCancelButton.tsx     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
index 356e1eea4590..7a8758897afc 100644
--- a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
@@ -15,7 +15,7 @@ type MessageEditCancelButtonProps = ViewProps & {
     testID?: string;
 };
 
-function MessageEditCancelButton({onCancel, ...restProps}: MessageEditCancelButtonProps) {
+function MessageEditCancelButton({onCancel, testID, ...restProps}: MessageEditCancelButtonProps) {
     const styles = useThemeStyles();
     const theme = useTheme();
     const {translate} = useLocalize();
@@ -28,6 +28,7 @@ function MessageEditCancelButton({onCancel, ...restProps}: MessageEditCancelButt
         <View {...restProps}>
             <Tooltip text={translate('common.cancel')}>
                 <PressableWithFeedback
+                    testID={testID}
                     onPress={onCancel}
                     style={closeButtonStyles}
                     role={CONST.ROLE.BUTTON}

From 2289cd55b86ea8ca91ecd9f0bcc3704f2b04c7d5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 22 Apr 2026 19:53:23 +0100
Subject: [PATCH 203/233] fix: test mock type errors

---
 tests/unit/hooks/useEditComposerToggleTest.ts | 27 +++++++------------
 1 file changed, 10 insertions(+), 17 deletions(-)

diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
index 170552fe8ff7..f67d3ace4aaa 100644
--- a/tests/unit/hooks/useEditComposerToggleTest.ts
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -2,6 +2,8 @@
 import {renderHook} from '@testing-library/react-native';
 import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
 import ReportActionComposeUtils from '@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils';
 import useEditComposerToggle from '@pages/inbox/report/ReportActionCompose/useEditComposerToggle';
 import type {ReportActionEditMessageContextValue} from '@pages/inbox/report/ReportActionEditMessageContext';
@@ -17,20 +19,7 @@ jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => {
     };
 });
 
-jest.mock('@hooks/useResponsiveLayout', () => ({
-    __esModule: true,
-    default: jest.fn(() => ({
-        shouldUseNarrowLayout: true,
-        isSmallScreenWidth: true,
-        isInNarrowPaneModal: false,
-        isExtraSmallScreenHeight: false,
-        isExtraSmallScreenWidth: false,
-        isMediumScreenWidth: false,
-        onboardingIsMediumOrLargerScreenWidth: false,
-        isLargeScreenWidth: false,
-        isSmallScreen: true,
-    })),
-}));
+jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
 
 jest.mock('@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils', () => ({
     __esModule: true,
@@ -45,7 +34,7 @@ jest.mock('@libs/getPlatform', () => ({
 }));
 
 const mockUseReportActionActiveEdit = jest.mocked(ReportActionEditMessageContext.useReportActionActiveEdit);
-const mockUseResponsiveLayout = jest.requireMock('@hooks/useResponsiveLayout').default as jest.Mock;
+const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction<typeof useResponsiveLayout>;
 const mockUpdateNativeSelectionValue = jest.mocked(ReportActionComposeUtils.updateNativeSelectionValue);
 
 type ActiveEdit = ReportActionEditMessageContextValue & {editingState: 'off' | 'editing' | 'submitted'};
@@ -75,7 +64,7 @@ function defaultActiveEdit(overrides?: Partial<ActiveEdit>): ActiveEdit {
     };
 }
 
-function wideLayoutResult() {
+function wideLayoutResult(): ResponsiveLayoutResult {
     return {
         shouldUseNarrowLayout: false,
         isSmallScreenWidth: false,
@@ -85,11 +74,13 @@ function wideLayoutResult() {
         isMediumScreenWidth: false,
         onboardingIsMediumOrLargerScreenWidth: true,
         isLargeScreenWidth: true,
+        isExtraLargeScreenWidth: false,
         isSmallScreen: false,
+        isInLandscapeMode: false,
     };
 }
 
-function narrowLayoutResult() {
+function narrowLayoutResult(): ResponsiveLayoutResult {
     return {
         shouldUseNarrowLayout: true,
         isSmallScreenWidth: true,
@@ -99,7 +90,9 @@ function narrowLayoutResult() {
         isMediumScreenWidth: false,
         onboardingIsMediumOrLargerScreenWidth: false,
         isLargeScreenWidth: false,
+        isExtraLargeScreenWidth: false,
         isSmallScreen: true,
+        isInLandscapeMode: false,
     };
 }
 

From 3fd68a798b94e875189d24311d4aefbca233abf2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 23 Apr 2026 11:42:22 +0100
Subject: [PATCH 204/233] fix: `clearComposer` callback invalid use of
 `scheduleOnUI`

---
 .../inbox/report/ReportActionCompose/ComposerProvider.tsx | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index b7f2c5cc1d57..6c670c0102ef 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -1,6 +1,5 @@
 import React, {useEffect, useRef, useState} from 'react';
 import type {View} from 'react-native';
-import {useSharedValue} from 'react-native-reanimated';
 import {scheduleOnUI} from 'react-native-worklets';
 import useOnyx from '@hooks/useOnyx';
 import useOriginalReportID from '@hooks/useOriginalReportID';
@@ -62,8 +61,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const actionButtonRef = useRef<View | HTMLDivElement | null>(null);
     const attachmentFileRef = useRef<FileObject | FileObject[] | null>(null);
 
-    const composerRefShared = useSharedValue<Partial<ComposerWithSuggestionsRef>>({});
-
     const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
@@ -106,7 +103,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     });
 
     const clearComposer = () => {
-        const clearWorklet = composerRefShared.get().clearWorklet;
+        const clearWorklet = composerRef.current?.clearWorklet;
         if (!clearWorklet) {
             throw new Error('The composerRef.clearWorklet function is not set yet. This should never happen, and indicates a developer error.');
         }
@@ -115,9 +112,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
 
     const setComposerRef = (ref: ComposerWithSuggestionsRef | null) => {
         composerRef.current = ref;
-        composerRefShared.set({
-            clearWorklet: ref?.clearWorklet,
-        });
     };
 
     const composerState = {

From 28f9b824804398ad1b8e2dcd50b4f1864d3e5812 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Fri, 24 Apr 2026 19:03:36 +0100
Subject: [PATCH 205/233] fix: forward expand/collapse button style

---
 .../report/ReportActionCompose/ComposerEditingButtons.tsx  | 7 ++++++-
 .../ReportActionCompose/ComposerExpandCollapseButton.tsx   | 7 +++++--
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
index b5060f393c42..7d627e50653d 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
@@ -22,12 +22,17 @@ function ComposerEditingButtons({reportID}: ComposerEditingButtonsProps) {
         styles.justifyContentCenter,
         {paddingVertical: styles.composerSizeButton.marginHorizontal},
     ];
+    const expandCollapseComposerButtonStyles = [styles.flexGrow1, styles.flexShrink0, {marginRight: styles.composerSizeButton.marginHorizontal}];
+
     return (
         <View
             testID={CONST.COMPOSER.TEST_ID.EDITING_MESSAGE_ACTION_ROW}
             style={editingButtonsContainerStyles}
         >
-            <ComposerExpandCollapseButton reportID={reportID} />
+            <ComposerExpandCollapseButton
+                reportID={reportID}
+                style={expandCollapseComposerButtonStyles}
+            />
             <MessageEditCancelButton
                 onCancel={deleteDraft}
                 testID={CONST.COMPOSER.TEST_ID.MESSAGE_EDIT_CANCEL_MAIN_COMPOSER}
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
index c45140e1c043..7d55275f0100 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import type {ViewProps} from 'react-native';
 import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered';
 import useOnyx from '@hooks/useOnyx';
 import {setIsComposerFullSize} from '@userActions/Report';
@@ -6,11 +7,11 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import {useComposerSendState, useComposerState} from './ComposerContext';
 import ExpandCollapseButton from './ExpandCollapseButton';
 
-type ComposerExpandCollapseButtonProps = {
+type ComposerExpandCollapseButtonProps = ViewProps & {
     reportID: string;
 };
 
-function ComposerExpandCollapseButton({reportID}: ComposerExpandCollapseButtonProps) {
+function ComposerExpandCollapseButton({reportID, ...restProps}: ComposerExpandCollapseButtonProps) {
     const {isBlockedFromConcierge} = useComposerSendState();
     const {raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
@@ -25,6 +26,8 @@ function ComposerExpandCollapseButton({reportID}: ComposerExpandCollapseButtonPr
             raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLayoutTriggered}
             setIsComposerFullSize={setIsComposerFullSize}
             disabled={isBlockedFromConcierge}
+            // eslint-disable-next-line react/jsx-props-no-spreading
+            {...restProps}
         />
     );
 }

From aa394b76c919c253c103133a7f4425dc7246668c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 28 Apr 2026 09:34:28 +0100
Subject: [PATCH 206/233] fix: remove `canEvict` `useOnyx` flag

---
 src/pages/inbox/report/ReportActionEditMessageContext.tsx | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 5f745ff1e458..fdb842952c74 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -56,9 +56,7 @@ type ReportActionEditMessageContextProviderProps = {
 
 function ReportActionEditMessageContextProvider({reportID, children}: ReportActionEditMessageContextProviderProps) {
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
-    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
-        canEvict: false,
-    });
+    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`);
 
     const ancestors = useAncestors(report, shouldExcludeAncestorReportAction);
 

From 9117a243f185727871b0c0079fb10030b876448f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 11:31:19 +0100
Subject: [PATCH 207/233] refactor: use `useComposerEditState` instead of
 `useReportActionActiveEdit`

---
 .../ReportActionCompose/ComposerContext.ts      | 14 +++++++++++---
 .../ReportActionCompose/ComposerProvider.tsx    |  3 ++-
 .../ComposerWithSuggestions.tsx                 |  5 +++--
 .../useEditComposerToggle.ts                    | 15 ++++++++++++---
 .../report/ReportActionEditMessageContext.tsx   |  2 +-
 tests/unit/hooks/useEditComposerToggleTest.ts   | 17 +++++++++--------
 6 files changed, 38 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
index 225e25c736ab..d6bebf43a981 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
@@ -2,7 +2,9 @@ import type {RefObject} from 'react';
 import {createContext, useContext} from 'react';
 import type {BlurEvent, TextInputSelectionChangeEvent, View} from 'react-native';
 import type {Emoji} from '@assets/emojis/types';
+import type {TextSelection} from '@components/Composer/types';
 import type {Mention} from '@components/MentionSuggestions';
+import type {ReportAction} from '@src/types/onyx';
 import type {FileObject} from '@src/types/utils/Attachment';
 import type {ComposerWithSuggestionsRef} from './ComposerWithSuggestions';
 import type useDebouncedCommentMaxLengthValidation from './useDebouncedCommentMaxLengthValidation';
@@ -29,12 +31,15 @@ type ComposerState = {
 };
 
 type ComposerEditState = {
+    editingState: 'off' | 'editing' | 'submitted';
     isEditingInComposer: boolean;
+    editingReportID: string | null;
     editingReportActionID: string | null;
+    editingReportAction: ReportAction | null;
     editingMessage: string | null;
-    editingState: 'off' | 'editing' | 'submitted';
     draftComment: string | null | undefined;
     effectiveDraft: string | null | undefined;
+    currentEditMessageSelection: TextSelection | null;
 };
 
 // Warm — changes based on content + policy
@@ -99,12 +104,15 @@ const defaultSendState: ComposerSendState = {
 };
 
 const defaultEditState: ComposerEditState = {
+    editingState: 'off',
     isEditingInComposer: false,
+    editingReportID: null,
     editingReportActionID: null,
+    editingReportAction: null,
     editingMessage: null,
-    editingState: 'off',
     draftComment: undefined,
     effectiveDraft: undefined,
+    currentEditMessageSelection: null,
 };
 const ComposerEditStateContext = createContext<ComposerEditState>(defaultEditState);
 
@@ -181,4 +189,4 @@ export {
     useComposerEditActions,
     useComposerMeta,
 };
-export type {SuggestionsRef, ComposerText, ComposerState, ComposerSendState, ComposerActions, ComposerMeta};
+export type {SuggestionsRef, ComposerText, ComposerState, ComposerEditState, ComposerSendState, ComposerActions, ComposerMeta};
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index 6c670c0102ef..6d41cff7335c 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -61,7 +61,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const actionButtonRef = useRef<View | HTMLDivElement | null>(null);
     const attachmentFileRef = useRef<FileObject | FileObject[] | null>(null);
 
-    const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage} = useReportActionActiveEdit();
+    const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
     useEffect(() => {
@@ -122,6 +122,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     };
 
     const composerEditState = {
+        editingState,
         isEditingInComposer,
         editingReportActionID,
         editingMessage,
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 4bd63c834c09..f13506e1076e 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -48,7 +48,7 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag
 import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUtils';
 import updateMultilineInputRange from '@libs/updateMultilineInputRange';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
-import {useReportActionActiveEdit, useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext';
+import {useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext';
 import useDraftMessageVideoAttributeCache from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
@@ -63,6 +63,7 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
+import {useComposerEditState} from './ComposerContext';
 import getCursorPosition from './getCursorPosition';
 import getScrollPosition from './getScrollPosition';
 import type {SuggestionsRef} from './ReportActionCompose';
@@ -260,7 +261,7 @@ function ComposerWithSuggestions({
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingState, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
+    const {editingState, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useComposerEditState();
     const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
 
     const isEditing = editingState !== 'off';
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index ffcad92cdfe5..d233c3285f46 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -4,8 +4,8 @@ import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import getPlatform from '@libs/getPlatform';
-import {useReportActionActiveEdit} from '@pages/inbox/report/ReportActionEditMessageContext';
 import CONST from '@src/CONST';
+import {useComposerEditState} from './ComposerContext';
 import ReportActionComposeUtils from './ReportActionComposeUtils';
 
 const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
@@ -29,7 +29,7 @@ type UseEditComposerToggleProps = {
 function useEditComposerToggle({selection, draftComment, composerRef, onFocus, onValueChange, onSelectionChange}: UseEditComposerToggleProps) {
     const {shouldUseNarrowLayout} = useResponsiveLayout();
 
-    const {editingState, editingReportActionID, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
+    const {editingState, editingReportActionID, editingMessage, currentEditMessageSelection} = useComposerEditState();
     const isEditing = editingState !== 'off';
 
     const wasEditingRef = useRef(isEditing);
@@ -134,7 +134,16 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
         }
 
         previousEditingReportActionIDRef.current = editingReportActionID;
-    }, [applyComposerValue, composerRef, draftComment, editingMessage, editingReportActionID, editingState, selection, shouldUseNarrowLayout]);
+    }, [
+        applyComposerValue,
+        composerRef,
+        draftComment,
+        editingMessage,
+        editingReportActionID,
+        editingState,
+        selection,
+        shouldUseNarrowLayout,
+    ]);
 }
 
 export default useEditComposerToggle;
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index fdb842952c74..2493731e4eb2 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -34,12 +34,12 @@ type ReportActionEditMessageContextActions = {
 };
 
 const ReportActionEditMessageContext = createContext<ReportActionEditMessageContextValue>({
+    editingState: 'off',
     editingReportID: null,
     editingReportActionID: null,
     editingReportAction: null,
     editingMessage: null,
     currentEditMessageSelection: null,
-    editingState: 'off',
 });
 
 const ReportActionEditMessageActionsContext = createContext<ReportActionEditMessageContextActions>({
diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
index f67d3ace4aaa..8da04140c803 100644
--- a/tests/unit/hooks/useEditComposerToggleTest.ts
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -4,10 +4,10 @@ import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
+import * as ComposerContext from '@pages/inbox/report/ReportActionCompose/ComposerContext';
+import type {ComposerEditState} from '@pages/inbox/report/ReportActionCompose/ComposerContext';
 import ReportActionComposeUtils from '@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils';
 import useEditComposerToggle from '@pages/inbox/report/ReportActionCompose/useEditComposerToggle';
-import type {ReportActionEditMessageContextValue} from '@pages/inbox/report/ReportActionEditMessageContext';
-import * as ReportActionEditMessageContext from '@pages/inbox/report/ReportActionEditMessageContext';
 
 jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => {
     // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -33,12 +33,10 @@ jest.mock('@libs/getPlatform', () => ({
     default: () => 'web',
 }));
 
-const mockUseReportActionActiveEdit = jest.mocked(ReportActionEditMessageContext.useReportActionActiveEdit);
+const mockUseComposerEditState = jest.mocked(ComposerContext.useComposerEditState);
 const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction<typeof useResponsiveLayout>;
 const mockUpdateNativeSelectionValue = jest.mocked(ReportActionComposeUtils.updateNativeSelectionValue);
 
-type ActiveEdit = ReportActionEditMessageContextValue & {editingState: 'off' | 'editing' | 'submitted'};
-
 function makeComposerRef(overrides?: Partial<ComposerRef>): RefObject<ComposerRef | null> {
     return {
         current: {
@@ -52,14 +50,17 @@ function makeComposerRef(overrides?: Partial<ComposerRef>): RefObject<ComposerRe
     };
 }
 
-function defaultActiveEdit(overrides?: Partial<ActiveEdit>): ActiveEdit {
+function defaultActiveEdit(overrides?: Partial<ComposerEditState>): ComposerEditState {
     return {
+        editingState: 'off',
+        isEditingInComposer: false,
         editingReportID: null,
         editingReportActionID: null,
         editingReportAction: null,
         editingMessage: null,
         currentEditMessageSelection: null,
-        editingState: 'off',
+        draftComment: undefined,
+        effectiveDraft: undefined,
         ...overrides,
     };
 }
@@ -102,7 +103,7 @@ describe('useEditComposerToggle', () => {
     beforeEach(() => {
         jest.clearAllMocks();
         activeEditRef.current = defaultActiveEdit();
-        mockUseReportActionActiveEdit.mockImplementation(() => activeEditRef.current);
+        mockUseComposerEditState.mockImplementation(() => activeEditRef.current);
         mockUseResponsiveLayout.mockReturnValue(narrowLayoutResult());
     });
 

From 83c613d8803a677db6b6db69a353d18862301e4a Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 12:23:36 +0100
Subject: [PATCH 208/233] refactor: move `didResetComposerHeight` reset call to
 `useEditComposerToggle`

---
 .../report/ReportActionCompose/ComposerProvider.tsx | 13 ++++---------
 .../ReportActionCompose/useEditComposerToggle.ts    | 11 +++++++++--
 2 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index 6d41cff7335c..bea30aaddee9 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useRef, useState} from 'react';
 import type {View} from 'react-native';
 import {scheduleOnUI} from 'react-native-worklets';
 import useOnyx from '@hooks/useOnyx';
@@ -64,13 +64,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const {editingState, editingReportID, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useReportActionActiveEdit();
 
     const [didResetComposerHeight, setDidResetComposerHeight] = useState(false);
-    useEffect(() => {
-        if (editingState !== 'off' || !didResetComposerHeight) {
-            return;
-        }
-
-        setDidResetComposerHeight(false);
-    }, [didResetComposerHeight, editingState]);
 
     const isEditingInComposer = shouldUseNarrowLayout && editingState !== 'off' && !didResetComposerHeight;
     const effectiveDraft = isEditingInComposer ? editingMessage : draftComment;
@@ -124,11 +117,13 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
     const composerEditState = {
         editingState,
         isEditingInComposer,
+        editingReportID,
         editingReportActionID,
+        editingReportAction,
         editingMessage,
-        editingState,
         draftComment,
         effectiveDraft,
+        currentEditMessageSelection,
     };
 
     const composerSendState = {
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index d233c3285f46..3f3d9dac3bc7 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -5,7 +5,7 @@ import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import getPlatform from '@libs/getPlatform';
 import CONST from '@src/CONST';
-import {useComposerEditState} from './ComposerContext';
+import {useComposerActions, useComposerEditState} from './ComposerContext';
 import ReportActionComposeUtils from './ReportActionComposeUtils';
 
 const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
@@ -29,7 +29,8 @@ type UseEditComposerToggleProps = {
 function useEditComposerToggle({selection, draftComment, composerRef, onFocus, onValueChange, onSelectionChange}: UseEditComposerToggleProps) {
     const {shouldUseNarrowLayout} = useResponsiveLayout();
 
-    const {editingState, editingReportActionID, editingMessage, currentEditMessageSelection} = useComposerEditState();
+    const {isEditingInComposer, editingState, editingReportActionID, editingMessage, currentEditMessageSelection} = useComposerEditState();
+    const {setDidResetComposerHeight} = useComposerActions();
     const isEditing = editingState !== 'off';
 
     const wasEditingRef = useRef(isEditing);
@@ -83,6 +84,10 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
                 applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
 
+                if (isEditingInComposer) {
+                    setDidResetComposerHeight(true);
+                }
+
                 if (!wasComposerFocusedBeforeEditingRef.current) {
                     composerRef.current?.blur();
                 }
@@ -141,7 +146,9 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
         editingMessage,
         editingReportActionID,
         editingState,
+        isEditingInComposer,
         selection,
+        setDidResetComposerHeight,
         shouldUseNarrowLayout,
     ]);
 }

From 3421d13b50153109e35fd8f1a7801a6d22c8aab7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 13:05:02 +0100
Subject: [PATCH 209/233] refactor: remove
 `InteractionManager.runAfterInteractions`

---
 .../report/ReportActionCompose/ReportActionComposeUtils.ts    | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
index a397e4bd94bd..dc89ef29d741 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
@@ -1,4 +1,3 @@
-import {InteractionManager} from 'react-native';
 import type {ComposerRef} from '@components/Composer/types';
 import getPlatform from '@libs/getPlatform';
 import CONST from '@src/CONST';
@@ -11,8 +10,7 @@ const updateNativeSelectionValue = (composerRef: React.RefObject<ComposerRef | n
     }
 
     // Ensure that native selection value is set imperatively after all state changes are effective
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    InteractionManager.runAfterInteractions(() => {
+    requestIdleCallback(() => {
         // note: this implementation is only available on non-web RN, thus the wrapping
         // 'if' block contains a redundant (since the ref is only used on iOS) platform check
         composerRef.current?.setSelection(start, end);

From 30f26fe469fa414384744397487d4c55f6a4c621 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 13:05:26 +0100
Subject: [PATCH 210/233] refactor: remove manual memoization from
 `useDebounce`

---
 src/hooks/useDebounce.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts
index a5438785295a..639f917d7abe 100644
--- a/src/hooks/useDebounce.ts
+++ b/src/hooks/useDebounce.ts
@@ -1,7 +1,7 @@
 // eslint-disable-next-line lodash/import-scope
 import type {DebouncedFunc, DebounceSettings} from 'lodash';
 import lodashDebounce from 'lodash/debounce';
-import {useCallback, useEffect, useRef} from 'react';
+import {useEffect, useRef} from 'react';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 type GenericFunction = (...args: any[]) => void;
@@ -34,13 +34,13 @@ export default function useDebounce<T extends GenericFunction>(func: T, wait: nu
         };
     }, [func, wait, leading, maxWait, trailing]);
 
-    const debounceCallback = useCallback((...args: Parameters<T>) => {
+    const debounceCallback = (...args: Parameters<T>) => {
         const debouncedFn = debouncedFnRef.current;
 
         if (debouncedFn) {
             debouncedFn(...args);
         }
-    }, []);
+    };
 
     return debounceCallback as T;
 }

From 6dede483bda17b3dde38b4e113bc89d0a13e40fb Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 13:07:40 +0100
Subject: [PATCH 211/233] fix: callback memoization not able to get preserved
 (RC)

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index f13506e1076e..3476af206c73 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -485,7 +485,7 @@ function ComposerWithSuggestions({
             // previous text before change
             const prevText = lastTextRef.current;
             // snapshot selection (should be the selection that was active just before the paste/change)
-            const prevSelectionStart = selection?.start ?? 0;
+            const prevSelectionStart = selection.start ?? 0;
             const prevSelectionEnd = selection?.end ?? 0;
 
             // detect newly added text (existing helper)
@@ -578,7 +578,7 @@ function ComposerWithSuggestions({
             suggestionsRef,
             raiseIsScrollLikelyLayoutTriggered,
             selection.end,
-            selection?.start,
+            selection.start,
             setCurrentEditMessageSelection,
             setEditingMessage,
             shouldUseNarrowLayout,

From 1eb90fd5b3115910438d9e509212c55b6dfefd1f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 13:13:50 +0100
Subject: [PATCH 212/233] refactor: extract debounced save functions to
 separate hook

---
 .../ComposerWithSuggestions.tsx               | 64 +++++--------------
 .../report/ReportActionItemMessageEdit.tsx    | 37 +++--------
 .../inbox/report/useDebouncedSaveDraft.ts     | 38 +++++++++++
 3 files changed, 63 insertions(+), 76 deletions(-)
 create mode 100644 src/pages/inbox/report/useDebouncedSaveDraft.ts

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 3476af206c73..d3a3ba9c14b7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -1,7 +1,7 @@
 import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
 import lodashDebounce from 'lodash/debounce';
 import type {Ref, RefObject} from 'react';
-import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
 import type {
     BlurEvent,
     LayoutChangeEvent,
@@ -49,6 +49,7 @@ import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUt
 import updateMultilineInputRange from '@libs/updateMultilineInputRange';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
 import {useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext';
+import useDebouncedSaveDraft from '@pages/inbox/report/useDebouncedSaveDraft';
 import useDraftMessageVideoAttributeCache from '@pages/inbox/report/useDraftMessageVideoAttributeCache';
 import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
@@ -275,15 +276,19 @@ function ComposerWithSuggestions({
         return initialValue;
     });
 
-    // The ref to check whether the comment saving is in progress
-    const isDraftPendingSaved = useRef(false);
+    // Save the draft of the report action. This debounced so that we're not ceaselessly saving your edit.
+    const {saveDraft: debouncedSaveReportActionDraft, isSavePending: isDraftSavePending} = useDebouncedSaveDraft(saveReportActionDraft);
+
+    // Save the draft of the report comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
+    // allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
+    const {saveDraft: debouncedSaveComment, isSavePending: isCommentSavePending} = useDebouncedSaveDraft(saveReportDraftComment);
 
     useDraftMessageVideoAttributeCache({
         draftMessage: value,
         isEditing,
         editingReportAction,
         updateDraftMessage: setValue,
-        isEditInProgressRef: isDraftPendingSaved,
+        isEditInProgressRef: isDraftSavePending,
     });
 
     const [selection, setSelection] = useState<TextSelection>(() => currentEditMessageSelection ?? {start: value.length, end: value.length});
@@ -383,40 +388,6 @@ function ComposerWithSuggestions({
         RNTextInputReset.resetKeyboardInput(CONST.COMPOSER.NATIVE_ID);
     }, []);
 
-    /**
-     * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
-     * allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
-     * @param {String} newDraft
-     */
-    const debouncedSaveDraft = useMemo(
-        () =>
-            lodashDebounce((newDraft: string) => {
-                saveReportActionDraft(reportID, editingReportAction, newDraft);
-                isDraftPendingSaved.current = false;
-            }, 1000),
-        [reportID, editingReportAction],
-    );
-
-    useEffect(
-        () => () => {
-            debouncedSaveDraft.cancel();
-            isDraftPendingSaved.current = false;
-        },
-        [debouncedSaveDraft],
-    );
-
-    // The ref to check whether the comment saving is in progress
-    const isCommentPendingSaved = useRef(false);
-
-    const debouncedSaveReportComment = useMemo(
-        () =>
-            lodashDebounce((selectedReportID: string, newComment: string | null) => {
-                saveReportDraftComment(selectedReportID, newComment);
-                isCommentPendingSaved.current = false;
-            }, 1000),
-        [],
-    );
-
     useEffect(() => {
         const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToPreExistingReport_${reportID}`, ({reportToCopyDraftTo, callback}: SwitchToCurrentReportProps) => {
             if (!commentRef.current) {
@@ -545,8 +516,7 @@ function ComposerWithSuggestions({
             if (editingState === 'editing' && shouldUseNarrowLayout) {
                 setEditingMessage(newCommentConverted);
                 if (shouldDebounceSaveComment) {
-                    isDraftPendingSaved.current = true;
-                    debouncedSaveDraft(newCommentConverted);
+                    debouncedSaveReportActionDraft(reportID, editingReportAction, newCommentConverted);
                     return;
                 }
 
@@ -555,8 +525,7 @@ function ComposerWithSuggestions({
             }
 
             if (shouldDebounceSaveComment) {
-                isCommentPendingSaved.current = true;
-                debouncedSaveReportComment(reportID, newCommentConverted);
+                debouncedSaveComment(reportID, newCommentConverted);
             } else {
                 saveReportDraftComment(reportID, newCommentConverted);
             }
@@ -567,21 +536,22 @@ function ComposerWithSuggestions({
         },
         [
             currentUserAccountID,
-            debouncedSaveDraft,
-            debouncedSaveReportComment,
+            editingReportAction,
             editingReportActionID,
             editingState,
             findNewlyAddedChars,
             preferredLocale,
             preferredSkinTone,
-            reportID,
-            suggestionsRef,
             raiseIsScrollLikelyLayoutTriggered,
+            reportID,
             selection.end,
             selection.start,
             setCurrentEditMessageSelection,
             setEditingMessage,
             shouldUseNarrowLayout,
+            suggestionsRef,
+            debouncedSaveReportActionDraft,
+            debouncedSaveComment,
         ],
     );
 
@@ -1123,7 +1093,7 @@ function ComposerWithSuggestions({
                     value={value}
                     updateComment={updateComment}
                     commentRef={commentRef}
-                    isCommentPendingSaved={isCommentPendingSaved}
+                    isCommentPendingSaved={isCommentSavePending}
                     isTransitioningToPreExistingReport={isTransitioningToPreExistingReport}
                     onTransitionToPreExistingReportComplete={handleTransitionToPreExistingReportComplete}
                 />
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 8399a5342c68..ba5597a893d3 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -1,4 +1,3 @@
-import lodashDebounce from 'lodash/debounce';
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 // eslint-disable-next-line no-restricted-imports
 import {InteractionManager, View} from 'react-native';
@@ -47,6 +46,7 @@ import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDeb
 import useEditMessage from './ReportActionCompose/useEditMessage';
 import {useReportActionActiveEdit, useReportActionActiveEditActions} from './ReportActionEditMessageContext';
 import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';
+import useDebouncedSaveDraft from './useDebouncedSaveDraft';
 import useDraftMessageVideoAttributeCache from './useDraftMessageVideoAttributeCache';
 
 type ReportActionItemMessageEditProps = {
@@ -137,15 +137,17 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
     const composerRef = useRef<ComposerRef | null>(null);
     const draftRef = useRef(draft);
     const emojiPickerSelectionRef = useRef<Selection | undefined>(undefined);
-    // The ref to check whether the comment saving is in progress
-    const isCommentPendingSaved = useRef(false);
+
+    // Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
+    // allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
+    const {saveDraft, isSavePending: isDraftSavePending} = useDebouncedSaveDraft(saveReportActionDraft);
 
     useDraftMessageVideoAttributeCache({
         draftMessage: editingMessage ?? '',
         isEditing: true,
         editingReportAction: action,
         updateDraftMessage: setDraft,
-        isEditInProgressRef: isCommentPendingSaved,
+        isEditInProgressRef: isDraftSavePending,
     });
 
     useEffect(() => {
@@ -184,28 +186,6 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
         }, true);
     }, [focus]);
 
-    /**
-     * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
-     * allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
-     * @param {String} newDraft
-     */
-    const debouncedSaveDraft = useMemo(
-        () =>
-            lodashDebounce((newDraft: string) => {
-                saveReportActionDraft(reportID, action, newDraft);
-                isCommentPendingSaved.current = false;
-            }, 1000),
-        [reportID, action],
-    );
-
-    useEffect(
-        () => () => {
-            debouncedSaveDraft.cancel();
-            isCommentPendingSaved.current = false;
-        },
-        [debouncedSaveDraft],
-    );
-
     /**
      * Update the value of the draft in Onyx
      *
@@ -238,10 +218,9 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
             setEditingMessage(newDraft);
 
             // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted.
-            debouncedSaveDraft(newDraft);
-            isCommentPendingSaved.current = true;
+            saveDraft(reportID, action, newDraft);
         },
-        [debouncedSaveDraft, preferredLocale, preferredSkinTone, raiseIsScrollLayoutTriggered, selection?.end, setEditingMessage, setSelection],
+        [action, preferredLocale, preferredSkinTone, raiseIsScrollLayoutTriggered, reportID, selection.end, setEditingMessage, setSelection, saveDraft],
     );
 
     useEffect(() => {
diff --git a/src/pages/inbox/report/useDebouncedSaveDraft.ts b/src/pages/inbox/report/useDebouncedSaveDraft.ts
new file mode 100644
index 000000000000..f3c663743dcf
--- /dev/null
+++ b/src/pages/inbox/report/useDebouncedSaveDraft.ts
@@ -0,0 +1,38 @@
+import {useEffect, useRef} from 'react';
+import useDebounce from '@hooks/useDebounce';
+
+const DEFAULT_DEBOUNCE_DELAY = 1000;
+
+/**
+ * Debounces a function to save a draft for a report comment or report action draft.
+ * @param saveDraft - The function to save the draft. It will be called with the arguments passed to the triggerSaveDraft function.
+ * @returns An object containing the debounced save draft function, the trigger save draft function, and the is save pending ref.
+ * @property {Function} debouncedSaveDraft - The debounced save draft function.
+ * @property {Function} triggerSaveDraft - The trigger save draft function.
+ * @property {Ref<boolean>} isSavePending - The ref to check whether the save is pending.
+ */
+function useDebouncedSaveDraft<SaveDraftArgs extends unknown[]>(saveDraftFn: (...args: SaveDraftArgs) => void, wait = DEFAULT_DEBOUNCE_DELAY) {
+    const isSavePending = useRef(false);
+
+    const debouncedSaveDraft = useDebounce(saveDraftFn, wait);
+
+    const saveDraft = (...args: SaveDraftArgs) => {
+        isSavePending.current = true;
+        debouncedSaveDraft(...args);
+    };
+
+    // Cancel the debounced save draft on unmount
+    useEffect(
+        () => () => {
+            isSavePending.current = false;
+        },
+        [debouncedSaveDraft],
+    );
+
+    return {
+        saveDraft,
+        isSavePending,
+    };
+}
+
+export default useDebouncedSaveDraft;

From 0e13386b77cfadf8a79899660543528bc6495f80 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 13:31:07 +0100
Subject: [PATCH 213/233] fix: keep `editingReportActionID` as a dependency of
 debounced saveDraft function

---
 .../ComposerWithSuggestions.tsx               | 23 +++++++++++++++----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index d3a3ba9c14b7..fb7eef64946b 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -277,11 +277,25 @@ function ComposerWithSuggestions({
     });
 
     // Save the draft of the report action. This debounced so that we're not ceaselessly saving your edit.
-    const {saveDraft: debouncedSaveReportActionDraft, isSavePending: isDraftSavePending} = useDebouncedSaveDraft(saveReportActionDraft);
+    const {saveDraft: debouncedSaveReportActionDraft, isSavePending: isDraftSavePending} = useDebouncedSaveDraft(
+        useCallback(
+            (comment: string) => {
+                saveReportActionDraft(reportID, editingReportAction, comment);
+            },
+            [reportID, editingReportAction],
+        ),
+    );
 
     // Save the draft of the report comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft
     // allows one to navigate somewhere else and come back to the comment and still have it in edit mode.
-    const {saveDraft: debouncedSaveComment, isSavePending: isCommentSavePending} = useDebouncedSaveDraft(saveReportDraftComment);
+    const {saveDraft: debouncedSaveComment, isSavePending: isCommentSavePending} = useDebouncedSaveDraft(
+        useCallback(
+            (comment: string) => {
+                saveReportDraftComment(reportID, comment);
+            },
+            [reportID],
+        ),
+    );
 
     useDraftMessageVideoAttributeCache({
         draftMessage: value,
@@ -516,7 +530,7 @@ function ComposerWithSuggestions({
             if (editingState === 'editing' && shouldUseNarrowLayout) {
                 setEditingMessage(newCommentConverted);
                 if (shouldDebounceSaveComment) {
-                    debouncedSaveReportActionDraft(reportID, editingReportAction, newCommentConverted);
+                    debouncedSaveReportActionDraft(newCommentConverted);
                     return;
                 }
 
@@ -525,7 +539,7 @@ function ComposerWithSuggestions({
             }
 
             if (shouldDebounceSaveComment) {
-                debouncedSaveComment(reportID, newCommentConverted);
+                debouncedSaveComment(newCommentConverted);
             } else {
                 saveReportDraftComment(reportID, newCommentConverted);
             }
@@ -536,7 +550,6 @@ function ComposerWithSuggestions({
         },
         [
             currentUserAccountID,
-            editingReportAction,
             editingReportActionID,
             editingState,
             findNewlyAddedChars,

From 5536df02a17790a2939dee2c92fe4124b67ebc44 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 17:31:08 +0100
Subject: [PATCH 214/233] test: fix invalid mock

---
 tests/unit/hooks/useEditComposerToggleTest.ts | 32 +++++++++----------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
index 8da04140c803..f530eea7e11c 100644
--- a/tests/unit/hooks/useEditComposerToggleTest.ts
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -9,13 +9,13 @@ import type {ComposerEditState} from '@pages/inbox/report/ReportActionCompose/Co
 import ReportActionComposeUtils from '@pages/inbox/report/ReportActionCompose/ReportActionComposeUtils';
 import useEditComposerToggle from '@pages/inbox/report/ReportActionCompose/useEditComposerToggle';
 
-jest.mock('@pages/inbox/report/ReportActionEditMessageContext', () => {
+jest.mock('@pages/inbox/report/ReportActionCompose/ComposerContext', () => {
     // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-    const actual = jest.requireActual('@pages/inbox/report/ReportActionEditMessageContext');
+    const actual = jest.requireActual('@pages/inbox/report/ReportActionCompose/ComposerContext');
     // eslint-disable-next-line @typescript-eslint/no-unsafe-return
     return {
         ...actual,
-        useReportActionActiveEdit: jest.fn(),
+        useComposerEditState: jest.fn(),
     };
 });
 
@@ -50,7 +50,7 @@ function makeComposerRef(overrides?: Partial<ComposerRef>): RefObject<ComposerRe
     };
 }
 
-function defaultActiveEdit(overrides?: Partial<ComposerEditState>): ComposerEditState {
+function defaultComposerEditState(overrides?: Partial<ComposerEditState>): ComposerEditState {
     return {
         editingState: 'off',
         isEditingInComposer: false,
@@ -98,19 +98,19 @@ function narrowLayoutResult(): ResponsiveLayoutResult {
 }
 
 describe('useEditComposerToggle', () => {
-    const activeEditRef = {current: defaultActiveEdit()};
+    const composerEditStateRef = {current: defaultComposerEditState()};
 
     beforeEach(() => {
         jest.clearAllMocks();
-        activeEditRef.current = defaultActiveEdit();
-        mockUseComposerEditState.mockImplementation(() => activeEditRef.current);
+        composerEditStateRef.current = defaultComposerEditState();
+        mockUseComposerEditState.mockImplementation(() => composerEditStateRef.current);
         mockUseResponsiveLayout.mockReturnValue(narrowLayoutResult());
     });
 
     it('does not run apply logic while editingState is submitted', () => {
         const onValueChange = jest.fn();
         const composerRef = makeComposerRef();
-        activeEditRef.current = defaultActiveEdit({editingState: 'submitted', editingMessage: 'hello'});
+        composerEditStateRef.current = defaultComposerEditState({editingState: 'submitted', editingMessage: 'hello'});
 
         renderHook(() =>
             useEditComposerToggle({
@@ -144,7 +144,7 @@ describe('useEditComposerToggle', () => {
             {initialProps: {selection: priorSelection, draft: 'keep my draft'}},
         );
 
-        activeEditRef.current = defaultActiveEdit({
+        composerEditStateRef.current = defaultComposerEditState({
             editingState: 'editing',
             editingMessage: 'edited body',
             editingReportActionID: '100',
@@ -173,7 +173,7 @@ describe('useEditComposerToggle', () => {
             }),
         );
 
-        activeEditRef.current = defaultActiveEdit({
+        composerEditStateRef.current = defaultComposerEditState({
             editingState: 'editing',
             editingMessage: 'from thread',
         });
@@ -191,7 +191,7 @@ describe('useEditComposerToggle', () => {
         // Start with edit off so wasEditingRef is false; then turn editing on to capture previousDraftSelectionRef.
         const {rerender} = renderHook(
             (props: {selection: TextSelection; draft: string; editing: boolean}) => {
-                activeEditRef.current = defaultActiveEdit(props.editing ? {editingState: 'editing', editingMessage: 'e', editingReportActionID: '1'} : {editingState: 'off'});
+                composerEditStateRef.current = defaultComposerEditState(props.editing ? {editingState: 'editing', editingMessage: 'e', editingReportActionID: '1'} : {editingState: 'off'});
                 return useEditComposerToggle({
                     selection: props.selection,
                     draftComment: props.draft,
@@ -220,7 +220,7 @@ describe('useEditComposerToggle', () => {
         const onFocus = jest.fn();
         const composerRef = makeComposerRef();
 
-        activeEditRef.current = defaultActiveEdit({
+        composerEditStateRef.current = defaultComposerEditState({
             editingState: 'editing',
             editingMessage: 'first',
             editingReportActionID: 'a',
@@ -228,7 +228,7 @@ describe('useEditComposerToggle', () => {
 
         const {rerender} = renderHook(
             (id: string) => {
-                activeEditRef.current = defaultActiveEdit({
+                composerEditStateRef.current = defaultComposerEditState({
                     editingState: 'editing',
                     editingMessage: id === 'a' ? 'first' : 'second',
                     editingReportActionID: id,
@@ -260,7 +260,7 @@ describe('useEditComposerToggle', () => {
         const onFocus = jest.fn();
         const composerRef = makeComposerRef();
 
-        activeEditRef.current = defaultActiveEdit({
+        composerEditStateRef.current = defaultComposerEditState({
             editingState: 'editing',
             editingMessage: 'wide first',
         });
@@ -288,7 +288,7 @@ describe('useEditComposerToggle', () => {
         const onValueChange = jest.fn();
         const composerRef = makeComposerRef();
 
-        activeEditRef.current = defaultActiveEdit({
+        composerEditStateRef.current = defaultComposerEditState({
             editingState: 'editing',
             editingMessage: 'editing in narrow',
         });
@@ -320,7 +320,7 @@ describe('useEditComposerToggle', () => {
 
         const {rerender} = renderHook(
             (editing: boolean) => {
-                activeEditRef.current = defaultActiveEdit(editing ? {editingState: 'editing', editingMessage: 'hi', editingReportActionID: '1'} : {editingState: 'off'});
+                composerEditStateRef.current = defaultComposerEditState(editing ? {editingState: 'editing', editingMessage: 'hi', editingReportActionID: '1'} : {editingState: 'off'});
                 return useEditComposerToggle({
                     selection: {start: 0, end: 0},
                     draftComment: 'd',

From a2475f338cf235e3da0a16efb3a0b1b63a22015d Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 17:49:12 +0100
Subject: [PATCH 215/233] refactor: draftMessage usage

---
 .../report/ReportActionCompose/ComposerActionMenu.tsx    | 3 +--
 .../inbox/report/ReportActionCompose/ComposerContext.ts  | 4 ++--
 .../report/ReportActionCompose/ComposerProvider.tsx      | 2 +-
 .../ReportActionCompose/ComposerWithSuggestions.tsx      | 9 ++++-----
 src/pages/inbox/report/ReportActionItemMessageEdit.tsx   | 2 ++
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
index 08af19646a21..1fe0bd566459 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
@@ -17,14 +17,13 @@ type ComposerActionMenuProps = {
 
 function ComposerActionMenu({reportID}: ComposerActionMenuProps) {
     const currentUserPersonalDetails = useCurrentUserPersonalDetails();
-    const {isMenuVisible, isFullComposerAvailable} = useComposerState();
+    const {isMenuVisible, isFullComposerAvailable, draftComment} = useComposerState();
     const {exceededMaxLength} = useComposerSendState();
     const {setMenuVisibility, onAddActionPressed, onItemSelected, onTriggerAttachmentPicker} = useComposerActions();
     const {actionButtonRef, composerRef} = useComposerMeta();
     const {pickAttachments, PDFValidationComponent, ErrorModal} = useAttachmentPicker(reportID);
 
     const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`);
-    const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`);
 
     const {raiseIsScrollLayoutTriggered} = useIsScrollLikelyLayoutTriggered();
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
index d6bebf43a981..7768e03f7572 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerContext.ts
@@ -28,6 +28,7 @@ type ComposerState = {
     isMenuVisible: boolean;
     isFullComposerAvailable: boolean;
     didResetComposerHeight: boolean;
+    draftComment: string | undefined;
 };
 
 type ComposerEditState = {
@@ -37,7 +38,6 @@ type ComposerEditState = {
     editingReportActionID: string | null;
     editingReportAction: ReportAction | null;
     editingMessage: string | null;
-    draftComment: string | null | undefined;
     effectiveDraft: string | null | undefined;
     currentEditMessageSelection: TextSelection | null;
 };
@@ -91,6 +91,7 @@ const defaultState: ComposerState = {
     isMenuVisible: false,
     isFullComposerAvailable: false,
     didResetComposerHeight: false,
+    draftComment: undefined,
 };
 const ComposerStateContext = createContext<ComposerState>(defaultState);
 
@@ -110,7 +111,6 @@ const defaultEditState: ComposerEditState = {
     editingReportActionID: null,
     editingReportAction: null,
     editingMessage: null,
-    draftComment: undefined,
     effectiveDraft: undefined,
     currentEditMessageSelection: null,
 };
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
index bea30aaddee9..23590d162d0c 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
@@ -112,6 +112,7 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         isMenuVisible,
         isFullComposerAvailable,
         didResetComposerHeight,
+        draftComment,
     };
 
     const composerEditState = {
@@ -121,7 +122,6 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) {
         editingReportActionID,
         editingReportAction,
         editingMessage,
-        draftComment,
         effectiveDraft,
         currentEditMessageSelection,
     };
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index fb7eef64946b..389c8019db0c 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -64,7 +64,7 @@ import type {FileObject} from '@src/types/utils/Attachment';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 // eslint-disable-next-line no-restricted-imports
 import findNodeHandle from '@src/utils/findNodeHandle';
-import {useComposerEditState} from './ComposerContext';
+import {useComposerEditState, useComposerState} from './ComposerContext';
 import getCursorPosition from './getCursorPosition';
 import getScrollPosition from './getScrollPosition';
 import type {SuggestionsRef} from './ReportActionCompose';
@@ -257,18 +257,17 @@ function ComposerWithSuggestions({
     const cursorPositionValue = useSharedValue({x: 0, y: 0});
     const tag = useSharedValue(-1);
     const isInSidePanel = useIsInSidePanel();
-    const [draftComment = ''] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`);
     const {shouldUseNarrowLayout} = useResponsiveLayout();
 
     const composerRef = useRef<ComposerRef | null>(null);
 
-    const {editingState, editingReportActionID, editingReportAction, editingMessage, currentEditMessageSelection} = useComposerEditState();
+    const {draftComment = ''} = useComposerState();
+    const {editingState, editingReportActionID, editingReportAction, effectiveDraft, currentEditMessageSelection} = useComposerEditState();
     const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
 
     const isEditing = editingState !== 'off';
-    const isEditingInComposer = shouldUseNarrowLayout && isEditing;
     const [value, setValue] = useState(() => {
-        const initialValue = isEditingInComposer ? (editingMessage ?? draftComment) : draftComment;
+        const initialValue = effectiveDraft ?? draftComment;
 
         if (initialValue) {
             emojisPresentBefore.current = extractEmojis(initialValue);
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index ba5597a893d3..9fcbc935ed88 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -118,6 +118,8 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
     );
 
     useEffect(() => {
+        // When the current edit message selection changes, we need to update the selection state
+        // eslint-disable-next-line react-hooks/set-state-in-effect
         setSelectionState(currentEditMessageSelection ?? defaultSelection);
     }, [currentEditMessageSelection, defaultSelection, draft.length, setSelection]);
 

From a82b009be13cc6a005035dffc54be6a48c926c37 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 17:49:25 +0100
Subject: [PATCH 216/233] chore: fix `eslint-seatbelt` complaining about moved
 file

---
 config/eslint/eslint.seatbelt.tsv | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv
index bbf0c7e8c27a..89c68d0969d4 100644
--- a/config/eslint/eslint.seatbelt.tsv
+++ b/config/eslint/eslint.seatbelt.tsv
@@ -379,8 +379,8 @@
 "../../src/pages/inbox/report/PureReportActionItem.tsx"	"react-hooks/refs"	2
 "../../src/pages/inbox/report/PureReportActionItem.tsx"	"react-hooks/set-state-in-effect"	1
 "../../src/pages/inbox/report/ReactionList/HeaderReactionList.tsx"	"no-restricted-syntax"	1
-"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx"	"react-hooks/preserve-manual-memoization"	2
-"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx"	"react-hooks/refs"	8
+"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx"	"react-hooks/preserve-manual-memoization"	2
+"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx"	"react-hooks/refs"	8
 "../../src/pages/inbox/report/ReportActionCompose/SuggestionEmoji.tsx"	"react-hooks/refs"	1
 "../../src/pages/inbox/report/ReportActionCompose/SuggestionMention.tsx"	"react-hooks/refs"	3
 "../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx"	"no-restricted-syntax"	1

From 3a5e5fe87c0fc5e163992f57a8c76b504fde2415 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 22:17:53 +0100
Subject: [PATCH 217/233] fix: update draftComment composer hook

---
 .../inbox/report/ReportActionCompose/useComposerSubmit.ts     | 4 ++--
 tests/unit/hooks/useEditComposerToggleTest.ts                 | 1 -
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts
index cc55a5c9a5f1..c08bb3d191da 100644
--- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts
@@ -45,10 +45,10 @@ function useComposerSubmit(reportID: string): ComposerSubmitFunctions {
     const delegateAccountID = useDelegateAccountID();
 
     const {composerRef, attachmentFileRef} = useComposerMeta();
-    const {didResetComposerHeight} = useComposerState();
+    const {didResetComposerHeight, draftComment} = useComposerState();
     const {setDidResetComposerHeight, clearComposer} = useComposerActions();
     const {isSendDisabled, debouncedCommentMaxLengthValidation} = useComposerSendState();
-    const {isEditingInComposer, editingMessage, draftComment, effectiveDraft} = useComposerEditState();
+    const {isEditingInComposer, editingMessage, effectiveDraft} = useComposerEditState();
     const {publishDraft} = useComposerEditActions();
     const {scrollOffsetRef} = useContext(ActionListContext);
 
diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
index f530eea7e11c..76c808c2d158 100644
--- a/tests/unit/hooks/useEditComposerToggleTest.ts
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -59,7 +59,6 @@ function defaultComposerEditState(overrides?: Partial<ComposerEditState>): Compo
         editingReportAction: null,
         editingMessage: null,
         currentEditMessageSelection: null,
-        draftComment: undefined,
         effectiveDraft: undefined,
         ...overrides,
     };

From 2b422eb8351eba806710ebcff07c7ed30ba899d0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Wed, 29 Apr 2026 22:18:46 +0100
Subject: [PATCH 218/233] fix: remove unused `useOnyx` call

---
 src/pages/inbox/report/ReportActionsList.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index a25bd3507e33..38d3aba883aa 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -191,7 +191,6 @@ function ReportActionsList({
     const isFocused = useIsFocused();
 
     const isReportArchived = useReportIsArchived(report?.reportID);
-    const [] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`);
     const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID);
     const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT);
     const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;

From 8c8bfa1a2b983f6114c53cca5d841f866180c8d2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Tue, 5 May 2026 10:51:54 +0100
Subject: [PATCH 219/233] fix: lint errors

---
 src/pages/inbox/report/PureReportActionItem.tsx | 1 -
 src/pages/inbox/report/ReportActionsList.tsx    | 3 ---
 tests/ui/ReportActionComposeTest.tsx            | 1 -
 tests/ui/ReportActionMessageEditLayoutTest.tsx  | 1 -
 tests/unit/hooks/useEditComposerToggleTest.ts   | 1 -
 tests/unit/hooks/useEditMessage.test.ts         | 4 ----
 6 files changed, 11 deletions(-)

diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 311394089072..44326c4dc91d 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -9,7 +9,6 @@ import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
 import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView';
 import type {ComposerRef} from '@components/Composer/types';
-import DisplayNames from '@components/DisplayNames';
 import Hoverable from '@components/Hoverable';
 import InlineSystemMessage from '@components/InlineSystemMessage';
 import {ModalActions} from '@components/Modal/Global/ModalContext';
diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index 574f1b90cdc8..57d1adf540db 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -50,7 +50,6 @@ import {
     canShowReportRecipientLocalTime,
     canUserPerformWriteAction,
     chatIncludesChronosWithID,
-    getOriginalReportID,
     getReportLastVisibleActionCreated,
     isArchivedNonExpenseReport,
     isCanceledTaskReport,
@@ -791,7 +790,6 @@ function ReportActionsList({
             report,
             showHiddenHistory,
             hasPreviousMessages,
-            translate,
             onShowPreviousMessages,
             parentReportAction,
             parentReportActionForTransactionThread,
@@ -810,7 +808,6 @@ function ReportActionsList({
             isTryNewDotNVPDismissed,
             reportNameValuePairs?.origin,
             reportNameValuePairs?.originalID,
-            styles,
             showHiddenHistory,
             hasPreviousMessages,
             onShowPreviousMessages,
diff --git a/tests/ui/ReportActionComposeTest.tsx b/tests/ui/ReportActionComposeTest.tsx
index b4d1aa3cacd0..0863baf49d2b 100644
--- a/tests/ui/ReportActionComposeTest.tsx
+++ b/tests/ui/ReportActionComposeTest.tsx
@@ -68,7 +68,6 @@ function ReportScreenProviders({children}: PropsWithChildren) {
 }
 
 const renderReportActionCompose = (props?: Partial<ReportActionComposeProps>) => {
-    // eslint-disable-next-line react/jsx-props-no-spreading
     return render(
         <ReportScreenProviders>
             <ReportActionCompose
diff --git a/tests/ui/ReportActionMessageEditLayoutTest.tsx b/tests/ui/ReportActionMessageEditLayoutTest.tsx
index 6466fde55607..5f148148f94d 100644
--- a/tests/ui/ReportActionMessageEditLayoutTest.tsx
+++ b/tests/ui/ReportActionMessageEditLayoutTest.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/naming-convention */
 import type * as NativeNavigation from '@react-navigation/native';
 import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react-native';
 import React from 'react';
diff --git a/tests/unit/hooks/useEditComposerToggleTest.ts b/tests/unit/hooks/useEditComposerToggleTest.ts
index 76c808c2d158..47c496edce5a 100644
--- a/tests/unit/hooks/useEditComposerToggleTest.ts
+++ b/tests/unit/hooks/useEditComposerToggleTest.ts
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/naming-convention */
 import {renderHook} from '@testing-library/react-native';
 import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';
diff --git a/tests/unit/hooks/useEditMessage.test.ts b/tests/unit/hooks/useEditMessage.test.ts
index 8b5cc09bf678..6d544b9b37df 100644
--- a/tests/unit/hooks/useEditMessage.test.ts
+++ b/tests/unit/hooks/useEditMessage.test.ts
@@ -36,25 +36,21 @@ jest.mock('@pages/inbox/report/ContextMenu/ReportActionContextMenu', () => {
 });
 
 jest.mock('@hooks/useAncestors', () => ({
-    // eslint-disable-next-line @typescript-eslint/naming-convention
     __esModule: true,
     default: () => [],
 }));
 
 jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({
-    // eslint-disable-next-line @typescript-eslint/naming-convention
     __esModule: true,
     default: () => ({email: 'user@test.com'}),
 }));
 
 jest.mock('@hooks/useReportIsArchived', () => ({
-    // eslint-disable-next-line @typescript-eslint/naming-convention
     __esModule: true,
     default: () => false,
 }));
 
 jest.mock('@hooks/useReportScrollManager', () => ({
-    // eslint-disable-next-line @typescript-eslint/naming-convention
     __esModule: true,
     default: () => ({scrollToIndex: jest.fn()}),
 }));

From b3f9ed4f73a1d9466fceb12276b8f884c530ad46 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 12:50:28 +0100
Subject: [PATCH 220/233] fix: ESLint errors

---
 src/pages/inbox/report/ReportActionsList.tsx | 29 +++++++++-----------
 1 file changed, 13 insertions(+), 16 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index 82fc470e0ef2..578896e966eb 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -782,30 +782,27 @@ function ReportActionsList({
             );
         },
         [
-            report,
-            showHiddenHistory,
+            actionIndexMap,
+            firstVisibleReportActionID,
             hasPreviousMessages,
+            isOffline,
+            isReportArchived,
+            isTryNewDotNVPDismissed,
+            linkedReportActionID,
             onShowPreviousMessages,
             parentReportAction,
             parentReportActionForTransactionThread,
-            isOffline,
-            transactionThreadReport,
-            linkedReportActionID,
-            actionIndexMap,
-            renderedVisibleReportActions,
-            shouldHideThreadDividerLine,
-            unreadMarkerReportActionID,
-            firstVisibleReportActionID,
-            shouldUseThreadDividerLine,
             personalDetailsList,
-            isReportArchived,
-            userBillingFundID,
-            isTryNewDotNVPDismissed,
+            renderedVisibleReportActions,
+            report,
             reportNameValuePairs?.origin,
             reportNameValuePairs?.originalID,
+            shouldHideThreadDividerLine,
+            shouldUseThreadDividerLine,
             showHiddenHistory,
-            hasPreviousMessages,
-            onShowPreviousMessages,
+            transactionThreadReport,
+            unreadMarkerReportActionID,
+            userBillingFundID,
         ],
     );
 

From 3d7dead03a0e4f784febdc656b356c37bf1fc25f Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 13:38:24 +0100
Subject: [PATCH 221/233] refactor: `React.RefObject` type import

---
 .../report/ReportActionCompose/ReportActionComposeUtils.ts     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
index dc89ef29d741..10faf40d535f 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionComposeUtils.ts
@@ -1,10 +1,11 @@
+import type {RefObject} from 'react';
 import type {ComposerRef} from '@components/Composer/types';
 import getPlatform from '@libs/getPlatform';
 import CONST from '@src/CONST';
 
 const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
 
-const updateNativeSelectionValue = (composerRef: React.RefObject<ComposerRef | null>, start: number, end: number) => {
+const updateNativeSelectionValue = (composerRef: RefObject<ComposerRef | null>, start: number, end: number) => {
     if (!isIOSNative) {
         return;
     }

From 3899406910b351d184a354a298e958b5ea46fed3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 13:38:39 +0100
Subject: [PATCH 222/233] fix: reset debounced safe draft ref

---
 src/pages/inbox/report/useDebouncedSaveDraft.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/useDebouncedSaveDraft.ts b/src/pages/inbox/report/useDebouncedSaveDraft.ts
index f3c663743dcf..12870247cf15 100644
--- a/src/pages/inbox/report/useDebouncedSaveDraft.ts
+++ b/src/pages/inbox/report/useDebouncedSaveDraft.ts
@@ -14,7 +14,10 @@ const DEFAULT_DEBOUNCE_DELAY = 1000;
 function useDebouncedSaveDraft<SaveDraftArgs extends unknown[]>(saveDraftFn: (...args: SaveDraftArgs) => void, wait = DEFAULT_DEBOUNCE_DELAY) {
     const isSavePending = useRef(false);
 
-    const debouncedSaveDraft = useDebounce(saveDraftFn, wait);
+    const debouncedSaveDraft = useDebounce((...args: SaveDraftArgs) => {
+        saveDraftFn(...args);
+        isSavePending.current = false;
+    }, wait);
 
     const saveDraft = (...args: SaveDraftArgs) => {
         isSavePending.current = true;

From ec7671feebd8a49c4633df5ace079eb4d1e2d181 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 13:38:55 +0100
Subject: [PATCH 223/233] fix: also clear report action drafts when report
 changes

---
 src/pages/inbox/ReportScreen.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index 5b3b3a6c40cc..d21a791820f2 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -57,14 +57,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
     const isTopMostReportId = currentReportIDValue === reportIDFromRoute;
     const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
 
-    // When the report screen is navigated away from, clear all report action edit drafts
+    // When the report screen is navigated away from or the report changes, clear all report action edit drafts
     useEffect(() => {
         clearAllReportActionDrafts();
 
         return () => {
             clearAllReportActionDrafts();
         };
-    }, []);
+    }, [reportIDFromRoute]);
 
     const shouldDeferNonEssentials = useDeferNonEssentials(reportIDFromRoute);
 

From 76b3dc552e3219b3cf1af44737422c8db62338a0 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 13:39:26 +0100
Subject: [PATCH 224/233] fix: remove unused eslint-disable directive

---
 .../inbox/report/ReportActionCompose/useEditComposerToggle.ts    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index 3f3d9dac3bc7..4b07fdff7abe 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-param-reassign */
 import {useCallback, useEffect, useRef} from 'react';
 import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';

From c32aa0fb4cd122e738a1479574837195df526dd5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 13:56:03 +0100
Subject: [PATCH 225/233] refactor: split `SendOrSaveButton` into compositional
 component usages

---
 .../ComposerSendButton.tsx                    | 13 ++++++--
 ...OrSaveButton.tsx => SubmitDraftButton.tsx} | 31 ++++++++-----------
 .../report/ReportActionItemMessageEdit.tsx    | 12 ++++---
 3 files changed, 31 insertions(+), 25 deletions(-)
 rename src/pages/inbox/report/ReportActionCompose/{SendOrSaveButton.tsx => SubmitDraftButton.tsx} (53%)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerSendButton.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerSendButton.tsx
index adf38f7b1868..ed16804decac 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerSendButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerSendButton.tsx
@@ -1,17 +1,19 @@
 import React from 'react';
 import {View} from 'react-native';
 import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
 import {useComposerEditState, useComposerSendState} from './ComposerContext';
-import SendOrSaveButton from './SendOrSaveButton';
+import SubmitDraftButton from './SubmitDraftButton';
 import useComposerSubmit from './useComposerSubmit';
 
 function ComposerSendButton({reportID}: {reportID: string}) {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
+    const icons = useMemoizedLazyExpensifyIcons(['Send']);
 
     const {isEditingInComposer} = useComposerEditState();
     const {isSendDisabled} = useComposerSendState();
@@ -48,9 +50,14 @@ function ComposerSendButton({reportID}: {reportID: string}) {
                     accessibilityLabel={accessibilityLabel}
                     collapsable={false}
                 >
-                    <SendOrSaveButton
+                    <SubmitDraftButton
                         isDisabled={isSendDisabled}
-                        isEditing={isEditingInComposer}
+                        icon={icons.Send}
+                        label={translate('common.send')}
+                        sentryLabel={CONST.SENTRY_LABEL.REPORT.SEND_BUTTON}
+                        onPress={submitDraftAndClearComposer}
+                        // Since the parent View has accessible, we need to set accessible to false here to avoid duplicate accessibility elements.
+                        // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.
                         accessible={false}
                         focusable={false}
                     />
diff --git a/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx b/src/pages/inbox/report/ReportActionCompose/SubmitDraftButton.tsx
similarity index 53%
rename from src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
rename to src/pages/inbox/report/ReportActionCompose/SubmitDraftButton.tsx
index 4b5311bccbaa..4d4ceebe248c 100644
--- a/src/pages/inbox/report/ReportActionCompose/SendOrSaveButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/SubmitDraftButton.tsx
@@ -3,30 +3,27 @@ import Icon from '@components/Icon';
 import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
 import type {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback';
 import Tooltip from '@components/Tooltip';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
-import useLocalize from '@hooks/useLocalize';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type SubmitDraftButtonProps = PressableWithFeedbackProps & {
+    /** The label to display on the button */
+    label: string;
+
+    /** The icon to display on the button */
+    icon: IconAsset;
 
-type SendOrSaveButtonProps = PressableWithFeedbackProps & {
     /** Whether the button is disabled */
     isDisabled: boolean;
 
-    /** Whether the button is in editing mode */
-    isEditing?: boolean;
-
     /** Handle clicking on send button */
-    onSendOrSave?: () => void;
+    onPress?: () => void;
 };
 
-function SendOrSaveButton({isDisabled: isDisabledProp = false, isEditing = false, onSendOrSave, ...restProps}: SendOrSaveButtonProps) {
+function SubmitDraftButton({isDisabled: isDisabledProp = false, icon, label, sentryLabel, onPress, ...restProps}: SubmitDraftButtonProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
-    const {translate} = useLocalize();
-    const icons = useMemoizedLazyExpensifyIcons(['Send', 'Checkmark']);
-    const label = translate(isEditing ? 'common.saveChanges' : 'common.send');
-    const sentryLabel = isEditing ? CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON : CONST.SENTRY_LABEL.REPORT.SEND_BUTTON;
 
     return (
         <Tooltip text={label}>
@@ -36,9 +33,7 @@ function SendOrSaveButton({isDisabled: isDisabledProp = false, isEditing = false
                     isDisabledProp || pressed || isDisabled ? undefined : styles.buttonSuccess,
                     isDisabledProp ? styles.cursorDisabled : undefined,
                 ]}
-                // Since the parent View has accessible, we need to set accessible to false here to avoid duplicate accessibility elements.
-                // On Android when TalkBack is enabled, only the parent element should be accessible, otherwise the button will not work.
-                onPress={onSendOrSave}
+                onPress={onPress}
                 sentryLabel={sentryLabel}
                 disabled={isDisabledProp}
                 // eslint-disable-next-line react/jsx-props-no-spreading
@@ -46,7 +41,7 @@ function SendOrSaveButton({isDisabled: isDisabledProp = false, isEditing = false
             >
                 {({pressed}) => (
                     <Icon
-                        src={isEditing ? icons.Checkmark : icons.Send}
+                        src={icon}
                         fill={isDisabledProp || pressed ? theme.icon : theme.textLight}
                     />
                 )}
@@ -55,4 +50,4 @@ function SendOrSaveButton({isDisabled: isDisabledProp = false, isEditing = false
     );
 }
 
-export default SendOrSaveButton;
+export default SubmitDraftButton;
diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
index 6d0ca33675ad..3a5ba34826b3 100644
--- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx
@@ -12,6 +12,7 @@ import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton';
 import ExceededCommentLength from '@components/ExceededCommentLength';
 import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered';
 import useKeyboardState from '@hooks/useKeyboardState';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import useReportScrollManager from '@hooks/useReportScrollManager';
@@ -40,7 +41,7 @@ import getCursorPosition from './ReportActionCompose/getCursorPosition';
 import getScrollPosition from './ReportActionCompose/getScrollPosition';
 import MessageEditCancelButton from './ReportActionCompose/MessageEditCancelButton';
 import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose';
-import SendOrSaveButton from './ReportActionCompose/SendOrSaveButton';
+import SubmitDraftButton from './ReportActionCompose/SubmitDraftButton';
 import Suggestions from './ReportActionCompose/Suggestions';
 import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation';
 import useEditMessage from './ReportActionCompose/useEditMessage';
@@ -96,6 +97,7 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
     const cursorPositionValue = useSharedValue({x: 0, y: 0});
     const tag = useSharedValue(-1);
     const emojisPresentBefore = useRef<Emoji[]>([]);
+    const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
 
     const {currentEditMessageSelection, editingMessage} = useReportActionActiveEdit();
     const {setEditingMessage, setCurrentEditMessageSelection} = useReportActionActiveEditActions();
@@ -483,10 +485,12 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy
                     </View>
 
                     <View style={styles.alignSelfEnd}>
-                        <SendOrSaveButton
+                        <SubmitDraftButton
                             isDisabled={isExceedingMaxLength}
-                            isEditing
-                            onSendOrSave={() => publishDraft(draft)}
+                            icon={icons.Checkmark}
+                            label={translate('common.saveChanges')}
+                            sentryLabel={CONST.SENTRY_LABEL.REPORT.REPORT_ACTION_ITEM_MESSAGE_EDIT_SAVE_BUTTON}
+                            onPress={() => publishDraft(draft)}
                             accessibilityLabel={translate('common.saveChanges')}
                             role={CONST.ROLE.BUTTON}
                             hoverDimmingValue={1}

From 42a865ba601b3dc574b9f58eb0d41e9d19934a0c Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 14:08:08 +0100
Subject: [PATCH 226/233] refactor: move platform-specific logic into platform
 files

---
 .../ComposerWithSuggestions.tsx                   |  8 +++-----
 .../getUpdatedSyncSelection/index.ios.ts          | 12 ++++++++++++
 .../getUpdatedSyncSelection/index.ts              |  4 ++++
 .../getUpdatedSyncSelection/types.ts              | 12 ++++++++++++
 .../updateNativeTextInputValue/index.ios.ts       | 14 ++++++++++++++
 .../updateNativeTextInputValue/index.ts           |  4 ++++
 .../updateNativeTextInputValue/types.ts           | 15 +++++++++++++++
 .../ReportActionCompose/useEditComposerToggle.ts  | 11 ++---------
 8 files changed, 66 insertions(+), 14 deletions(-)
 create mode 100644 src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ios.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/types.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ios.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts
 create mode 100644 src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/types.ts

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 0fdbafe70fbe..ade58d411771 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -40,7 +40,6 @@ import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getTextVSCursorOffset
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
 import type {Selection} from '@libs/focusComposerWithDelay/types';
 import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
-import getPlatform from '@libs/getPlatform';
 import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener';
 import {detectAndRewritePaste} from '@libs/MarkdownLinkHelpers';
 import Parser from '@libs/Parser';
@@ -177,8 +176,6 @@ type SwitchToCurrentReportProps = {
 };
 const {RNTextInputReset} = NativeModules;
 
-const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
-
 /**
  * Broadcast that the user is typing. Debounced to limit how often we publish client events.
  */
@@ -512,8 +509,9 @@ function ComposerWithSuggestions({
                 const adjustedCursorPosition = cursorPosition !== undefined && cursorPosition !== null ? cursorPosition + textVSOffset : undefined;
                 const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), adjustedCursorPosition ?? 0);
 
-                if (commentWithSpaceInserted !== newComment && isIOSNative) {
-                    syncSelectionWithOnChangeTextRef.current = {position, value: newComment};
+                const updatedSyncSelection = getUpdatedSyncSelection({commentWithSpaceInserted, newComment, position});
+                if (updatedSyncSelection) {
+                    syncSelectionWithOnChangeTextRef.current = updatedSyncSelection;
                 }
 
                 // Keep selection in sync after emoji conversion / insertion while editing (e.g. emoji picker on web);
diff --git a/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ios.ts b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ios.ts
new file mode 100644
index 000000000000..40c9e12db43e
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ios.ts
@@ -0,0 +1,12 @@
+import type GetUpdatedSyncSelection from './types';
+
+// We only need to update the sync selection on iOS platforms
+const getUpdatedSyncSelection: GetUpdatedSyncSelection = ({commentWithSpaceInserted, newComment, position}) => {
+    if (commentWithSpaceInserted === newComment) {
+        return;
+    }
+
+    return {position, value: newComment};
+};
+
+export default getUpdatedSyncSelection;
diff --git a/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts
new file mode 100644
index 000000000000..f0f516e4250c
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts
@@ -0,0 +1,4 @@
+// This is a no-op function for non-iOS platforms
+const noop = () => undefined;
+
+export default noop;
diff --git a/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/types.ts b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/types.ts
new file mode 100644
index 000000000000..e2f750764272
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/types.ts
@@ -0,0 +1,12 @@
+type GetUpdatedSyncSelectionProps = {
+    /** The comment with space inserted */
+    commentWithSpaceInserted: string;
+    /** The new comment */
+    newComment: string;
+    /** The position of the comment */
+    position: number;
+};
+
+type GetUpdatedSyncSelection = (props: GetUpdatedSyncSelectionProps) => {position: number; value: string} | undefined;
+
+export default GetUpdatedSyncSelection;
diff --git a/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ios.ts b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ios.ts
new file mode 100644
index 000000000000..99a8f2f23e44
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ios.ts
@@ -0,0 +1,14 @@
+import type UpdateNativeTextInputValue from './types';
+
+// We need to manually update the native text prop on iOS platforms,
+// in order to force a re-calculation of the composer height and layout,
+// when the composer changes in or out of edit mode.
+const updateNativeTextInputValue: UpdateNativeTextInputValue = ({text, shouldForceNativeValueUpdate, composerRef}) => {
+    if (!shouldForceNativeValueUpdate) {
+        return;
+    }
+
+    composerRef.current?.setNativeProps({text});
+};
+
+export default updateNativeTextInputValue;
diff --git a/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts
new file mode 100644
index 000000000000..75a1e96cb44e
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts
@@ -0,0 +1,4 @@
+// We don't need to manually update the native text prop on non-iOS platforms
+const noop = () => undefined;
+
+export default noop;
diff --git a/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/types.ts b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/types.ts
new file mode 100644
index 000000000000..234919473076
--- /dev/null
+++ b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/types.ts
@@ -0,0 +1,15 @@
+import type {RefObject} from 'react';
+import type {ComposerRef} from '@components/Composer/types';
+
+type UpdateNativeTextInputValueProps = {
+    /** The text to update */
+    text: string;
+    /** Whether to force a native value update */
+    shouldForceNativeValueUpdate: boolean;
+    /** The ref to the composer */
+    composerRef: RefObject<ComposerRef | null>;
+};
+
+type UpdateNativeTextInputValue = (props: UpdateNativeTextInputValueProps) => void;
+
+export default UpdateNativeTextInputValue;
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index 4b07fdff7abe..0f25b5f1c934 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -2,12 +2,10 @@ import {useCallback, useEffect, useRef} from 'react';
 import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import getPlatform from '@libs/getPlatform';
-import CONST from '@src/CONST';
 import {useComposerActions, useComposerEditState} from './ComposerContext';
 import ReportActionComposeUtils from './ReportActionComposeUtils';
+import updateNativeTextInputValue from './updateNativeTextInputValue/index.ios';
 
-const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
 type UseEditComposerToggleProps = {
     selection: TextSelection;
     draftComment: string;
@@ -55,12 +53,7 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
             const selectionToApply = explicitSelection ?? (shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection);
 
             onValueChange?.(nextValue);
-            // We need to manually update the native text prop,
-            // in order to force a re-calculation of the composer height and layout,
-            // when the composer changes in or out of edit mode.
-            if (isIOSNative && options?.shouldForceNativeValueUpdate) {
-                composerRef.current?.setNativeProps({text: nextValue});
-            }
+            updateNativeTextInputValue({text: nextValue, shouldForceNativeValueUpdate: options?.shouldForceNativeValueUpdate ?? false, composerRef});
 
             onSelectionChange?.(selectionToApply);
             ReportActionComposeUtils.updateNativeSelectionValue(composerRef, selectionToApply.start, selectionToApply.end ?? selectionToApply.start);

From c23849e7e66511287897fca5f85283e0accc8308 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 14:13:49 +0100
Subject: [PATCH 227/233] docs: add JSDoc to all new type properties

---
 .../ReportActionCompose/ComposerEditingButtons.tsx  |  1 +
 .../ComposerExpandCollapseButton.tsx                |  1 +
 .../ReportActionCompose/ComposerWithSuggestions.tsx |  1 +
 .../ReportActionCompose/MessageEditCancelButton.tsx |  3 +++
 .../report/ReportActionCompose/useComposerSubmit.ts |  7 +------
 .../useDebouncedCommentMaxLengthValidation.ts       |  3 +++
 .../ReportActionCompose/useEditComposerToggle.ts    | 11 +++++++++++
 .../report/ReportActionCompose/useEditMessage.ts    |  6 ++++++
 .../inbox/report/ReportActionEditMessageContext.tsx | 13 +++++++++++++
 9 files changed, 40 insertions(+), 6 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
index 7d627e50653d..77f8f58dfae0 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerEditingButtons.tsx
@@ -7,6 +7,7 @@ import ComposerExpandCollapseButton from './ComposerExpandCollapseButton';
 import MessageEditCancelButton from './MessageEditCancelButton';
 
 type ComposerEditingButtonsProps = {
+    /** The report ID */
     reportID: string;
 };
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
index 7d55275f0100..8d3a6cfb62b7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerExpandCollapseButton.tsx
@@ -8,6 +8,7 @@ import {useComposerSendState, useComposerState} from './ComposerContext';
 import ExpandCollapseButton from './ExpandCollapseButton';
 
 type ComposerExpandCollapseButtonProps = ViewProps & {
+    /** The report ID */
     reportID: string;
 };
 
diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index ade58d411771..9150bcd76932 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -66,6 +66,7 @@ import findNodeHandle from '@src/utils/findNodeHandle';
 import {useComposerEditState, useComposerState} from './ComposerContext';
 import getCursorPosition from './getCursorPosition';
 import getScrollPosition from './getScrollPosition';
+import getUpdatedSyncSelection from './getUpdatedSyncSelection/index.ios';
 import type {SuggestionsRef} from './ReportActionCompose';
 import ReportActionComposeUtils from './ReportActionComposeUtils';
 import SilentCommentUpdater from './SilentCommentUpdater';
diff --git a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
index 7a8758897afc..644e61ef1fea 100644
--- a/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/MessageEditCancelButton.tsx
@@ -11,7 +11,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
 
 type MessageEditCancelButtonProps = ViewProps & {
+    /** Handle clicking on cancel button */
     onCancel: () => void;
+
+    /** The test ID to use for the button */
     testID?: string;
 };
 
diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts
index 482c7545e414..9e3d99b12d13 100644
--- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts
@@ -28,12 +28,7 @@ import type * as OnyxTypes from '@src/types/onyx';
 import {useComposerActions, useComposerEditActions, useComposerEditState, useComposerMeta, useComposerSendState, useComposerState} from './ComposerContext';
 import useSidePanelContext from './useSidePanelContext';
 
-type ComposerSubmitFunctions = {
-    validateAndSubmitDraft: (draftMessage: string) => void;
-    submitDraftAndClearComposer: () => void;
-};
-
-function useComposerSubmit(reportID: string): ComposerSubmitFunctions {
+function useComposerSubmit(reportID: string) {
     const {isOffline} = useNetwork();
     const currentUserPersonalDetails = useCurrentUserPersonalDetails();
     const personalDetails = usePersonalDetails();
diff --git a/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts b/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
index 15baf8e62669..ce9495104e78 100644
--- a/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useDebouncedCommentMaxLengthValidation.ts
@@ -4,7 +4,10 @@ import {getCommentLength} from '@libs/ReportUtils';
 import CONST from '@src/CONST';
 
 type UseDebouncedCommentValidationProps = {
+    /** The report ID */
     reportID: string | undefined;
+
+    /** Whether the composer is in edit mode */
     isEditing?: boolean;
 };
 
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index 0f25b5f1c934..d07151df306c 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -7,11 +7,22 @@ import ReportActionComposeUtils from './ReportActionComposeUtils';
 import updateNativeTextInputValue from './updateNativeTextInputValue/index.ios';
 
 type UseEditComposerToggleProps = {
+    /** The selection of the composer */
     selection: TextSelection;
+
+    /** The draft comment of the composer */
     draftComment: string;
+
+    /** The ref to the composer */
     composerRef: RefObject<ComposerRef | null>;
+
+    /** Handle changing the selection of the composer */
     onSelectionChange?: (selection: TextSelection) => void;
+
+    /** Handle focusing the composer */
     onFocus?: () => void;
+
+    /** Handle changing the value of the composer */
     onValueChange?: (value: string) => void;
 };
 
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
index 8a26a4ac97d3..e39bf1abf21d 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts
@@ -17,11 +17,17 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type * as OnyxTypes from '@src/types/onyx';
 
 type UseEditMessageProps = {
+    /** The report ID */
     reportID: string | undefined;
+    /** The original report ID */
     originalReportID: string | undefined;
+    /** The report action */
     reportAction: OnyxTypes.ReportAction | null | undefined;
+    /** Whether to scroll to the last message */
     shouldScrollToLastMessage?: boolean;
+    /** The debounced comment max length validation */
     debouncedCommentMaxLengthValidation: DebouncedFuncLeading<(value: string) => boolean>;
+    /** The ref to the composer */
     composerRef: React.RefObject<ComposerRef | null>;
 };
 
diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index 2493731e4eb2..a470d2aac4d7 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -12,24 +12,35 @@ function NOOP() {
     return null;
 }
 
+/** Whether the report is currently being edited, is already submitted or is not editing any m */
 type EditingState = 'off' | 'editing' | 'submitted';
 
 type ReportActionActiveEdit = {
+    /** The report ID */
     editingReportID: string | null;
+    /** The report action ID */
     editingReportActionID: string | null;
+    /** The report action */
     editingReportAction: OnyxTypes.ReportAction | null;
+    /** The editing message */
     editingMessage: string | null;
 };
 
 type ReportActionEditMessageContextValue = ReportActionActiveEdit & {
+    /** The current edit message selection */
     currentEditMessageSelection: TextSelection | null;
+    /** The editing state */
     editingState: EditingState;
 };
 
 type ReportActionEditMessageContextActions = {
+    /** Set the editing message */
     setEditingMessage: Dispatch<SetStateAction<string | null>>;
+    /** Set the current edit message selection */
     setCurrentEditMessageSelection: Dispatch<SetStateAction<TextSelection | null>>;
+    /** Submit the edit */
     submitEdit: () => void;
+    /** Stop the editing */
     stopEditing: () => void;
 };
 
@@ -50,7 +61,9 @@ const ReportActionEditMessageActionsContext = createContext<ReportActionEditMess
 });
 
 type ReportActionEditMessageContextProviderProps = {
+    /** The report ID */
     reportID: string | undefined;
+    /** The children */
     children: React.ReactNode;
 };
 

From fb5d5f355d14cb10b93caa33540b61bfef1ab6a7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Thu, 7 May 2026 14:25:26 +0100
Subject: [PATCH 228/233] fix: manual composer height state is never reset

---
 .../inbox/report/ReportActionCompose/useEditComposerToggle.ts  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index d07151df306c..e9848e697cc2 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -87,8 +87,9 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
                 // Editing just ended in the composer – restore the draft comment and its previous selection.
                 applyComposerValue(draftComment ?? '', {selection: previousDraftSelectionRef.current, shouldForceNativeValueUpdate: true});
 
+                // Once the composer is no longer in edit mode, we can reset the manual composer height.
                 if (isEditingInComposer) {
-                    setDidResetComposerHeight(true);
+                    setDidResetComposerHeight(false);
                 }
 
                 if (!wasComposerFocusedBeforeEditingRef.current) {

From da9d5e34000b721f5fba5362c982c7727e2cc212 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 11 May 2026 09:43:17 +0100
Subject: [PATCH 229/233] fix: replace `index.ios` module imports

---
 .../report/ReportActionCompose/ComposerWithSuggestions.tsx      | 2 +-
 .../inbox/report/ReportActionCompose/useEditComposerToggle.ts   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
index 9bf348874a90..c3c9e8128e34 100644
--- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx
@@ -66,7 +66,7 @@ import findNodeHandle from '@src/utils/findNodeHandle';
 import {useComposerEditState, useComposerState} from './ComposerContext';
 import getCursorPosition from './getCursorPosition';
 import getScrollPosition from './getScrollPosition';
-import getUpdatedSyncSelection from './getUpdatedSyncSelection/index.ios';
+import getUpdatedSyncSelection from './getUpdatedSyncSelection';
 import type {SuggestionsRef} from './ReportActionCompose';
 import ReportActionComposeUtils from './ReportActionComposeUtils';
 import SilentCommentUpdater from './SilentCommentUpdater';
diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index e9848e697cc2..1640d4af4554 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -4,7 +4,7 @@ import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import {useComposerActions, useComposerEditState} from './ComposerContext';
 import ReportActionComposeUtils from './ReportActionComposeUtils';
-import updateNativeTextInputValue from './updateNativeTextInputValue/index.ios';
+import updateNativeTextInputValue from './updateNativeTextInputValue';
 
 type UseEditComposerToggleProps = {
     /** The selection of the composer */

From bb6401c40ec5ba4b7611b5d215aee16c690d4978 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 11 May 2026 09:48:12 +0100
Subject: [PATCH 230/233] fix: types of `updateNativeTextInputValue`

---
 .../updateNativeTextInputValue/index.ts                   | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts
index 75a1e96cb44e..5b97a110d5e5 100644
--- a/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts
+++ b/src/pages/inbox/report/ReportActionCompose/updateNativeTextInputValue/index.ts
@@ -1,4 +1,8 @@
-// We don't need to manually update the native text prop on non-iOS platforms
+import type UpdateNativeTextInputValue from './types';
+
 const noop = () => undefined;
 
-export default noop;
+// We don't need to manually update the native text prop on non-iOS platforms
+const updateNativeTextInputValue: UpdateNativeTextInputValue = noop;
+
+export default updateNativeTextInputValue;

From 9883f391b8f6544ecfaa4533a4e1b156232e4681 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 11 May 2026 09:48:28 +0100
Subject: [PATCH 231/233] refactor: remove manual `useCallback` from
 `useEditComposerToggle`

---
 .../useEditComposerToggle.ts                  | 33 +++++++++----------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
index 1640d4af4554..caee59f76d9f 100644
--- a/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
+++ b/src/pages/inbox/report/ReportActionCompose/useEditComposerToggle.ts
@@ -1,4 +1,4 @@
-import {useCallback, useEffect, useRef} from 'react';
+import {useEffect, useRef} from 'react';
 import type {RefObject} from 'react';
 import type {ComposerRef, TextSelection} from '@components/Composer/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -54,27 +54,24 @@ function useEditComposerToggle({selection, draftComment, composerRef, onFocus, o
         shouldForceNativeValueUpdate?: boolean;
     };
 
-    const applyComposerValue = useCallback(
-        (nextValue: string, options?: ApplyComposerValueOptions) => {
-            const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
-            const shouldUseEditingSelection = options?.isEditingInComposer ?? false;
-            const shouldForceSelectionToEnd = options?.shouldMoveSelectionToEnd ?? false;
-            const explicitSelection = options?.selection ?? null;
+    const applyComposerValue = (nextValue: string, options?: ApplyComposerValueOptions) => {
+        const defaultSelection: TextSelection = {start: nextValue.length, end: nextValue.length};
+        const shouldUseEditingSelection = options?.isEditingInComposer ?? false;
+        const shouldForceSelectionToEnd = options?.shouldMoveSelectionToEnd ?? false;
+        const explicitSelection = options?.selection ?? null;
 
-            const selectionToApply = explicitSelection ?? (shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection);
+        const selectionToApply = explicitSelection ?? (shouldUseEditingSelection && !shouldForceSelectionToEnd ? (currentEditMessageSelection ?? defaultSelection) : defaultSelection);
 
-            onValueChange?.(nextValue);
-            updateNativeTextInputValue({text: nextValue, shouldForceNativeValueUpdate: options?.shouldForceNativeValueUpdate ?? false, composerRef});
+        onValueChange?.(nextValue);
+        updateNativeTextInputValue({text: nextValue, shouldForceNativeValueUpdate: options?.shouldForceNativeValueUpdate ?? false, composerRef});
 
-            onSelectionChange?.(selectionToApply);
-            ReportActionComposeUtils.updateNativeSelectionValue(composerRef, selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
+        onSelectionChange?.(selectionToApply);
+        ReportActionComposeUtils.updateNativeSelectionValue(composerRef, selectionToApply.start, selectionToApply.end ?? selectionToApply.start);
 
-            if (options?.isEditingInComposer) {
-                onFocus?.();
-            }
-        },
-        [composerRef, currentEditMessageSelection, onFocus, onSelectionChange, onValueChange],
-    );
+        if (options?.isEditingInComposer) {
+            onFocus?.();
+        }
+    };
 
     useEffect(() => {
         // If the draft message is already being submitted, do nothing.

From 03174f0e8d0c177139106f27e0907e0ad237d9c4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 11 May 2026 09:50:17 +0100
Subject: [PATCH 232/233] refactor: remove manual memos

---
 .../report/ReportActionEditMessageContext.tsx | 68 +++++++++----------
 1 file changed, 31 insertions(+), 37 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionEditMessageContext.tsx b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
index a470d2aac4d7..8da94bc60c03 100644
--- a/src/pages/inbox/report/ReportActionEditMessageContext.tsx
+++ b/src/pages/inbox/report/ReportActionEditMessageContext.tsx
@@ -1,4 +1,4 @@
-import React, {createContext, useCallback, useContext, useState} from 'react';
+import React, {createContext, useContext, useState} from 'react';
 import type {Dispatch, SetStateAction} from 'react';
 import type {OnyxCollection} from 'react-native-onyx';
 import type {TextSelection} from '@components/Composer/types';
@@ -73,46 +73,40 @@ function ReportActionEditMessageContextProvider({reportID, children}: ReportActi
 
     const ancestors = useAncestors(report, shouldExcludeAncestorReportAction);
 
-    const ancestorReportActionsSelector = useCallback(
-        (allReportActions: OnyxCollection<OnyxTypes.ReportActions>) => {
-            if (!allReportActions) {
-                return {};
-            }
-            const result: OnyxCollection<OnyxTypes.ReportActions> = {};
-            for (const ancestor of ancestors) {
-                const key = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`;
-                result[key] = allReportActions[key];
-            }
-            return result;
-        },
-        [ancestors],
-    );
+    const ancestorReportActionsSelector = (allReportActions: OnyxCollection<OnyxTypes.ReportActions>) => {
+        if (!allReportActions) {
+            return {};
+        }
+        const result: OnyxCollection<OnyxTypes.ReportActions> = {};
+        for (const ancestor of ancestors) {
+            const key = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`;
+            result[key] = allReportActions[key];
+        }
+        return result;
+    };
 
     const [ancestorsReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: ancestorReportActionsSelector}, [ancestors]);
 
-    const ancestorDraftSelector = useCallback(
-        (allDrafts: OnyxCollection<OnyxTypes.ReportActionsDrafts>) => {
-            if (!allDrafts) {
-                return {};
-            }
-            const result: OnyxCollection<OnyxTypes.ReportActionsDrafts> = {};
-            if (reportID) {
-                const currentDraftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`;
-                result[currentDraftKey] = allDrafts[currentDraftKey];
-            }
-            for (const ancestor of ancestors) {
-                const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`];
-                const origID = getOriginalReportID(ancestor.report.reportID, ancestor.reportAction, reportActionsForAncestor);
-                if (!origID) {
-                    continue;
-                }
-                const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`;
-                result[draftKey] = allDrafts[draftKey];
+    const ancestorDraftSelector = (allDrafts: OnyxCollection<OnyxTypes.ReportActionsDrafts>) => {
+        if (!allDrafts) {
+            return {};
+        }
+        const result: OnyxCollection<OnyxTypes.ReportActionsDrafts> = {};
+        if (reportID) {
+            const currentDraftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`;
+            result[currentDraftKey] = allDrafts[currentDraftKey];
+        }
+        for (const ancestor of ancestors) {
+            const reportActionsForAncestor = ancestorsReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestor.report.reportID}`];
+            const origID = getOriginalReportID(ancestor.report.reportID, ancestor.reportAction, reportActionsForAncestor);
+            if (!origID) {
+                continue;
             }
-            return result;
-        },
-        [ancestors, ancestorsReportActions, reportID],
-    );
+            const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${origID}`;
+            result[draftKey] = allDrafts[draftKey];
+        }
+        return result;
+    };
 
     const [reportActionDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, {selector: ancestorDraftSelector}, [ancestors, ancestorsReportActions, reportID]);
 

From ec111b2dcd80597691f929aebd2956069a88de37 Mon Sep 17 00:00:00 2001
From: Christoph Pader <chris@margelo.com>
Date: Mon, 11 May 2026 10:14:44 +0100
Subject: [PATCH 233/233] fix: types for `getUpdatedSyncSelection`

---
 .../ReportActionCompose/getUpdatedSyncSelection/index.ts  | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts
index f0f516e4250c..40bb0fb3f939 100644
--- a/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts
+++ b/src/pages/inbox/report/ReportActionCompose/getUpdatedSyncSelection/index.ts
@@ -1,4 +1,8 @@
-// This is a no-op function for non-iOS platforms
+import type GetUpdatedSyncSelection from './types';
+
 const noop = () => undefined;
 
-export default noop;
+// This is a no-op function for non-iOS platforms
+const getUpdatedSyncSelection: GetUpdatedSyncSelection = noop;
+
+export default getUpdatedSyncSelection;
