diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 7d3f7e3e341d2..0181bb0114ff5 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'; @@ -60,6 +60,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'; @@ -293,6 +294,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(null); @@ -481,6 +483,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; diff --git a/src/hooks/useActionListContextValue.ts b/src/hooks/useActionListContextValue.ts new file mode 100644 index 0000000000000..ba73ff804cc07 --- /dev/null +++ b/src/hooks/useActionListContextValue.ts @@ -0,0 +1,13 @@ +import {useRef} from 'react'; +import type {FlatList} from 'react-native'; +import type {ActionListContextType, ScrollPosition} from '@pages/inbox/ReportScreenContext'; + +function useActionListContextValue(): ActionListContextType { + const flatListRef = useRef(null); + const scrollPositionRef = useRef({}); + const scrollOffsetRef = useRef(0); + + return {flatListRef, scrollPositionRef, scrollOffsetRef}; +} + +export default useActionListContextValue; diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts index 39e5a1b66ee7d..180196dd0b3ed 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 30c383e62c2f2..6b888584887cf 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/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index f0cf692c349c6..b858d24167129 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) * @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/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index 6d807b94e4e30..57694ff3b5c09 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/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 94ee399de7d0e..a139fef8e4fa3 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'; @@ -33,10 +33,10 @@ import { isMoneyRequestAction, } from '@libs/ReportActionsUtils'; import {isMoneyRequestReport, 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'; -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'; @@ -121,9 +121,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute, isReportArchived); - const [scrollPosition, setScrollPosition] = useState({}); - const flatListRef = useRef(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}); @@ -219,6 +217,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. diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index cbf87cb8335a1..c2e15f7ab1638 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'; @@ -87,7 +88,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'; @@ -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(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({}); const viewportOffsetTop = useViewportOffsetTop(); @@ -652,6 +650,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]); @@ -876,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 fc1d5ab6de748..50b1e3e707f90 100644 --- a/src/pages/inbox/ReportScreenContext.ts +++ b/src/pages/inbox/ReportScreenContext.ts @@ -19,12 +19,12 @@ type ScrollPosition = {offset?: number}; type ActionListContextType = { flatListRef: FlatListRefType; - scrollPosition: ScrollPosition | null; - setScrollPosition: (position: {offset: number}) => void; + scrollPositionRef: RefObject; + scrollOffsetRef: RefObject; }; type ReactionListContextType = RefObject | null; -const ActionListContext = createContext({flatListRef: null, scrollPosition: null, setScrollPosition: () => {}}); +const ActionListContext = createContext({flatListRef: null, scrollPositionRef: {current: {}}, scrollOffsetRef: {current: 0}}); const ReactionListContext = createContext(null); export {ActionListContext, ReactionListContext}; diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx index 52d8243eb8bb8..336512880c385 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'; @@ -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 { @@ -64,6 +65,7 @@ import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutsi 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'; @@ -91,7 +93,7 @@ type SuggestionsRef = { type ReportActionComposeProps = Pick & { /** 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; @@ -166,6 +168,8 @@ function ReportActionCompose({ canBeMissing: true, }); const ancestors = useAncestors(transactionThreadReport ?? report); + const {scrollOffsetRef} = useContext(ActionListContext); + /** * Updates the Highlight state of the composer */ @@ -356,16 +360,23 @@ 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, - }, - }); - onSubmit(newCommentTrimmed); + // 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}_${optimisticReportActionID}`, { + 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, optimisticReportActionID); } }, [ @@ -379,6 +390,7 @@ function ReportActionCompose({ personalDetail.timezone, isInSidePanel, onSubmit, + scrollOffsetRef, ], ); diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 58743c41886a3..4b1b8ab15df90 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>(); const reportScrollManager = useReportScrollManager(); + const {scrollOffsetRef} = useContext(ActionListContext); const userActiveSince = useRef(DateUtils.getDBTime()); const lastMessageTime = useRef(null); const [isVisible, setIsVisible] = useState(Visibility.isVisible); @@ -214,7 +216,6 @@ function ReportActionsList({ return unsubscribe; }, []); - const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); const hasHeaderRendered = useRef(false); const linkedReportActionID = route?.params?.reportActionID; @@ -293,7 +294,7 @@ function ReportActionsList({ currentUserAccountID, prevSortedVisibleReportActionsObjects, unreadMarkerTime, - scrollingVerticalOffset: scrollingVerticalOffset.current, + scrollingVerticalOffset: scrollOffsetRef.current, prevUnreadMarkerReportActionID: prevUnreadMarkerReportActionID.current, }); if (shouldDisplayNewMarker) { @@ -302,7 +303,7 @@ function ReportActionsList({ } return [null, -1]; - }, [currentUserAccountID, isAnonymousUser, earliestReceivedOfflineMessageIndex, prevSortedVisibleReportActionsObjects, sortedVisibleReportActions, unreadMarkerTime]); + }, [currentUserAccountID, isAnonymousUser, earliestReceivedOfflineMessageIndex, prevSortedVisibleReportActionsObjects, scrollOffsetRef, sortedVisibleReportActions, unreadMarkerTime]); prevUnreadMarkerReportActionID.current = unreadMarkerReportActionID; /** @@ -359,12 +360,12 @@ function ReportActionsList({ const {isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible, trackVerticalScrolling, onViewableItemsChanged} = useReportUnreadMessageScrollTracking({ reportID: report.reportID, - currentVerticalScrollingOffsetRef: scrollingVerticalOffset, + currentVerticalScrollingOffsetRef: scrollOffsetRef, readActionSkippedRef: readActionSkipped, unreadMarkerReportActionIndex, isInverted: true, onTrackScrolling: (event: NativeSyntheticEvent) => { - scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; + scrollOffsetRef.current = event.nativeEvent.contentOffset.y; onScroll?.(event); if (shouldScrollToEndAfterLayout && (!hasCreatedActionAdded || isOffline)) { setShouldScrollToEndAfterLayout(false); @@ -374,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 @@ -384,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; @@ -414,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}); @@ -445,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/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx index be3c0bb05e901..c4d58618ef86e 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 diff --git a/src/pages/inbox/report/comment/TextCommentFragment.tsx b/src/pages/inbox/report/comment/TextCommentFragment.tsx index 6274558145bc6..1a692b9517473 100644 --- a/src/pages/inbox/report/comment/TextCommentFragment.tsx +++ b/src/pages/inbox/report/comment/TextCommentFragment.tsx @@ -65,8 +65,13 @@ function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsM useEffect(() => { Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text}); - endSpan(CONST.TELEMETRY.SPAN_SEND_MESSAGE); }, [text]); + useEffect(() => { + if (!reportActionID) { + return; + } + endSpan(`${CONST.TELEMETRY.SPAN_SEND_MESSAGE}_${reportActionID}`); + }, [reportActionID]); // If the only difference between fragment.text and fragment.html is
tags and emoji tag // on native, we render it as text, not as html diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx index 9886df0076876..72122f8bf02a5 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, scrollPositionRef: {current: {}}, scrollOffsetRef: {current: 0}}; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com';