diff --git a/src/languages/en.ts b/src/languages/en.ts
index b80428166fff..6ef7e74adb1a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -3003,6 +3003,10 @@ export default {
offline:
"You appear to be offline. Unfortunately, Expensify Classic doesn't work offline, but New Expensify does. If you prefer to use Expensify Classic, try again when you have an internet connection.",
},
+ listBoundary: {
+ errorMessage: 'There was an error loading more messages.',
+ tryAgain: 'Try again',
+ },
systemMessage: {
mergedWithCashTransaction: 'matched a receipt to this transaction.',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 8ddba987d6f4..351989f5de30 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3507,6 +3507,10 @@ export default {
offline:
'Parece que estás desconectado. Desafortunadamente, Expensify Classic no funciona sin conexión, pero New Expensify sí. Si prefieres utilizar Expensify Classic, inténtalo de nuevo cuando tengas conexión a internet.',
},
+ listBoundary: {
+ errorMessage: 'Se produjo un error al cargar más mensajes.',
+ tryAgain: 'Inténtalo de nuevo',
+ },
systemMessage: {
mergedWithCashTransaction: 'encontró un recibo para esta transacción.',
},
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index d71664a959ed..feefd51698f6 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -746,7 +746,9 @@ function openReport(
value: {
isLoadingInitialReportActions: true,
isLoadingOlderReportActions: false,
+ hasLoadingOlderReportActionsError: false,
isLoadingNewerReportActions: false,
+ hasLoadingNewerReportActionsError: false,
lastVisitTime: DateUtils.getDBTime(),
},
},
@@ -1038,6 +1040,7 @@ function getOlderActions(reportID: string, reportActionID: string) {
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {
isLoadingOlderReportActions: true,
+ hasLoadingOlderReportActionsError: false,
},
},
];
@@ -1058,6 +1061,7 @@ function getOlderActions(reportID: string, reportActionID: string) {
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {
isLoadingOlderReportActions: false,
+ hasLoadingOlderReportActionsError: true,
},
},
];
@@ -1081,6 +1085,7 @@ function getNewerActions(reportID: string, reportActionID: string) {
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {
isLoadingNewerReportActions: true,
+ hasLoadingNewerReportActionsError: false,
},
},
];
@@ -1101,6 +1106,7 @@ function getNewerActions(reportID: string, reportActionID: string) {
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`,
value: {
isLoadingNewerReportActions: false,
+ hasLoadingNewerReportActionsError: true,
},
},
];
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 54e7ab923aa9..645f508a322d 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -137,7 +137,9 @@ function ReportScreen({
reportMetadata = {
isLoadingInitialReportActions: true,
isLoadingOlderReportActions: false,
+ hasLoadingOlderReportActionsError: false,
isLoadingNewerReportActions: false,
+ hasLoadingNewerReportActionsError: false,
},
parentReportActions,
accountManagerReportID,
@@ -709,7 +711,9 @@ function ReportScreen({
parentReportAction={parentReportAction}
isLoadingInitialReportActions={reportMetadata?.isLoadingInitialReportActions}
isLoadingNewerReportActions={reportMetadata?.isLoadingNewerReportActions}
+ hasLoadingNewerReportActionsError={reportMetadata?.hasLoadingNewerReportActionsError}
isLoadingOlderReportActions={reportMetadata?.isLoadingOlderReportActions}
+ hasLoadingOlderReportActionsError={reportMetadata?.hasLoadingOlderReportActionsError}
isReadyForCommentLinking={!shouldShowSkeleton}
transactionThreadReportID={transactionThreadReportID}
/>
@@ -766,7 +770,9 @@ export default withCurrentReportID(
initialValue: {
isLoadingInitialReportActions: true,
isLoadingOlderReportActions: false,
+ hasLoadingOlderReportActionsError: false,
isLoadingNewerReportActions: false,
+ hasLoadingNewerReportActionsError: false,
},
},
isComposerFullSize: {
diff --git a/src/pages/home/report/ListBoundaryLoader.tsx b/src/pages/home/report/ListBoundaryLoader.tsx
index a359606b9ed5..8081e18bb668 100644
--- a/src/pages/home/report/ListBoundaryLoader.tsx
+++ b/src/pages/home/report/ListBoundaryLoader.tsx
@@ -1,7 +1,10 @@
-import React from 'react';
+import React, {useEffect} from 'react';
import {ActivityIndicator, View} from 'react-native';
import type {ValueOf} from 'type-fest';
+import Button from '@components/Button';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -22,6 +25,12 @@ type ListBoundaryLoaderProps = {
/** Name of the last report action */
lastReportActionName?: string;
+
+ /** Shows if there was an error when loading report actions */
+ hasError?: boolean;
+
+ /** Function to retry if there was an error */
+ onRetry?: () => void;
};
function ListBoundaryLoader({
@@ -30,11 +39,47 @@ function ListBoundaryLoader({
isLoadingInitialReportActions = false,
lastReportActionName = '',
isLoadingNewerReportActions = false,
+ hasError = false,
+ onRetry,
}: ListBoundaryLoaderProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
+ const {translate} = useLocalize();
+
+ // When retrying we want to show the loading state in the retry button so we
+ // have this separate state to handle that.
+ const [isRetrying, setIsRetrying] = React.useState(false);
+
+ const retry = () => {
+ setIsRetrying(true);
+ onRetry?.();
+ };
+
+ // Reset the retrying state once loading is done.
+ useEffect(() => {
+ if (isLoadingNewerReportActions || isLoadingOlderReportActions) {
+ return;
+ }
+ setIsRetrying(false);
+ }, [isLoadingNewerReportActions, isLoadingOlderReportActions]);
+
+ if (hasError || isRetrying) {
+ return (
+
+ {translate('listBoundary.errorMessage')}
+ {!isOffline && (
+
+ )}
+
+ );
+ }
// We use two different loading components for the header and footer
// to reduce the jumping effect when the user is scrolling to the newer report actions
if (type === CONST.LIST_COMPONENTS.FOOTER) {
@@ -55,7 +100,7 @@ function ListBoundaryLoader({
// applied for a header of the list, i.e. when you scroll to the bottom of the list
// the styles for android and the rest components are different that's why we use two different components
return (
-
+
void;
@@ -75,10 +81,10 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & {
onScroll?: (event: NativeSyntheticEvent) => void;
/** Function to load more chats */
- loadOlderChats: () => void;
+ loadOlderChats: (force?: boolean) => void;
/** Function to load newer chats */
- loadNewerChats: () => void;
+ loadNewerChats: (force?: boolean) => void;
/** Whether the composer is in full size */
isComposerFullSize?: boolean;
@@ -139,7 +145,9 @@ function ReportActionsList({
parentReportAction,
isLoadingInitialReportActions = false,
isLoadingOlderReportActions = false,
+ hasLoadingOlderReportActionsError = false,
isLoadingNewerReportActions = false,
+ hasLoadingNewerReportActionsError = false,
sortedReportActions,
onScroll,
mostRecentIOUReportActionID = '',
@@ -590,11 +598,16 @@ function ReportActionsList({
const lastReportAction: OnyxTypes.ReportAction | EmptyObject = useMemo(() => sortedReportActions.at(-1) ?? {}, [sortedReportActions]);
- const listFooterComponent = useCallback(() => {
+ const retryLoadOlderChatsError = useCallback(() => {
+ loadOlderChats(true);
+ }, [loadOlderChats]);
+
+ const listFooterComponent = useMemo(() => {
// Skip this hook on the first render (when online), as we are not sure if more actions are going to be loaded,
// Therefore showing the skeleton on footer might be misleading.
- // When offline, there should be no second render, so we should show the skeleton if the corresponding loading prop is present
- if (!isOffline && !hasFooterRendered.current) {
+ // When offline, there should be no second render, so we should show the skeleton if the corresponding loading prop is present.
+ // In case of an error we want to display the footer no matter what.
+ if (!isOffline && !hasFooterRendered.current && !hasLoadingOlderReportActionsError) {
hasFooterRendered.current = true;
return null;
}
@@ -605,9 +618,11 @@ function ReportActionsList({
isLoadingOlderReportActions={isLoadingOlderReportActions}
isLoadingInitialReportActions={isLoadingInitialReportActions}
lastReportActionName={lastReportAction.actionName}
+ hasError={hasLoadingOlderReportActionsError}
+ onRetry={retryLoadOlderChatsError}
/>
);
- }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction.actionName, isOffline]);
+ }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction.actionName, isOffline, hasLoadingOlderReportActionsError, retryLoadOlderChatsError]);
const onLayoutInner = useCallback(
(event: LayoutChangeEvent) => {
@@ -622,8 +637,13 @@ function ReportActionsList({
[onContentSizeChange],
);
- const listHeaderComponent = useCallback(() => {
- if (!canShowHeader) {
+ const retryLoadNewerChatsError = useCallback(() => {
+ loadNewerChats(true);
+ }, [loadNewerChats]);
+
+ const listHeaderComponent = useMemo(() => {
+ // In case of an error we want to display the header no matter what.
+ if (!canShowHeader && !hasLoadingNewerReportActionsError) {
hasHeaderRendered.current = true;
return null;
}
@@ -632,9 +652,19 @@ function ReportActionsList({
);
- }, [isLoadingNewerReportActions, canShowHeader]);
+ }, [isLoadingNewerReportActions, canShowHeader, hasLoadingNewerReportActionsError, retryLoadNewerChatsError]);
+
+ const onStartReached = useCallback(() => {
+ loadNewerChats(false);
+ }, [loadNewerChats]);
+
+ const onEndReached = useCallback(() => {
+ loadOlderChats(false);
+ }, [loadOlderChats]);
// When performing comment linking, initially 25 items are added to the list. Subsequent fetches add 15 items from the cache or 50 items from the server.
// This is to ensure that the user is able to see the 'scroll to newer comments' button when they do comment linking and have not reached the end of the list yet.
@@ -656,9 +686,9 @@ function ReportActionsList({
contentContainerStyle={contentContainerStyle}
keyExtractor={keyExtractor}
initialNumToRender={initialNumToRender}
- onEndReached={loadOlderChats}
+ onEndReached={onEndReached}
onEndReachedThreshold={0.75}
- onStartReached={loadNewerChats}
+ onStartReached={onStartReached}
onStartReachedThreshold={0.75}
ListFooterComponent={listFooterComponent}
ListHeaderComponent={listHeaderComponent}
diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index 02008a464859..ba4230551e13 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -62,9 +62,15 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & {
/** The report actions are loading more data */
isLoadingOlderReportActions?: boolean;
+ /** There an error when loading older report actions */
+ hasLoadingOlderReportActionsError?: boolean;
+
/** The report actions are loading newer data */
isLoadingNewerReportActions?: boolean;
+ /** There an error when loading newer report actions */
+ hasLoadingNewerReportActionsError?: boolean;
+
/** Whether the report is ready for comment linking */
isReadyForCommentLinking?: boolean;
@@ -87,7 +93,9 @@ function ReportActionsView({
transactionThreadReportActions = [],
isLoadingInitialReportActions = false,
isLoadingOlderReportActions = false,
+ hasLoadingOlderReportActionsError = false,
isLoadingNewerReportActions = false,
+ hasLoadingNewerReportActionsError = false,
isReadyForCommentLinking = false,
}: ReportActionsViewProps) {
useCopySelectionHelper();
@@ -95,6 +103,8 @@ function ReportActionsView({
const route = useRoute>();
const reportActionID = route?.params?.reportActionID;
const didLayout = useRef(false);
+ const didLoadOlderChats = useRef(false);
+ const didLoadNewerChats = useRef(false);
// triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest,we don't maintain their position and instead trigger a recalculation of their positioning in the list.
// we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned
@@ -295,85 +305,118 @@ function ReportActionsView({
* Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently
* displaying.
*/
- const loadOlderChats = useCallback(() => {
- Log.info(
- `[ReportActionsView] loadOlderChats ${JSON.stringify({
- isOffline: network.isOffline,
- isLoadingOlderReportActions,
- isLoadingInitialReportActions,
- oldestReportActionID: oldestReportAction?.reportActionID,
- hasCreatedAction,
- isTransactionThread: !isEmptyObject(transactionThreadReport),
- })}`,
- );
+ const loadOlderChats = useCallback(
+ (force = false) => {
+ Log.info(
+ `[ReportActionsView] loadOlderChats ${JSON.stringify({
+ isOffline: network.isOffline,
+ isLoadingOlderReportActions,
+ isLoadingInitialReportActions,
+ oldestReportActionID: oldestReportAction?.reportActionID,
+ hasCreatedAction,
+ isTransactionThread: !isEmptyObject(transactionThreadReport),
+ })}`,
+ );
+
+ // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline.
+ if (
+ !force &&
+ (!!network.isOffline ||
+ isLoadingOlderReportActions ||
+ // If there was an error only try again once on initial mount.
+ (didLoadOlderChats.current && hasLoadingOlderReportActionsError) ||
+ isLoadingInitialReportActions)
+ ) {
+ return;
+ }
- // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline.
- if (!!network.isOffline || isLoadingOlderReportActions || isLoadingInitialReportActions) {
- return;
- }
+ // Don't load more chats if we're already at the beginning of the chat history
+ if (!oldestReportAction || hasCreatedAction) {
+ return;
+ }
- // Don't load more chats if we're already at the beginning of the chat history
- if (!oldestReportAction || hasCreatedAction) {
- return;
- }
+ didLoadOlderChats.current = true;
- if (!isEmptyObject(transactionThreadReport)) {
- // Get older actions based on the oldest reportAction for the current report
- const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID);
- Report.getOlderActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0');
+ if (!isEmptyObject(transactionThreadReport)) {
+ // Get older actions based on the oldest reportAction for the current report
+ const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID);
+ Report.getOlderActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0');
- // Get older actions based on the oldest reportAction for the transaction thread report
- const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID);
- Report.getOlderActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0');
- } else {
- // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments
- Report.getOlderActions(reportID, oldestReportAction.reportActionID);
- }
- }, [network.isOffline, isLoadingOlderReportActions, isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID, reportActionIDMap, transactionThreadReport]);
-
- const loadNewerChats = useCallback(() => {
- // Determines if loading older reports is necessary when the content is smaller than the list
- // and there are fewer than 23 items, indicating we've reached the oldest message.
- const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23;
-
- Log.info(
- `[ReportActionsView] loadNewerChats ${JSON.stringify({
- isOffline: network.isOffline,
- isLoadingOlderReportActions,
- isLoadingInitialReportActions,
- newestReportAction: newestReportAction.pendingAction,
- firstReportActionID: newestReportAction?.reportActionID,
- isLoadingOlderReportsFirstNeeded,
- reportActionID,
- })}`,
- );
+ // Get older actions based on the oldest reportAction for the transaction thread report
+ const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID);
+ Report.getOlderActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0');
+ } else {
+ // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments
+ Report.getOlderActions(reportID, oldestReportAction.reportActionID);
+ }
+ },
+ [
+ network.isOffline,
+ isLoadingOlderReportActions,
+ isLoadingInitialReportActions,
+ oldestReportAction,
+ hasCreatedAction,
+ reportID,
+ reportActionIDMap,
+ transactionThreadReport,
+ hasLoadingOlderReportActionsError,
+ ],
+ );
- if (
- !reportActionID ||
- !isFocused ||
- isLoadingInitialReportActions ||
- isLoadingOlderReportActions ||
- network.isOffline ||
- newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE
- ) {
- return;
- }
+ const loadNewerChats = useCallback(
+ (force = false) => {
+ // Determines if loading older reports is necessary when the content is smaller than the list
+ // and there are fewer than 23 items, indicating we've reached the oldest message.
+ const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23;
+
+ Log.info(
+ `[ReportActionsView] loadNewerChats ${JSON.stringify({
+ isOffline: network.isOffline,
+ isLoadingNewerReportActions,
+ isLoadingInitialReportActions,
+ newestReportAction: newestReportAction.pendingAction,
+ firstReportActionID: newestReportAction?.reportActionID,
+ isLoadingOlderReportsFirstNeeded,
+ reportActionID,
+ })}`,
+ );
+
+ if (
+ !force &&
+ (!reportActionID ||
+ !isFocused ||
+ isLoadingInitialReportActions ||
+ isLoadingNewerReportActions ||
+ // If there was an error only try again once on initial mount. We should also still load
+ // more in case we have cached messages.
+ (!hasMoreCached && didLoadNewerChats.current && hasLoadingNewerReportActionsError) ||
+ network.isOffline ||
+ newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
+ ) {
+ return;
+ }
- if ((reportActionID && indexOfLinkedAction > -1 && !isLoadingOlderReportsFirstNeeded) || (!reportActionID && !isLoadingOlderReportsFirstNeeded)) {
- handleReportActionPagination({firstReportActionID: newestReportAction?.reportActionID});
- }
- }, [
- isLoadingInitialReportActions,
- isLoadingOlderReportActions,
- checkIfContentSmallerThanList,
- reportActionID,
- indexOfLinkedAction,
- handleReportActionPagination,
- network.isOffline,
- reportActions.length,
- newestReportAction,
- isFocused,
- ]);
+ didLoadNewerChats.current = true;
+
+ if ((reportActionID && indexOfLinkedAction > -1 && !isLoadingOlderReportsFirstNeeded) || (!reportActionID && !isLoadingOlderReportsFirstNeeded)) {
+ handleReportActionPagination({firstReportActionID: newestReportAction?.reportActionID});
+ }
+ },
+ [
+ isLoadingInitialReportActions,
+ isLoadingNewerReportActions,
+ checkIfContentSmallerThanList,
+ reportActionID,
+ indexOfLinkedAction,
+ handleReportActionPagination,
+ network.isOffline,
+ reportActions.length,
+ newestReportAction,
+ isFocused,
+ hasLoadingNewerReportActionsError,
+ hasMoreCached,
+ ],
+ );
/**
* Runs when the FlatList finishes laying out
@@ -530,7 +573,9 @@ function ReportActionsView({
loadNewerChats={loadNewerChats}
isLoadingInitialReportActions={isLoadingInitialReportActions}
isLoadingOlderReportActions={isLoadingOlderReportActions}
+ hasLoadingOlderReportActionsError={hasLoadingOlderReportActionsError}
isLoadingNewerReportActions={isLoadingNewerReportActions}
+ hasLoadingNewerReportActionsError={hasLoadingNewerReportActionsError}
listID={listID}
onContentSizeChange={onContentSizeChange}
shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll}
@@ -576,6 +621,14 @@ function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActions
return false;
}
+ if (oldProps.hasLoadingOlderReportActionsError !== newProps.hasLoadingOlderReportActionsError) {
+ return false;
+ }
+
+ if (oldProps.hasLoadingNewerReportActionsError !== newProps.hasLoadingNewerReportActionsError) {
+ return false;
+ }
+
return lodashIsEqual(oldProps.report, newProps.report);
}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 884f241c555f..0d93a2d0dc4d 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -4494,7 +4494,7 @@ const styles = (theme: ThemeColors) =>
top: -36,
},
- chatBottomLoader: {
+ listBoundaryLoader: {
position: 'absolute',
top: 0,
bottom: 0,
@@ -4502,6 +4502,15 @@ const styles = (theme: ThemeColors) =>
right: 0,
height: CONST.CHAT_HEADER_LOADER_HEIGHT,
},
+ listBoundaryError: {
+ paddingVertical: 15,
+ paddingHorizontal: 20,
+ },
+ listBoundaryErrorText: {
+ color: theme.textSupporting,
+ fontSize: variables.fontSizeLabel,
+ marginBottom: 10,
+ },
videoContainer: {
...flex.flex1,
diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts
index c6484705553c..1c6035853564 100644
--- a/src/types/onyx/ReportMetadata.ts
+++ b/src/types/onyx/ReportMetadata.ts
@@ -2,9 +2,15 @@ type ReportMetadata = {
/** Are we loading newer report actions? */
isLoadingNewerReportActions?: boolean;
+ /** Was there an error when loading newer report actions? */
+ hasLoadingNewerReportActionsError?: boolean;
+
/** Are we loading older report actions? */
isLoadingOlderReportActions?: boolean;
+ /** Was there an error when loading older report actions? */
+ hasLoadingOlderReportActionsError?: boolean;
+
/** Flag to check if the report actions data are loading */
isLoadingInitialReportActions?: boolean;