Skip to content

Fix scrolling to bottom list on MoneyRequestReportView #59664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@ const CONST = {
},
},
THREAD_DISABLED: ['CREATED'],
// Used when displaying reportActions list to handle of unread messages icon/button
// Used when displaying reportActions list to handle unread messages icon/button
SCROLL_VERTICAL_OFFSET_THRESHOLD: 200,
ACTION_VISIBLE_THRESHOLD: 250,
},
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions,
id={transaction.transactionID}
style={[pressableStyle]}
onMouseLeave={handleMouseLeave}
key={transaction.transactionID}
>
<TransactionItemRow
transactionItem={transaction}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Navigation/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@
activeWorkspaceID?: string;
shouldDisplaySearch?: boolean;
shouldDisplayHelpButton?: boolean;
cancelSearch?: () => void;
shouldShowLoadingBar?: boolean;
cancelSearch?: () => void;
};

function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayHelpButton = true, cancelSearch, shouldShowLoadingBar = false}: TopBarProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const policy = usePolicy(activeWorkspaceID);
const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}});

Check failure on line 33 in src/components/Navigation/TopBar.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

useOnyx() calls require you to pass the "canBeMissing" param
const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);

Check failure on line 34 in src/components/Navigation/TopBar.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

useOnyx() calls require you to pass the "canBeMissing" param
const isAnonymousUser = isAnonymousUserUtil(session);
const {canUseLeftHandBar} = usePermissions();

Expand Down
9 changes: 9 additions & 0 deletions src/hooks/useReportScrollManager/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {useCallback, useContext} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView} from 'react-native';
import {ActionListContext} from '@pages/home/ReportScreenContext';
import type ReportScrollManagerData from './types';

Expand Down Expand Up @@ -41,6 +43,13 @@ function useReportScrollManager(): ReportScrollManagerData {
return;
}

const scrollViewRef = flatListRef.current.getNativeScrollRef();
// Try to scroll on underlying scrollView if available, fallback to usual listRef
if (scrollViewRef && 'scrollToEnd' in scrollViewRef) {
(scrollViewRef as ScrollView).scrollToEnd({animated: false});
return;
}

flatListRef.current.scrollToEnd({animated: false});
}, [flatListRef]);

Expand Down
53 changes: 29 additions & 24 deletions src/pages/Search/SearchMoneyRequestReportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true});
const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, initialValue: {}});
const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];

Check failure on line 50 in src/pages/Search/SearchMoneyRequestReportPage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

useOnyx() calls require you to pass the "canBeMissing" param
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);

Check failure on line 51 in src/pages/Search/SearchMoneyRequestReportPage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

useOnyx() calls require you to pass the "canBeMissing" param

Check failure on line 52 in src/pages/Search/SearchMoneyRequestReportPage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

useOnyx() calls require you to pass the "canBeMissing" param
const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute);

Check failure on line 54 in src/pages/Search/SearchMoneyRequestReportPage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

useOnyx() calls require you to pass the "canBeMissing" param
const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
const flatListRef = useRef<FlatList>(null);
const reactionListRef = useRef<ReactionListRef>(null);
Expand Down Expand Up @@ -84,30 +84,34 @@

if (shouldUseNarrowLayout) {
return (
<ScreenWrapper
testID={SearchMoneyRequestReportPage.displayName}
shouldEnableMaxHeight
offlineIndicatorStyle={styles.mtAuto}
headerGapStyles={styles.searchHeaderGap}
>
<FullPageNotFoundView
shouldShow={shouldShowNotFoundPage}
subtitleKey="notFound.noAccess"
subtitleStyle={[styles.textSupporting]}
shouldDisplaySearchRouter
shouldShowBackButton={shouldUseNarrowLayout}
onBackButtonPress={Navigation.goBack}
linkKey="notFound.noAccess"
>
<MoneyRequestReportView
report={report}
reportMetadata={reportMetadata}
policy={policy}
shouldDisplayReportFooter={isCurrentReportLoadedFromOnyx}
backToRoute={route.params.backTo}
/>
</FullPageNotFoundView>
</ScreenWrapper>
<ActionListContext.Provider value={actionListValue}>
<ReactionListContext.Provider value={reactionListRef}>
<ScreenWrapper
testID={SearchMoneyRequestReportPage.displayName}
shouldEnableMaxHeight
offlineIndicatorStyle={styles.mtAuto}
headerGapStyles={styles.searchHeaderGap}
>
<FullPageNotFoundView
shouldShow={shouldShowNotFoundPage}
subtitleKey="notFound.noAccess"
subtitleStyle={[styles.textSupporting]}
shouldDisplaySearchRouter
shouldShowBackButton={shouldUseNarrowLayout}
onBackButtonPress={Navigation.goBack}
linkKey="notFound.noAccess"
>
<MoneyRequestReportView
report={report}
reportMetadata={reportMetadata}
policy={policy}
shouldDisplayReportFooter={isCurrentReportLoadedFromOnyx}
backToRoute={route.params.backTo}
/>
</FullPageNotFoundView>
</ScreenWrapper>
</ReactionListContext.Provider>
</ActionListContext.Provider>
);
}

Expand All @@ -127,6 +131,7 @@
<TopBar
breadcrumbLabel={translate('common.reports')}
shouldDisplaySearch={false}
shouldShowLoadingBar={false}
/>
<SearchTypeMenu queryJSON={undefined} />
</View>
Expand Down
75 changes: 19 additions & 56 deletions src/pages/home/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList';
import {useIsFocused, useRoute} from '@react-navigation/native';
// eslint-disable-next-line lodash/import-scope
import type {DebouncedFunc} from 'lodash';
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';
Expand Down Expand Up @@ -31,11 +29,9 @@ import {
isConsecutiveActionMadeByPreviousActor,
isConsecutiveChronosAutomaticTimerAction,
isDeletedParentAction,
isReportActionUnread,
isReportPreviewAction,
isReversedTransaction,
isTransactionThread,
shouldHideNewMarker,
wasMessageReceivedWhileOffline,
} from '@libs/ReportActionsUtils';
import {
Expand Down Expand Up @@ -65,8 +61,7 @@ import FloatingMessageCounter from './FloatingMessageCounter';
import getInitialNumToRender from './getInitialNumReportActionsToRender';
import ListBoundaryLoader from './ListBoundaryLoader';
import ReportActionsListItemRenderer from './ReportActionsListItemRenderer';

type LoadNewerChats = DebouncedFunc<(params: {distanceFromStart: number}) => void>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed this was unused

import shouldDisplayNewMarkerOnReportAction from './shouldDisplayNewMarkerOnReportAction';

type ReportActionsListProps = {
/** The report currently being looked at */
Expand Down Expand Up @@ -237,62 +232,30 @@ function ReportActionsList({
* The reportActionID the unread marker should display above
*/
const unreadMarkerReportActionID = useMemo(() => {
const shouldDisplayNewMarker = (message: OnyxTypes.ReportAction, index: number): boolean => {
const nextMessage = sortedVisibleReportActions.at(index + 1);
const isNextMessageUnread = !!nextMessage && isReportActionUnread(nextMessage, unreadMarkerTime);

// If the current message is the earliest message received while offline, we want to display the unread marker above this message.
const isEarliestReceivedOfflineMessage = index === earliestReceivedOfflineMessageIndex;
if (isEarliestReceivedOfflineMessage && !isNextMessageUnread) {
return true;
}

// If the unread marker should be hidden or is not within the visible area, don't show the unread marker.
if (shouldHideNewMarker(message)) {
return false;
}

const isCurrentMessageUnread = isReportActionUnread(message, unreadMarkerTime);

// If the current message is read or the next message is unread, don't show the unread marker.
if (!isCurrentMessageUnread || isNextMessageUnread) {
return false;
}

const isPendingAdd = (action: OnyxTypes.ReportAction) => {
return action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
};

// If no unread marker exists, don't set an unread marker for newly added messages from the current user.
const isFromCurrentUser = accountID === (isReportPreviewAction(message) ? message.childLastActorAccountID : message.actorAccountID);
const isNewMessage = !prevSortedVisibleReportActionsObjects[message.reportActionID];

// The unread marker will show if the action's `created` time is later than `unreadMarkerTime`.
// The `unreadMarkerTime` has already been updated to match the optimistic action created time,
// but once the new action is saved on the backend, the actual created time will be later than the optimistic one.
// Therefore, we also need to prevent the unread marker from appearing for previously optimistic actions.
const isPreviouslyOptimistic =
(isPendingAdd(prevSortedVisibleReportActionsObjects[message.reportActionID]) && !isPendingAdd(message)) ||
(!!prevSortedVisibleReportActionsObjects[message.reportActionID]?.isOptimisticAction && !message.isOptimisticAction);
const shouldIgnoreUnreadForCurrentUserMessage = !prevUnreadMarkerReportActionID.current && isFromCurrentUser && (isNewMessage || isPreviouslyOptimistic);

if (isFromCurrentUser) {
return !shouldIgnoreUnreadForCurrentUserMessage;
}

return !isNewMessage || scrollingVerticalOffset.current >= CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
};

// If there are message that were recevied while offline,
// we can skip checking all messages later than the earliest recevied offline message.
// If there are message that were received while offline,
// we can skip checking all messages later than the earliest received offline message.
const startIndex = earliestReceivedOfflineMessageIndex ?? 0;

// Scan through each visible report action until we find the appropriate action to show the unread marker
for (let index = startIndex; index < sortedVisibleReportActions.length; index++) {
const reportAction = sortedVisibleReportActions.at(index);
const nextAction = sortedVisibleReportActions.at(index + 1);
const isEarliestReceivedOfflineMessage = index === earliestReceivedOfflineMessageIndex;

// eslint-disable-next-line react-compiler/react-compiler
if (reportAction && shouldDisplayNewMarker(reportAction, index)) {
const shouldDisplayNewMarker =
reportAction &&
shouldDisplayNewMarkerOnReportAction({
message: reportAction,
nextMessage: nextAction,
isEarliestReceivedOfflineMessage,
accountID,
prevSortedVisibleReportActionsObjects,
unreadMarkerTime,
scrollingVerticalOffset: scrollingVerticalOffset.current,
prevUnreadMarkerReportActionID: prevUnreadMarkerReportActionID.current,
});
if (shouldDisplayNewMarker) {
return reportAction.reportActionID;
}
}
Expand Down Expand Up @@ -774,4 +737,4 @@ ReportActionsList.displayName = 'ReportActionsList';

export default memo(ReportActionsList);

export type {LoadNewerChats, ReportActionsListProps};
export type {ReportActionsListProps};
88 changes: 88 additions & 0 deletions src/pages/home/report/shouldDisplayNewMarkerOnReportAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {isReportActionUnread, isReportPreviewAction, shouldHideNewMarker} from '@libs/ReportActionsUtils';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';

type ShouldDisplayNewMarkerOnReportActionParams = {
/** The reportAction for which the check is done */
message: OnyxTypes.ReportAction;

/** The reportAction adjacent to `message` (either previous or next one) */
nextMessage: OnyxTypes.ReportAction | undefined;

/** Is it the earliestReceivedOfflineMessage */
isEarliestReceivedOfflineMessage: boolean;

/** Time for unreadMarker */
unreadMarkerTime: string | undefined;

/** User accountID */
accountID: number | undefined;

/** Map of reportActions saved via usePrev */
prevSortedVisibleReportActionsObjects: Record<string, OnyxTypes.ReportAction>;

/** Current value for vertical offset */
scrollingVerticalOffset: number;

/** The id of reportAction that was last marked as read */
prevUnreadMarkerReportActionID: string | null;
};

/**
* This function decides whether the given report action (message) should have the new marker indicator displayed
* It's used for the standard "chat" Report and for `MoneyRequestReport` actions lists.
*/
const shouldDisplayNewMarkerOnReportAction = ({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function was extracted from ReportActionsList almost without change - I'm just passing down arguments so that it can be reused in 2 places.
No logic changes are intended for ReportActionsList

message,
nextMessage,
isEarliestReceivedOfflineMessage,
unreadMarkerTime,
accountID,
prevSortedVisibleReportActionsObjects,
prevUnreadMarkerReportActionID,
scrollingVerticalOffset,
}: ShouldDisplayNewMarkerOnReportActionParams): boolean => {
const isNextMessageUnread = !!nextMessage && isReportActionUnread(nextMessage, unreadMarkerTime);

// If the current message is the earliest message received while offline, we want to display the unread marker above this message.
if (isEarliestReceivedOfflineMessage && !isNextMessageUnread) {
return true;
}

// If the unread marker should be hidden or is not within the visible area, don't show the unread marker.
if (shouldHideNewMarker(message)) {
return false;
}

const isCurrentMessageUnread = isReportActionUnread(message, unreadMarkerTime);

// If the current message is read or the next message is unread, don't show the unread marker.
if (!isCurrentMessageUnread || isNextMessageUnread) {
return false;
}

const isPendingAdd = (action: OnyxTypes.ReportAction) => {
return action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
};

// If no unread marker exists, don't set an unread marker for newly added messages from the current user.
const isFromCurrentUser = accountID === (isReportPreviewAction(message) ? message.childLastActorAccountID : message.actorAccountID);
const isNewMessage = !prevSortedVisibleReportActionsObjects[message.reportActionID];

// The unread marker will show if the action's `created` time is later than `unreadMarkerTime`.
// The `unreadMarkerTime` has already been updated to match the optimistic action created time,
// but once the new action is saved on the backend, the actual created time will be later than the optimistic one.
// Therefore, we also need to prevent the unread marker from appearing for previously optimistic actions.
const isPreviouslyOptimistic =
(isPendingAdd(prevSortedVisibleReportActionsObjects[message.reportActionID]) && !isPendingAdd(message)) ||
(!!prevSortedVisibleReportActionsObjects[message.reportActionID]?.isOptimisticAction && !message.isOptimisticAction);
const shouldIgnoreUnreadForCurrentUserMessage = !prevUnreadMarkerReportActionID && isFromCurrentUser && (isNewMessage || isPreviouslyOptimistic);

if (isFromCurrentUser) {
return !shouldIgnoreUnreadForCurrentUserMessage;
}

return !isNewMessage || scrollingVerticalOffset >= CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
};

export default shouldDisplayNewMarkerOnReportAction;