From e04975911deb57be1af9ea776a288fd5779426c6 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 12 Feb 2026 22:01:13 +0200
Subject: [PATCH 01/15] Pass optimistic reportActionID to onSubmit

---
 src/libs/actions/Report/index.ts              | 32 ++++++++++++++++---
 .../ReportActionCompose.tsx                   |  5 +--
 src/pages/inbox/report/ReportFooter.tsx       |  3 +-
 3 files changed, 33 insertions(+), 7 deletions(-)

diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index 48a6da85f2f4..d5b348e0d80f 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -280,6 +280,7 @@ type AddCommentParams = {
     shouldPlaySound?: boolean;
     isInSidePanel?: boolean;
     pregeneratedResponseParams?: PregeneratedResponseParams;
+    reportActionID?: string;
 };
 
 type AddActionsParams = {
@@ -292,6 +293,7 @@ type AddActionsParams = {
     file?: FileObject;
     isInSidePanel?: boolean;
     pregeneratedResponseParams?: PregeneratedResponseParams;
+    reportActionID?: string;
 };
 
 type AddAttachmentWithCommentParams = {
@@ -609,7 +611,18 @@ function buildOptimisticResolvedFollowups(reportAction: OnyxEntry<ReportAction>)
  * @param isInSidePanel - Whether the comment is being added from the side panel
  * @param pregeneratedResponseParams - Optional params for pre-generated response (API only, no optimistic action - used when response display is delayed)
  */
-function addActions({report, notifyReportID, ancestors, timezoneParam, currentUserAccountID, text = '', file, isInSidePanel = false, pregeneratedResponseParams}: AddActionsParams) {
+function addActions({
+    report,
+    notifyReportID,
+    ancestors,
+    timezoneParam,
+    currentUserAccountID,
+    text = '',
+    file,
+    isInSidePanel = false,
+    pregeneratedResponseParams,
+    reportActionID,
+}: AddActionsParams) {
     if (!report?.reportID) {
         return;
     }
@@ -620,7 +633,7 @@ function addActions({report, notifyReportID, ancestors, timezoneParam, currentUs
     let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT | typeof WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT;
 
     if (text && !file) {
-        const reportComment = buildOptimisticAddCommentReportAction(text, undefined, undefined, undefined, reportID);
+        const reportComment = buildOptimisticAddCommentReportAction(text, undefined, undefined, undefined, reportID, reportActionID);
         reportCommentAction = reportComment.reportAction;
         reportCommentText = reportComment.commentText;
     }
@@ -848,11 +861,22 @@ function addAttachmentWithComment({
 }
 
 /** Add a single comment to a report */
-function addComment({report, notifyReportID, ancestors, text, timezoneParam, currentUserAccountID, shouldPlaySound, isInSidePanel, pregeneratedResponseParams}: AddCommentParams) {
+function addComment({
+    report,
+    notifyReportID,
+    ancestors,
+    text,
+    timezoneParam,
+    currentUserAccountID,
+    shouldPlaySound,
+    isInSidePanel,
+    pregeneratedResponseParams,
+    reportActionID,
+}: AddCommentParams) {
     if (shouldPlaySound) {
         playSound(SOUNDS.DONE);
     }
-    addActions({report, notifyReportID, ancestors, timezoneParam, currentUserAccountID, text, isInSidePanel, pregeneratedResponseParams});
+    addActions({report, notifyReportID, ancestors, timezoneParam, currentUserAccountID, text, isInSidePanel, pregeneratedResponseParams, reportActionID});
 }
 
 function reportActionsExist(reportID: string): boolean {
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 54f82235396b..f81a1fa7a461 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -38,6 +38,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
 import DomUtils from '@libs/DomUtils';
 import FS from '@libs/Fullstory';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import {rand64} from '@libs/NumberUtils';
 import Performance from '@libs/Performance';
 import {getLinkedTransactionID, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
 import {
@@ -91,7 +92,7 @@ type SuggestionsRef = {
 
 type ReportActionComposeProps = Pick<ComposerWithSuggestionsProps, 'reportID' | 'isComposerFullSize' | 'lastReportAction'> & {
     /** A method to call when the form is submitted */
-    onSubmit: (newComment: string) => void;
+    onSubmit: (newComment: string, reportActionID?: string) => void;
 
     /** The report currently being looked at */
     report: OnyxEntry<OnyxTypes.Report>;
@@ -364,7 +365,7 @@ function ReportActionCompose({
                         [CONST.TELEMETRY.ATTRIBUTE_MESSAGE_LENGTH]: newCommentTrimmed.length,
                     },
                 });
-                onSubmit(newCommentTrimmed);
+                onSubmit(newCommentTrimmed, rand64());
             }
         },
         [
diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx
index be3c0bb05e90..c4d58618ef86 100644
--- a/src/pages/inbox/report/ReportFooter.tsx
+++ b/src/pages/inbox/report/ReportFooter.tsx
@@ -187,7 +187,7 @@ function ReportFooter({
     const targetReportAncestors = useAncestors(targetReport);
 
     const onSubmitComment = useCallback(
-        (text: string) => {
+        (text: string, reportActionID?: string) => {
             const isTaskCreated = handleCreateTask(text);
             if (isTaskCreated) {
                 return;
@@ -203,6 +203,7 @@ function ReportFooter({
                 currentUserAccountID: personalDetail.accountID,
                 shouldPlaySound: true,
                 isInSidePanel,
+                reportActionID,
             });
         },
         // eslint-disable-next-line react-hooks/exhaustive-deps

From 8b06a7178d2be0fba47189f7a3a94487a3629cca Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 12 Feb 2026 22:24:05 +0200
Subject: [PATCH 02/15] Measure only when the chat is scrolled to the bottom

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

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index f81a1fa7a461..85ca696a23e7 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -1,6 +1,6 @@
 import lodashDebounce from 'lodash/debounce';
 import noop from 'lodash/noop';
-import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
 import type {BlurEvent, MeasureInWindowOnSuccessCallback, TextInputSelectionChangeEvent} from 'react-native';
 import {View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
@@ -62,6 +62,7 @@ import {
 import {startSpan} from '@libs/telemetry/activeSpans';
 import {getTransactionID, hasReceipt as hasReceiptTransactionUtils} from '@libs/TransactionUtils';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
+import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import AgentZeroProcessingRequestIndicator from '@pages/inbox/report/AgentZeroProcessingRequestIndicator';
 import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime';
 import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator';
@@ -166,6 +167,8 @@ function ReportActionCompose({
         canBeMissing: true,
     });
     const ancestors = useAncestors(transactionThreadReport ?? report);
+    const {scrollPosition} = useContext(ActionListContext);
+
     /**
      * Updates the Highlight state of the composer
      */
@@ -356,15 +359,18 @@ function ReportActionCompose({
                 });
                 attachmentFileRef.current = null;
             } else {
-                Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
-                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 isScrolledToBottom = !scrollPosition?.offset || scrollPosition.offset < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
+                if (isScrolledToBottom) {
+                    Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
+                    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,
+                        },
+                    });
+                }
                 onSubmit(newCommentTrimmed, rand64());
             }
         },
@@ -379,6 +385,7 @@ function ReportActionCompose({
             personalDetail.timezone,
             isInSidePanel,
             onSubmit,
+            scrollPosition,
         ],
     );
 

From 447467a23f9a03497135952b8a18683d6d636b2a Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 12 Feb 2026 22:28:19 +0200
Subject: [PATCH 03/15] Add reportActionID to the spanID

---
 .../report/ReportActionCompose/ReportActionCompose.tsx      | 5 +++--
 src/pages/inbox/report/comment/TextCommentFragment.tsx      | 6 ++++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 85ca696a23e7..fbdd85e4a6bd 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -359,10 +359,11 @@ function ReportActionCompose({
                 });
                 attachmentFileRef.current = null;
             } else {
+                const reportActionID = rand64();
                 const isScrolledToBottom = !scrollPosition?.offset || scrollPosition.offset < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
                 if (isScrolledToBottom) {
                     Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
-                    startSpan(CONST.TELEMETRY.SPAN_SEND_MESSAGE, {
+                    startSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`, {
                         name: 'send-message',
                         op: CONST.TELEMETRY.SPAN_SEND_MESSAGE,
                         attributes: {
@@ -371,7 +372,7 @@ function ReportActionCompose({
                         },
                     });
                 }
-                onSubmit(newCommentTrimmed, rand64());
+                onSubmit(newCommentTrimmed, reportActionID);
             }
         },
         [
diff --git a/src/pages/inbox/report/comment/TextCommentFragment.tsx b/src/pages/inbox/report/comment/TextCommentFragment.tsx
index 6274558145bc..c2bf5fa23889 100644
--- a/src/pages/inbox/report/comment/TextCommentFragment.tsx
+++ b/src/pages/inbox/report/comment/TextCommentFragment.tsx
@@ -65,8 +65,10 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
 
     useEffect(() => {
         Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text});
-        endSpan(CONST.TELEMETRY.SPAN_SEND_MESSAGE);
-    }, [text]);
+        if (reportActionID) {
+            endSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`);
+        }
+    }, [text, reportActionID]);
 
     // If the only difference between fragment.text and fragment.html is <br /> tags and emoji tag
     // on native, we render it as text, not as html

From e79e24182ca15973e829e34a5b62e99242d02cee Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 12 Feb 2026 22:31:35 +0200
Subject: [PATCH 04/15] Cleanup orphaned spans

---
 src/libs/telemetry/activeSpans.ts | 10 +++++++++-
 src/pages/inbox/ReportScreen.tsx  |  5 ++++-
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts
index 6d807b94e4e3..479faa75cae6 100644
--- a/src/libs/telemetry/activeSpans.ts
+++ b/src/libs/telemetry/activeSpans.ts
@@ -52,8 +52,16 @@ function cancelAllSpans() {
     }
 }
 
+function cancelSpansByPrefix(prefix: string) {
+    for (const [spanId] of activeSpans.entries()) {
+        if (spanId.startsWith(prefix)) {
+            cancelSpan(spanId);
+        }
+    }
+}
+
 function getSpan(spanId: string) {
     return activeSpans.get(spanId);
 }
 
-export {startSpan, endSpan, getSpan, cancelSpan, cancelAllSpans};
+export {startSpan, endSpan, getSpan, cancelSpan, cancelAllSpans, cancelSpansByPrefix};
diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index cbf87cb8335a..85df9f7b4d2d 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -87,7 +87,7 @@ import {
     isTaskReport,
     isValidReportIDFromPath,
 } from '@libs/ReportUtils';
-import {cancelSpan} from '@libs/telemetry/activeSpans';
+import {cancelSpan, cancelSpansByPrefix} from '@libs/telemetry/activeSpans';
 import {isNumeric} from '@libs/ValidationUtils';
 import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types';
 import {setShouldShowComposeInput} from '@userActions/Composer';
@@ -652,6 +652,9 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
 
             // We need to cancel telemetry span when user leaves the screen before full report data is loaded
             cancelSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`);
+
+            // Cancel any pending send-message spans to prevent orphaned spans when navigating away
+            cancelSpansByPrefix(CONST.TELEMETRY.SPAN_SEND_MESSAGE);
         };
     }, [reportID]);
 

From 87b4c9717a64ca22c68fa878ba0f65d6d2a23556 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 12 Feb 2026 22:43:11 +0200
Subject: [PATCH 05/15] Prettier

---
 .../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 fbdd85e4a6bd..e325dfbe5ffd 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -62,10 +62,10 @@ import {
 import {startSpan} from '@libs/telemetry/activeSpans';
 import {getTransactionID, hasReceipt as hasReceiptTransactionUtils} from '@libs/TransactionUtils';
 import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
-import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import AgentZeroProcessingRequestIndicator from '@pages/inbox/report/AgentZeroProcessingRequestIndicator';
 import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime';
 import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator';
+import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import {hideEmojiPicker, isActive as isActiveEmojiPickerAction, isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
 import {addAttachmentWithComment, setIsComposerFullSize} from '@userActions/Report';
 import {isBlockedFromConcierge as isBlockedFromConciergeUserAction} from '@userActions/User';

From 506c5a5ec535a12f5b451763a71112b1277ad965 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Fri, 13 Feb 2026 16:30:10 +0200
Subject: [PATCH 06/15] Refactor the ActionListContext

---
 src/hooks/useActionListContextValue.ts              | 13 +++++++++++++
 src/pages/Search/SearchMoneyRequestReportPage.tsx   |  9 +++------
 src/pages/inbox/ReportScreen.tsx                    |  8 +++-----
 src/pages/inbox/ReportScreenContext.ts              |  3 ++-
 .../ReportActionCompose/ReportActionCompose.tsx     |  8 +++++---
 src/pages/inbox/report/ReportActionsList.tsx        |  5 ++++-
 6 files changed, 30 insertions(+), 16 deletions(-)
 create mode 100644 src/hooks/useActionListContextValue.ts

diff --git a/src/hooks/useActionListContextValue.ts b/src/hooks/useActionListContextValue.ts
new file mode 100644
index 000000000000..7f4c1d1a6b78
--- /dev/null
+++ b/src/hooks/useActionListContextValue.ts
@@ -0,0 +1,13 @@
+import {useMemo, useRef, useState} from 'react';
+import type {FlatList} from 'react-native';
+import type {ActionListContextType, ScrollPosition} from '@pages/inbox/ReportScreenContext';
+
+function useActionListContextValue(): ActionListContextType {
+    const flatListRef = useRef<FlatList>(null);
+    const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
+    const scrollOffsetRef = useRef(0);
+
+    return useMemo(() => ({flatListRef, scrollPosition, setScrollPosition, scrollOffsetRef}), [scrollPosition]);
+}
+
+export default useActionListContextValue;
diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx
index 4a56ed8e1000..e69fd238a737 100644
--- a/src/pages/Search/SearchMoneyRequestReportPage.tsx
+++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx
@@ -1,7 +1,6 @@
 import {PortalHost} from '@gorhom/portal';
 import {useIsFocused} from '@react-navigation/native';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {FlatList} from 'react-native';
+import React, {useCallback, useEffect, useMemo, useRef} from 'react';
 import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
 import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
 import DragAndDropProvider from '@components/DragAndDrop/Provider';
@@ -10,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
 import {useSearchContext} from '@components/Search/SearchContext';
 import useShowSuperWideRHPVersion from '@components/WideRHPContextProvider/useShowSuperWideRHPVersion';
 import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper';
+import useActionListContextValue from '@hooks/useActionListContextValue';
 import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay';
 import useNetwork from '@hooks/useNetwork';
 import useOnyx from '@hooks/useOnyx';
@@ -36,7 +36,6 @@ import {isValidReportIDFromPath} from '@libs/ReportUtils';
 import {isDefaultAvatar, isLetterAvatar, isPresetAvatar} from '@libs/UserAvatarUtils';
 import Navigation from '@navigation/Navigation';
 import ReactionListWrapper from '@pages/inbox/ReactionListWrapper';
-import type {ActionListContextType, ScrollPosition} from '@pages/inbox/ReportScreenContext';
 import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import {createTransactionThreadReport, openReport, updateLastVisitTime} from '@userActions/Report';
 import CONST from '@src/CONST';
@@ -99,9 +98,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) {
 
     const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute, isReportArchived);
 
-    const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
-    const flatListRef = useRef<FlatList>(null);
-    const actionListValue = useMemo((): ActionListContextType => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]);
+    const actionListValue = useActionListContextValue();
 
     const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`, {canBeMissing: true});
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx
index 85df9f7b4d2d..c2e15f7ab163 100644
--- a/src/pages/inbox/ReportScreen.tsx
+++ b/src/pages/inbox/ReportScreen.tsx
@@ -3,7 +3,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native';
 import {accountIDSelector} from '@selectors/Session';
 import {deepEqual} from 'fast-equals';
 import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {FlatList, ViewStyle} from 'react-native';
+import type {ViewStyle} from 'react-native';
 // We use Animated for all functionality related to wide RHP to make it easier
 // to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated.
 // eslint-disable-next-line no-restricted-imports
@@ -22,6 +22,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
 import ScrollView from '@components/ScrollView';
 import useShowWideRHPVersion from '@components/WideRHPContextProvider/useShowWideRHPVersion';
 import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper';
+import useActionListContextValue from '@hooks/useActionListContextValue';
 import useAppFocusEvent from '@hooks/useAppFocusEvent';
 import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
 import {useCurrentReportIDState} from '@hooks/useCurrentReportID';
@@ -113,7 +114,6 @@ import useReportWasDeleted from './hooks/useReportWasDeleted';
 import ReactionListWrapper from './ReactionListWrapper';
 import ReportActionsView from './report/ReportActionsView';
 import ReportFooter from './report/ReportFooter';
-import type {ActionListContextType, ScrollPosition} from './ReportScreenContext';
 import {ActionListContext} from './ReportScreenContext';
 
 type ReportScreenNavigationProps =
@@ -167,7 +167,6 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
     const prevIsFocused = usePrevious(isFocused);
     const [firstRender, setFirstRender] = useState(true);
     const isSkippingOpenReport = useRef(false);
-    const flatListRef = useRef<FlatList>(null);
     const hasCreatedLegacyThreadRef = useRef(false);
     const {isBetaEnabled} = usePermissions();
     const {isOffline} = useNetwork();
@@ -317,7 +316,6 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
     const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${linkedAction?.childReportID}`, {canBeMissing: true});
 
     const [isBannerVisible, setIsBannerVisible] = useState(true);
-    const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
 
     const viewportOffsetTop = useViewportOffsetTop();
 
@@ -879,7 +877,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr
         };
     }, [report?.reportID, didSubscribeToReportLeavingEvents, reportIDFromRoute, report?.pendingFields, currentUserAccountID]);
 
-    const actionListValue = useMemo((): ActionListContextType => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]);
+    const actionListValue = useActionListContextValue();
 
     // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache.
     useEffect(() => {
diff --git a/src/pages/inbox/ReportScreenContext.ts b/src/pages/inbox/ReportScreenContext.ts
index fc1d5ab6de74..ec56c097eeeb 100644
--- a/src/pages/inbox/ReportScreenContext.ts
+++ b/src/pages/inbox/ReportScreenContext.ts
@@ -21,10 +21,11 @@ type ActionListContextType = {
     flatListRef: FlatListRefType;
     scrollPosition: ScrollPosition | null;
     setScrollPosition: (position: {offset: number}) => void;
+    scrollOffsetRef: RefObject<number>;
 };
 type ReactionListContextType = RefObject<ReactionListRef | null> | null;
 
-const ActionListContext = createContext<ActionListContextType>({flatListRef: null, scrollPosition: null, setScrollPosition: () => {}});
+const ActionListContext = createContext<ActionListContextType>({flatListRef: null, scrollPosition: null, setScrollPosition: () => {}, scrollOffsetRef: {current: 0}});
 const ReactionListContext = createContext<ReactionListContextType>(null);
 
 export {ActionListContext, ReactionListContext};
diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index e325dfbe5ffd..b66f548f2708 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -167,7 +167,7 @@ function ReportActionCompose({
         canBeMissing: true,
     });
     const ancestors = useAncestors(transactionThreadReport ?? report);
-    const {scrollPosition} = useContext(ActionListContext);
+    const {scrollOffsetRef} = useContext(ActionListContext);
 
     /**
      * Updates the Highlight state of the composer
@@ -360,7 +360,9 @@ function ReportActionCompose({
                 attachmentFileRef.current = null;
             } else {
                 const reportActionID = rand64();
-                const isScrolledToBottom = !scrollPosition?.offset || scrollPosition.offset < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
+
+                // The list is inverted, so an offset near 0 means the user is at the bottom (newest messages visible).
+                const isScrolledToBottom = scrollOffsetRef.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
                 if (isScrolledToBottom) {
                     Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
                     startSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`, {
@@ -386,7 +388,7 @@ function ReportActionCompose({
             personalDetail.timezone,
             isInSidePanel,
             onSubmit,
-            scrollPosition,
+            scrollOffsetRef,
         ],
     );
 
diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index 2824662771a4..5405ad6fddcc 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -2,7 +2,7 @@ import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/Vir
 import {useIsFocused, useRoute} from '@react-navigation/native';
 import {isUserValidatedSelector} from '@selectors/Account';
 import {tierNameSelector} from '@selectors/UserWallet';
-import React, {memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
 import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
 import {DeviceEventEmitter, InteractionManager, View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
@@ -61,6 +61,7 @@ import {
 } from '@libs/ReportUtils';
 import Visibility from '@libs/Visibility';
 import type {ReportsSplitNavigatorParamList} from '@navigation/types';
+import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import variables from '@styles/variables';
 import {openReport, readNewestAction, subscribeToNewActionEvent} from '@userActions/Report';
 import CONST from '@src/CONST';
@@ -172,6 +173,7 @@ function ReportActionsList({
     const {isOffline, lastOfflineAt, lastOnlineAt} = useNetworkWithOfflineStatus();
     const route = useRoute<PlatformStackRouteProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>>();
     const reportScrollManager = useReportScrollManager();
+    const {scrollOffsetRef} = useContext(ActionListContext);
     const userActiveSince = useRef<string>(DateUtils.getDBTime());
     const lastMessageTime = useRef<string | null>(null);
     const [isVisible, setIsVisible] = useState(Visibility.isVisible);
@@ -365,6 +367,7 @@ function ReportActionsList({
         isInverted: true,
         onTrackScrolling: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
             scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y;
+            scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
             onScroll?.(event);
             if (shouldScrollToEndAfterLayout && (!hasCreatedActionAdded || isOffline)) {
                 setShouldScrollToEndAfterLayout(false);

From f756382f3576e32e5d8e44f48ab9256527a5d1b4 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Fri, 13 Feb 2026 16:36:31 +0200
Subject: [PATCH 07/15] send-message span into a separate useEffect

---
 src/pages/inbox/report/comment/TextCommentFragment.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/comment/TextCommentFragment.tsx b/src/pages/inbox/report/comment/TextCommentFragment.tsx
index c2bf5fa23889..75daa18aa0e6 100644
--- a/src/pages/inbox/report/comment/TextCommentFragment.tsx
+++ b/src/pages/inbox/report/comment/TextCommentFragment.tsx
@@ -65,10 +65,12 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
 
     useEffect(() => {
         Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text});
+    }, [text]);
+    useEffect(() => {
         if (reportActionID) {
             endSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`);
         }
-    }, [text, reportActionID]);
+    }, [reportActionID]);
 
     // If the only difference between fragment.text and fragment.html is <br /> tags and emoji tag
     // on native, we render it as text, not as html

From b5988646ae757c5c7bcc275fd17ab4f716701a0f Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Fri, 13 Feb 2026 16:49:17 +0200
Subject: [PATCH 08/15] Fix ts and lint checks

---
 src/pages/inbox/report/comment/TextCommentFragment.tsx | 5 +++--
 tests/perf-test/ReportActionsList.perf-test.tsx        | 2 +-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/comment/TextCommentFragment.tsx b/src/pages/inbox/report/comment/TextCommentFragment.tsx
index 75daa18aa0e6..1a692b951747 100644
--- a/src/pages/inbox/report/comment/TextCommentFragment.tsx
+++ b/src/pages/inbox/report/comment/TextCommentFragment.tsx
@@ -67,9 +67,10 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM
         Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text});
     }, [text]);
     useEffect(() => {
-        if (reportActionID) {
-            endSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`);
+        if (!reportActionID) {
+            return;
         }
+        endSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`);
     }, [reportActionID]);
 
     // If the only difference between fragment.text and fragment.html is <br /> tags and emoji tag
diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx
index 9886df007687..309e5c289b44 100644
--- a/tests/perf-test/ReportActionsList.perf-test.tsx
+++ b/tests/perf-test/ReportActionsList.perf-test.tsx
@@ -70,7 +70,7 @@ beforeAll(() =>
 const mockOnLayout = jest.fn();
 const mockOnScroll = jest.fn();
 const mockLoadChats = jest.fn();
-const mockRef = {current: null, flatListRef: null, scrollPosition: null, setScrollPosition: () => {}};
+const mockRef = {current: null, flatListRef: null, scrollPosition: null, setScrollPosition: () => {}, scrollOffsetRef: {current: 0}};
 
 const TEST_USER_ACCOUNT_ID = 1;
 const TEST_USER_LOGIN = 'test@test.com';

From 9f5a0fd970198843231b156f7df04ba01da476e9 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Fri, 13 Feb 2026 17:17:15 +0200
Subject: [PATCH 09/15] Cleanup spans in SearchMoneyRequestReportPage

---
 src/pages/Search/SearchMoneyRequestReportPage.tsx | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx
index e69fd238a737..6e545f5b7584 100644
--- a/src/pages/Search/SearchMoneyRequestReportPage.tsx
+++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx
@@ -33,6 +33,7 @@ import {
     isMoneyRequestAction,
 } from '@libs/ReportActionsUtils';
 import {isValidReportIDFromPath} from '@libs/ReportUtils';
+import {cancelSpansByPrefix} from '@libs/telemetry/activeSpans';
 import {isDefaultAvatar, isLetterAvatar, isPresetAvatar} from '@libs/UserAvatarUtils';
 import Navigation from '@navigation/Navigation';
 import ReactionListWrapper from '@pages/inbox/ReactionListWrapper';
@@ -194,6 +195,11 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) {
 
     useEffect(() => {
         hasCreatedLegacyThreadRef.current = false;
+
+        return () => {
+            // Cancel any pending send-message spans to prevent orphaned spans when navigating away
+            cancelSpansByPrefix(CONST.TELEMETRY.SPAN_SEND_MESSAGE);
+        };
     }, [reportIDFromRoute]);
 
     // Create transaction thread for legacy transactions that don't have one yet.

From 334bca96b5cdd1db1ad4b84adc41666443aa90bd Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Mon, 16 Feb 2026 19:01:16 +0200
Subject: [PATCH 10/15] Rename variable

---
 src/libs/telemetry/activeSpans.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts
index 479faa75cae6..57694ff3b5c0 100644
--- a/src/libs/telemetry/activeSpans.ts
+++ b/src/libs/telemetry/activeSpans.ts
@@ -53,9 +53,9 @@ function cancelAllSpans() {
 }
 
 function cancelSpansByPrefix(prefix: string) {
-    for (const [spanId] of activeSpans.entries()) {
-        if (spanId.startsWith(prefix)) {
-            cancelSpan(spanId);
+    for (const [spanID] of activeSpans.entries()) {
+        if (spanID.startsWith(prefix)) {
+            cancelSpan(spanID);
         }
     }
 }

From a94dcb2530ef7ecd9126cda3cba21528e1fa5a22 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Mon, 16 Feb 2026 19:15:12 +0200
Subject: [PATCH 11/15] Remove unnecessary useMemo

---
 src/hooks/useActionListContextValue.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/hooks/useActionListContextValue.ts b/src/hooks/useActionListContextValue.ts
index 7f4c1d1a6b78..b25d3b9fce2d 100644
--- a/src/hooks/useActionListContextValue.ts
+++ b/src/hooks/useActionListContextValue.ts
@@ -1,4 +1,4 @@
-import {useMemo, useRef, useState} from 'react';
+import {useRef, useState} from 'react';
 import type {FlatList} from 'react-native';
 import type {ActionListContextType, ScrollPosition} from '@pages/inbox/ReportScreenContext';
 
@@ -7,7 +7,7 @@ function useActionListContextValue(): ActionListContextType {
     const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
     const scrollOffsetRef = useRef(0);
 
-    return useMemo(() => ({flatListRef, scrollPosition, setScrollPosition, scrollOffsetRef}), [scrollPosition]);
+    return {flatListRef, scrollPosition, setScrollPosition, scrollOffsetRef};
 }
 
 export default useActionListContextValue;

From ec0e4c449ca89ffc0ccebd568a4cc0d10e2416a9 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Mon, 16 Feb 2026 19:51:17 +0200
Subject: [PATCH 12/15] refactor: consolidate scroll state into refs, remove
 duplicate tracking

---
 src/hooks/useActionListContextValue.ts        |  6 +--
 .../useReportScrollManager/index.native.ts    | 47 ++++++++-----------
 src/hooks/useReportScrollManager/index.ts     | 44 ++++++++---------
 src/pages/inbox/ReportScreenContext.ts        |  5 +-
 src/pages/inbox/report/ReportActionsList.tsx  | 16 +++----
 .../perf-test/ReportActionsList.perf-test.tsx |  2 +-
 6 files changed, 51 insertions(+), 69 deletions(-)

diff --git a/src/hooks/useActionListContextValue.ts b/src/hooks/useActionListContextValue.ts
index b25d3b9fce2d..ba73ff804cc0 100644
--- a/src/hooks/useActionListContextValue.ts
+++ b/src/hooks/useActionListContextValue.ts
@@ -1,13 +1,13 @@
-import {useRef, useState} from 'react';
+import {useRef} from 'react';
 import type {FlatList} from 'react-native';
 import type {ActionListContextType, ScrollPosition} from '@pages/inbox/ReportScreenContext';
 
 function useActionListContextValue(): ActionListContextType {
     const flatListRef = useRef<FlatList>(null);
-    const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
+    const scrollPositionRef = useRef<ScrollPosition>({});
     const scrollOffsetRef = useRef(0);
 
-    return {flatListRef, scrollPosition, setScrollPosition, scrollOffsetRef};
+    return {flatListRef, scrollPositionRef, scrollOffsetRef};
 }
 
 export default useActionListContextValue;
diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts
index 39e5a1b66ee7..180196dd0b3e 100644
--- a/src/hooks/useReportScrollManager/index.native.ts
+++ b/src/hooks/useReportScrollManager/index.native.ts
@@ -1,44 +1,39 @@
-import {useCallback, useContext} from 'react';
+import {useContext} from 'react';
 // eslint-disable-next-line no-restricted-imports
 import type {ScrollView} from 'react-native';
 import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import type ReportScrollManagerData from './types';
 
 function useReportScrollManager(): ReportScrollManagerData {
-    const {flatListRef, setScrollPosition} = useContext(ActionListContext);
+    const {flatListRef, scrollPositionRef} = useContext(ActionListContext);
 
     /**
      * Scroll to the provided index.
      */
-    const scrollToIndex = useCallback(
-        (index: number) => {
-            if (!flatListRef?.current) {
-                return;
-            }
-
-            flatListRef.current.scrollToIndex({index});
-        },
-        [flatListRef],
-    );
+    const scrollToIndex = (index: number) => {
+        if (!flatListRef?.current) {
+            return;
+        }
+        flatListRef.current.scrollToIndex({index});
+    };
 
     /**
      * Scroll to the bottom of the inverted FlatList.
      * When FlatList is inverted it's "bottom" is really it's top
      */
-    const scrollToBottom = useCallback(() => {
+    const scrollToBottom = () => {
         if (!flatListRef?.current) {
             return;
         }
 
-        setScrollPosition({offset: 0});
-
+        scrollPositionRef.current = {offset: 0};
         flatListRef.current?.scrollToOffset({animated: false, offset: 0});
-    }, [flatListRef, setScrollPosition]);
+    };
 
     /**
      * Scroll to the end of the FlatList.
      */
-    const scrollToEnd = useCallback(() => {
+    const scrollToEnd = () => {
         if (!flatListRef?.current) {
             return;
         }
@@ -51,18 +46,14 @@ function useReportScrollManager(): ReportScrollManagerData {
         }
 
         flatListRef.current.scrollToEnd({animated: false});
-    }, [flatListRef]);
+    };
 
-    const scrollToOffset = useCallback(
-        (offset: number) => {
-            if (!flatListRef?.current) {
-                return;
-            }
-
-            flatListRef.current.scrollToOffset({offset, animated: false});
-        },
-        [flatListRef],
-    );
+    const scrollToOffset = (offset: number) => {
+        if (!flatListRef?.current) {
+            return;
+        }
+        flatListRef.current.scrollToOffset({offset, animated: false});
+    };
 
     return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToEnd, scrollToOffset};
 }
diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts
index 30c383e62c2f..6b888584887c 100644
--- a/src/hooks/useReportScrollManager/index.ts
+++ b/src/hooks/useReportScrollManager/index.ts
@@ -1,4 +1,4 @@
-import {useCallback, useContext} from 'react';
+import {useContext} from 'react';
 import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import type ReportScrollManagerData from './types';
 
@@ -8,50 +8,44 @@ function useReportScrollManager(): ReportScrollManagerData {
     /**
      * Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because
      */
-    const scrollToIndex = useCallback(
-        (index: number, isEditing?: boolean) => {
-            if (!flatListRef?.current || isEditing) {
-                return;
-            }
+    const scrollToIndex = (index: number, isEditing?: boolean) => {
+        if (!flatListRef?.current || isEditing) {
+            return;
+        }
 
-            flatListRef.current.scrollToIndex({index, animated: true});
-        },
-        [flatListRef],
-    );
+        flatListRef.current.scrollToIndex({index, animated: true});
+    };
 
     /**
      * Scroll to the bottom of the inverted FlatList.
      * When FlatList is inverted it's "bottom" is really it's top
      */
-    const scrollToBottom = useCallback(() => {
+    const scrollToBottom = () => {
         if (!flatListRef?.current) {
             return;
         }
 
         flatListRef.current.scrollToOffset({animated: false, offset: 0});
-    }, [flatListRef]);
+    };
 
     /**
      * Scroll to the end of the FlatList.
      */
-    const scrollToEnd = useCallback(() => {
+    const scrollToEnd = () => {
         if (!flatListRef?.current) {
             return;
         }
 
         flatListRef.current.scrollToEnd({animated: false});
-    }, [flatListRef]);
-
-    const scrollToOffset = useCallback(
-        (offset: number) => {
-            if (!flatListRef?.current) {
-                return;
-            }
-
-            flatListRef.current.scrollToOffset({animated: true, offset});
-        },
-        [flatListRef],
-    );
+    };
+
+    const scrollToOffset = (offset: number) => {
+        if (!flatListRef?.current) {
+            return;
+        }
+
+        flatListRef.current.scrollToOffset({animated: true, offset});
+    };
 
     return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToEnd, scrollToOffset};
 }
diff --git a/src/pages/inbox/ReportScreenContext.ts b/src/pages/inbox/ReportScreenContext.ts
index ec56c097eeeb..50b1e3e707f9 100644
--- a/src/pages/inbox/ReportScreenContext.ts
+++ b/src/pages/inbox/ReportScreenContext.ts
@@ -19,13 +19,12 @@ type ScrollPosition = {offset?: number};
 
 type ActionListContextType = {
     flatListRef: FlatListRefType;
-    scrollPosition: ScrollPosition | null;
-    setScrollPosition: (position: {offset: number}) => void;
+    scrollPositionRef: RefObject<ScrollPosition>;
     scrollOffsetRef: RefObject<number>;
 };
 type ReactionListContextType = RefObject<ReactionListRef | null> | null;
 
-const ActionListContext = createContext<ActionListContextType>({flatListRef: null, scrollPosition: null, setScrollPosition: () => {}, scrollOffsetRef: {current: 0}});
+const ActionListContext = createContext<ActionListContextType>({flatListRef: null, scrollPositionRef: {current: {}}, scrollOffsetRef: {current: 0}});
 const ReactionListContext = createContext<ReactionListContextType>(null);
 
 export {ActionListContext, ReactionListContext};
diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index e2f0f8f31106..4b1b8ab15df9 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -216,7 +216,6 @@ function ReportActionsList({
         return unsubscribe;
     }, []);
 
-    const scrollingVerticalOffset = useRef(0);
     const readActionSkipped = useRef(false);
     const hasHeaderRendered = useRef(false);
     const linkedReportActionID = route?.params?.reportActionID;
@@ -295,7 +294,7 @@ function ReportActionsList({
                     currentUserAccountID,
                     prevSortedVisibleReportActionsObjects,
                     unreadMarkerTime,
-                    scrollingVerticalOffset: scrollingVerticalOffset.current,
+                    scrollingVerticalOffset: scrollOffsetRef.current,
                     prevUnreadMarkerReportActionID: prevUnreadMarkerReportActionID.current,
                 });
             if (shouldDisplayNewMarker) {
@@ -304,7 +303,7 @@ function ReportActionsList({
         }
 
         return [null, -1];
-    }, [currentUserAccountID, isAnonymousUser, earliestReceivedOfflineMessageIndex, prevSortedVisibleReportActionsObjects, sortedVisibleReportActions, unreadMarkerTime]);
+    }, [currentUserAccountID, isAnonymousUser, earliestReceivedOfflineMessageIndex, prevSortedVisibleReportActionsObjects, scrollOffsetRef, sortedVisibleReportActions, unreadMarkerTime]);
     prevUnreadMarkerReportActionID.current = unreadMarkerReportActionID;
 
     /**
@@ -361,12 +360,11 @@ function ReportActionsList({
 
     const {isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible, trackVerticalScrolling, onViewableItemsChanged} = useReportUnreadMessageScrollTracking({
         reportID: report.reportID,
-        currentVerticalScrollingOffsetRef: scrollingVerticalOffset,
+        currentVerticalScrollingOffsetRef: scrollOffsetRef,
         readActionSkippedRef: readActionSkipped,
         unreadMarkerReportActionIndex,
         isInverted: true,
         onTrackScrolling: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
-            scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y;
             scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
             onScroll?.(event);
             if (shouldScrollToEndAfterLayout && (!hasCreatedActionAdded || isOffline)) {
@@ -377,7 +375,7 @@ function ReportActionsList({
 
     useEffect(() => {
         if (
-            scrollingVerticalOffset.current < AUTOSCROLL_TO_TOP_THRESHOLD &&
+            scrollOffsetRef.current < AUTOSCROLL_TO_TOP_THRESHOLD &&
             previousLastIndex.current !== lastActionIndex &&
             reportActionSize.current !== sortedVisibleReportActions.length &&
             hasNewestReportAction
@@ -387,7 +385,7 @@ function ReportActionsList({
         }
         previousLastIndex.current = lastActionIndex;
         reportActionSize.current = sortedVisibleReportActions.length;
-    }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction, linkedReportActionID, setIsFloatingMessageCounterVisible]);
+    }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction, linkedReportActionID, setIsFloatingMessageCounterVisible, scrollOffsetRef]);
 
     useEffect(() => {
         const shouldTriggerScroll = shouldFocusToTopOnMount && prevHasCreatedActionAdded && !hasCreatedActionAdded;
@@ -417,7 +415,7 @@ function ReportActionsList({
             // Currently, there's no programmatic way to dismiss the notification center panel.
             // To handle this, we use the 'referrer' parameter to check if the current navigation is triggered from a notification.
             const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION;
-            if ((isVisible || isFromNotification) && scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) {
+            if ((isVisible || isFromNotification) && scrollOffsetRef.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) {
                 readNewestAction(report.reportID);
                 if (isFromNotification) {
                     Navigation.setParams({referrer: undefined});
@@ -448,7 +446,7 @@ function ReportActionsList({
         lastMessageTime.current = null;
 
         const isArchivedReport = isArchivedNonExpenseReport(report, isReportArchived);
-        const hasNewMessagesInView = scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
+        const hasNewMessagesInView = scrollOffsetRef.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
         const hasUnreadReportAction = sortedVisibleReportActions.some(
             (reportAction) =>
                 newMessageTimeReference &&
diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx
index 309e5c289b44..72122f8bf02a 100644
--- a/tests/perf-test/ReportActionsList.perf-test.tsx
+++ b/tests/perf-test/ReportActionsList.perf-test.tsx
@@ -70,7 +70,7 @@ beforeAll(() =>
 const mockOnLayout = jest.fn();
 const mockOnScroll = jest.fn();
 const mockLoadChats = jest.fn();
-const mockRef = {current: null, flatListRef: null, scrollPosition: null, setScrollPosition: () => {}, scrollOffsetRef: {current: 0}};
+const mockRef = {current: null, flatListRef: null, scrollPositionRef: {current: {}}, scrollOffsetRef: {current: 0}};
 
 const TEST_USER_ACCOUNT_ID = 1;
 const TEST_USER_LOGIN = 'test@test.com';

From 27219ed2c0dd2d9698d7aec56754ab41a1185585 Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Mon, 16 Feb 2026 20:34:34 +0200
Subject: [PATCH 13/15] fix: wire scrollOffsetRef in
 MoneyRequestReportActionsList for scroll guard

---
 .../MoneyRequestReportView/MoneyRequestReportActionsList.tsx | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
index 4b07cd925b7b..1c367448f525 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
@@ -4,7 +4,7 @@ import {useIsFocused, useRoute} from '@react-navigation/native';
 import {isUserValidatedSelector} from '@selectors/Account';
 import {tierNameSelector} from '@selectors/UserWallet';
 import isEmpty from 'lodash/isEmpty';
-import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
 import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
 import {DeviceEventEmitter, InteractionManager, View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
@@ -59,6 +59,7 @@ import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';
 import {isTransactionPendingDelete} from '@libs/TransactionUtils';
 import Visibility from '@libs/Visibility';
 import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute';
+import {ActionListContext} from '@pages/inbox/ReportScreenContext';
 import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
 import getInitialNumToRender from '@pages/inbox/report/getInitialNumReportActionsToRender';
 import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer';
@@ -261,6 +262,7 @@ function MoneyRequestReportActionsList({
     const lastActionIndex = lastAction?.reportActionID;
     const previousLastIndex = useRef(lastActionIndex);
 
+    const {scrollOffsetRef} = useContext(ActionListContext);
     const scrollingVerticalBottomOffset = useRef(0);
     const scrollingVerticalTopOffset = useRef(0);
     const wrapperViewRef = useRef<View>(null);
@@ -449,6 +451,7 @@ function MoneyRequestReportActionsList({
              * Diff == (height of all items in the list) - (height of the layout with the list) - (how far user scrolled)
              */
             scrollingVerticalBottomOffset.current = fullContentHeight - layoutMeasurement.height - contentOffset.y;
+            scrollOffsetRef.current = scrollingVerticalBottomOffset.current;
 
             // We additionally track the top offset to be able to scroll to the new transaction when it's added
             scrollingVerticalTopOffset.current = contentOffset.y;

From d8716a8da0aca51745261f74dd6da9b896a9267f Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 19 Feb 2026 00:36:43 +0200
Subject: [PATCH 14/15] Add clarifying comment for pre-generated reportActionID

Co-authored-by: Cursor <cursoragent@cursor.com>
---
 .../inbox/report/ReportActionCompose/ReportActionCompose.tsx     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
index 383ad1805946..203db4db4042 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -360,6 +360,7 @@ function ReportActionCompose({
                 });
                 attachmentFileRef.current = null;
             } else {
+                // Pre-generate the optimistic reportActionID so we can correlate the Sentry send-message span with the exact message
                 const reportActionID = rand64();
 
                 // The list is inverted, so an offset near 0 means the user is at the bottom (newest messages visible).

From 106a45d787dc1f3459e1704aba63208d59dd166c Mon Sep 17 00:00:00 2001
From: Cristi Paval <cristi@expensify.com>
Date: Thu, 19 Feb 2026 12:46:21 +0200
Subject: [PATCH 15/15] Rename reportActionID to optimisticReportActionID in
 composer

Co-authored-by: Cursor <cursoragent@cursor.com>
---
 .../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 203db4db4042..336512880c38 100644
--- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx
@@ -360,14 +360,14 @@ function ReportActionCompose({
                 });
                 attachmentFileRef.current = null;
             } else {
-                // Pre-generate the optimistic reportActionID so we can correlate the Sentry send-message span with the exact message
-                const reportActionID = rand64();
+                // Pre-generate the reportActionID so we can correlate the Sentry send-message span with the exact message
+                const optimisticReportActionID = rand64();
 
                 // The list is inverted, so an offset near 0 means the user is at the bottom (newest messages visible).
                 const isScrolledToBottom = scrollOffsetRef.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
                 if (isScrolledToBottom) {
                     Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
-                    startSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`, {
+                    startSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${optimisticReportActionID}`, {
                         name: 'send-message',
                         op: CONST.TELEMETRY.SPAN_SEND_MESSAGE,
                         attributes: {
@@ -376,7 +376,7 @@ function ReportActionCompose({
                         },
                     });
                 }
-                onSubmit(newCommentTrimmed, reportActionID);
+                onSubmit(newCommentTrimmed, optimisticReportActionID);
             }
         },
         [
