diff --git a/src/CONST.ts b/src/CONST.ts index 3dfa6b4b078d..840743a27f65 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1449,6 +1449,8 @@ const CONST = { WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, SHOW_LOADING_SPINNER_DEBOUNCE_TIME: 250, + SKELETON_FADE_DURATION: 300, + SKELETON_SLIDE_DURATION: 1200, TEST_TOOLS_MODAL_THROTTLE_TIME: 800, TOOLTIP_SENSE: 1000, TRIE_INITIALIZATION: 'trie_initialization', diff --git a/src/components/LoadingBar.tsx b/src/components/LoadingBar.tsx index e6d1ec0cd66d..e13702517e10 100644 --- a/src/components/LoadingBar.tsx +++ b/src/components/LoadingBar.tsx @@ -1,83 +1,68 @@ import React, {useEffect} from 'react'; -import Animated, {cancelAnimation, Easing, useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming} from 'react-native-reanimated'; +import {View} from 'react-native'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; +import colors from '@styles/theme/colors'; import CONST from '@src/CONST'; type LoadingBarProps = { - // Whether or not to show the loading bar + // Whether to show the loading bar shouldShow: boolean; }; function LoadingBar({shouldShow}: LoadingBarProps) { - const left = useSharedValue(0); - const width = useSharedValue(0); + const left = useSharedValue(-30); const opacity = useSharedValue(0); + const isAnimating = useSharedValue(false); const styles = useThemeStyles(); useEffect(() => { - if (shouldShow) { - // eslint-disable-next-line react-compiler/react-compiler - left.set(0); - width.set(0); - opacity.set(withTiming(1, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION})); + if (shouldShow && !isAnimating.get()) { + isAnimating.set(true); + opacity.set(withTiming(1, {duration: CONST.TIMING.SKELETON_FADE_DURATION})); left.set( - withDelay( - CONST.ANIMATED_PROGRESS_BAR_DELAY, - withRepeat( - withSequence( - withTiming(0, {duration: 0}), - withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), - withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), - ), - -1, - false, - ), - ), - ); - - width.set( - withDelay( - CONST.ANIMATED_PROGRESS_BAR_DELAY, - withRepeat( - withSequence( - withTiming(0, {duration: 0}), - withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), - withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), - ), - -1, - false, - ), - ), + withTiming(100, {duration: CONST.TIMING.SKELETON_SLIDE_DURATION}, () => { + requestAnimationFrame(() => { + if (shouldShow) { + left.set(-30); + left.set(withTiming(100, {duration: CONST.TIMING.SKELETON_SLIDE_DURATION})); + } else { + opacity.set( + withTiming(0, {duration: CONST.TIMING.SKELETON_FADE_DURATION}, () => { + isAnimating.set(false); + }), + ); + } + }); + }), ); - } else { + } else if (!shouldShow) { opacity.set( - withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}, () => { - cancelAnimation(left); - cancelAnimation(width); + withTiming(0, {duration: CONST.TIMING.SKELETON_FADE_DURATION}, () => { + isAnimating.set(false); }), ); } - // we want to update only when shouldShow changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [shouldShow]); - const animatedIndicatorStyle = useAnimatedStyle(() => ({ + const barStyle = useAnimatedStyle(() => ({ left: `${left.get()}%`, - width: `${width.get()}%`, - })); - - const animatedContainerStyle = useAnimatedStyle(() => ({ + width: '30%', + height: '100%', + backgroundColor: colors.green, opacity: opacity.get(), + borderRadius: 2, })); return ( - - - + + + ); } -LoadingBar.displayName = 'ProgressBar'; +LoadingBar.displayName = 'LoadingBar'; export default LoadingBar; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 97ee16ff0015..5713e97aca82 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -75,7 +75,6 @@ import DelegateNoAccessModal from './DelegateNoAccessModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; -import LoadingBar from './LoadingBar'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; @@ -225,7 +224,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); const {isDelegateAccessRestricted} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA); const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; @@ -520,7 +518,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea )} )} - {isHoldMenuVisible && requestType !== undefined && ( )} - ); } diff --git a/src/components/ReportActionsSkeletonView/SkeletonViewLines.tsx b/src/components/ReportActionsSkeletonView/SkeletonViewLines.tsx index c4c2a3f43eb3..d3ea26ab5880 100644 --- a/src/components/ReportActionsSkeletonView/SkeletonViewLines.tsx +++ b/src/components/ReportActionsSkeletonView/SkeletonViewLines.tsx @@ -20,6 +20,7 @@ function SkeletonViewLines({numberOfRows, shouldAnimate = true}: SkeletonViewLin height={CONST.CHAT_SKELETON_VIEW.HEIGHT_FOR_ROW_COUNT[numberOfRows]} backgroundColor={theme.skeletonLHNIn} foregroundColor={theme.skeletonLHNOut} + speed={3} style={styles.mr5} > , p DebugUtils.stringifyJSON({ actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, reportID, - reportActionID: NumberUtils.rand64(), + reportActionID: rand64(), created: DateUtils.getDBTime(), actorAccountID: session?.accountID, avatar: (session?.accountID && personalDetailsList?.[session.accountID]?.avatar) ?? '', @@ -78,7 +78,7 @@ function DebugReportActionCreatePage({ {({safeAreaPaddingBottomStyle}) => ( @@ -107,7 +107,6 @@ function DebugReportActionCreatePage({ { - const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(reportAction) ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID : -1; + const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID; return IOUTransactionID === props.transaction?.transactionID; }); @@ -33,9 +34,8 @@ function DuplicateTransactionItem(props: DuplicateTransactionItemProps) { )} - {shouldShowEarlyDiscountBanner && ( ; - /** Array of report actions for the report for this action */ - // eslint-disable-next-line react/no-unused-prop-types - reportActions: OnyxTypes.ReportAction[]; - /** Report action belonging to the report's parent */ parentReportAction: OnyxEntry; @@ -1297,7 +1293,6 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { prevProps.linkedReportActionID === nextProps.linkedReportActionID && lodashIsEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) && lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && - lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && lodashIsEqual(prevParentReportAction, nextParentReportAction) && prevProps.draftMessage === nextProps.draftMessage && prevProps.iouReport?.reportID === nextProps.iouReport?.reportID && diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index b7134fd7059e..5d3284d7bee8 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -36,9 +36,6 @@ type ReportActionItemParentActionProps = { /** The transaction thread report associated with the current report, if any */ transactionThreadReport: OnyxEntry; - /** Array of report actions for this report */ - reportActions: OnyxTypes.ReportAction[]; - /** Report actions belonging to the report's parent */ parentReportAction: OnyxEntry; @@ -55,7 +52,6 @@ type ReportActionItemParentActionProps = { function ReportActionItemParentAction({ report, transactionThreadReport, - reportActions, parentReportAction, index = 0, shouldHideThreadDividerLine = false, @@ -144,7 +140,6 @@ function ReportActionItemParentAction({ } parentReportAction={parentReportAction} report={ancestor.report} - reportActions={reportActions} transactionThreadReport={transactionThreadReport} action={ancestor.reportAction} displayAsGroup={false} diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 6924725eb99b..3cdb7b59ae7a 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -176,6 +176,7 @@ function ReportActionsList({ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID}); const participantsContext = useContext(PersonalDetailsContext); + const [scrollOffset, setScrollOffset] = useState(0); useEffect(() => { const unsubscriber = Visibility.onVisibilityChange(() => { @@ -219,6 +220,7 @@ function ReportActionsList({ }, [report.reportID]); const prevUnreadMarkerReportActionID = useRef(null); + /** * Whether a message is NOT from the active user and it was received while the user was offline. */ @@ -512,6 +514,7 @@ function ReportActionsList({ }; const trackVerticalScrolling = (event: NativeSyntheticEvent) => { + setScrollOffset(event.nativeEvent.contentOffset.y); scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; handleUnreadFloatingButton(); onScroll?.(event); @@ -618,7 +621,6 @@ function ReportActionsList({ ({item: reportAction, index}: ListRenderItemInfo) => ( { loadOlderChats(false); + + requestAnimationFrame(() => { + reportScrollManager.ref?.current?.scrollToOffset({ + offset: scrollingVerticalOffset.current - scrollOffset, + animated: false, + }); + }); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [loadOlderChats]); // Parse Fullstory attributes on initial render diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index 1a0d8db13661..b9cc7406b890 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -11,9 +11,6 @@ type ReportActionsListItemRendererProps = { /** All the data of the action item */ reportAction: ReportAction; - /** Array of report actions for the report */ - reportActions: ReportAction[]; - /** The report's parentReportAction */ parentReportAction: OnyxEntry; @@ -56,7 +53,6 @@ type ReportActionsListItemRendererProps = { function ReportActionsListItemRenderer({ reportAction, - reportActions = [], parentReportAction, index, report, @@ -152,7 +148,6 @@ function ReportActionsListItemRenderer({ parentReportAction={parentReportAction} reportID={report.reportID} report={report} - reportActions={reportActions} transactionThreadReport={transactionThreadReport} index={index} isFirstVisibleReportAction={isFirstVisibleReportAction} @@ -169,7 +164,6 @@ function ReportActionsListItemRenderer({ transactionThreadReport={transactionThreadReport} parentReportActionForTransactionThread={parentReportActionForTransactionThread} action={action} - reportActions={reportActions} linkedReportActionID={linkedReportActionID} displayAsGroup={displayAsGroup} shouldDisplayNewMarker={shouldDisplayNewMarker} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 88b4cae332ed..6bd98e47f8aa 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import EmptyStateComponent from '@components/EmptyStateComponent'; import * as Illustrations from '@components/Icon/Illustrations'; +import LoadingBar from '@components/LoadingBar'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -53,8 +54,8 @@ type ReportActionsViewProps = { /** The report's parentReportAction */ parentReportAction: OnyxEntry; - /** The report metadata loading states */ - isLoadingInitialReportActions?: boolean; + /** The report's metadata */ + reportMetadata: OnyxTypes.ReportMetadata; /** The reportID of the transaction thread report associated with this current report, if any */ // eslint-disable-next-line react/no-unused-prop-types @@ -73,7 +74,7 @@ function ReportActionsView({ report, parentReportAction, reportActions: allReportActions, - isLoadingInitialReportActions, + reportMetadata, transactionThreadReportID, hasNewerActions, hasOlderActions, @@ -103,6 +104,8 @@ function ReportActionsView({ const reportID = report.reportID; const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); + const {isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions, hasLoadingNewerReportActionsError} = reportMetadata; + useEffect(() => { // When we linked to message - we do not need to wait for initial actions - they already exists if (!reportActionID || !isOffline) { @@ -211,6 +214,8 @@ function ReportActionsView({ [reportActions, isOffline, canPerformWriteAction], ); + const showLoadingBar = !!isLoadingOlderReportActions || !!isLoadingNewerReportActions || !!(visibleReportActions.length > 0 && isLoadingInitialReportActions); + const reportActionIDMap = useMemo(() => { const reportActionIDs = allReportActions?.map((action) => action.reportActionID); return reportActions.map((action) => ({ @@ -282,7 +287,7 @@ function ReportActionsView({ isOffline || // If there was an error only try again once on initial mount. We should also still load // more in case we have cached messages. - didLoadNewerChats.current || + !!(didLoadNewerChats.current && hasLoadingNewerReportActionsError) || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) ) { return; @@ -303,7 +308,7 @@ function ReportActionsView({ getNewerActions(reportID, newestReportAction.reportActionID); } }, - [reportActionID, isFocused, newestReportAction, hasNewerActions, isOffline, transactionThreadReport, reportActionIDMap, reportID], + [reportActionID, isFocused, newestReportAction, hasNewerActions, isOffline, hasLoadingNewerReportActionsError, transactionThreadReport, reportActionIDMap, reportID], ); /** @@ -372,6 +377,7 @@ function ReportActionsView({ const shouldEnableAutoScroll = (hasNewestReportAction && (!reportActionID || !isNavigatingToLinkedMessage)) || (transactionThreadReport && !prevTransactionThreadReport); return ( <> + progressBarWrapper: { height: 2, - width: '100%', - backgroundColor: theme.transparent, overflow: 'hidden', - position: 'absolute', - bottom: -1, - }, - - progressBar: { - height: '100%', - backgroundColor: theme.success, - width: '100%', }, accountSwitcherAnchorPosition: { diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts index 4d5894812195..8a9fd3cbd804 100644 --- a/src/styles/theme/themes/dark.ts +++ b/src/styles/theme/themes/dark.ts @@ -85,8 +85,8 @@ const darkTheme = { tooltipSupportingText: colors.productLight800, tooltipPrimaryText: colors.productLight900, trialBannerBackgroundColor: colors.green700, - skeletonLHNIn: colors.productDark400, - skeletonLHNOut: colors.productDark600, + skeletonLHNIn: colors.productDark200, + skeletonLHNOut: colors.productDark300, QRLogo: colors.green400, starDefaultBG: 'rgb(254, 228, 94)', mapAttributionText: colors.black, diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts index 1a187e85b0a2..6f4e5ee30e9d 100644 --- a/src/styles/theme/themes/light.ts +++ b/src/styles/theme/themes/light.ts @@ -85,8 +85,8 @@ const lightTheme = { tooltipSupportingText: colors.productDark800, tooltipPrimaryText: colors.productDark900, trialBannerBackgroundColor: colors.green100, - skeletonLHNIn: colors.productLight400, - skeletonLHNOut: colors.productLight600, + skeletonLHNIn: colors.productLight200, + skeletonLHNOut: colors.productLight300, QRLogo: colors.green400, starDefaultBG: 'rgb(254, 228, 94)', mapAttributionText: colors.black, diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 73df63153b1d..e08a4d50f51f 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -359,10 +359,10 @@ describe('Pagination', () => { TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3); TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 2); // We now have 10 messages. 5 from the initial OpenReport and 5 from the GetNewerActions call. - expect(getReportActions()).toHaveLength(10); + expect(getReportActions()).toHaveLength(15); // Simulate the backend returning no new messages to simulate reaching the start of the chat. mockGetNewerActions(0); @@ -374,9 +374,9 @@ describe('Pagination', () => { TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3); TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 3); // We still have 15 messages. 5 from the initial OpenReport and 5 from the GetNewerActions call. - expect(getReportActions()).toHaveLength(10); + expect(getReportActions()).toHaveLength(15); }); });