From 3f500dbb58ae3631babab4d9f79e5b9686cd88eb Mon Sep 17 00:00:00 2001 From: William Quayson Date: Wed, 2 Jul 2025 13:32:31 -0300 Subject: [PATCH 01/96] chore: remove SwipeableView component --- src/components/SwipeableView/index.native.tsx | 32 ------------------- src/components/SwipeableView/index.tsx | 4 --- src/components/SwipeableView/types.ts | 11 ------- src/pages/home/report/ReportFooter.tsx | 29 ++++++++--------- 4 files changed, 13 insertions(+), 63 deletions(-) delete mode 100644 src/components/SwipeableView/index.native.tsx delete mode 100644 src/components/SwipeableView/index.tsx delete mode 100644 src/components/SwipeableView/types.ts diff --git a/src/components/SwipeableView/index.native.tsx b/src/components/SwipeableView/index.native.tsx deleted file mode 100644 index 4376585c6f0a..000000000000 --- a/src/components/SwipeableView/index.native.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, {useRef} from 'react'; -import {PanResponder, View} from 'react-native'; -import CONST from '@src/CONST'; -import type SwipeableViewProps from './types'; - -function SwipeableView({children, onSwipeDown}: SwipeableViewProps) { - const minimumPixelDistance = CONST.COMPOSER_MAX_HEIGHT; - const oldYRef = useRef(0); - const panResponder = useRef( - // eslint-disable-next-line react-compiler/react-compiler - PanResponder.create({ - // The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards - onMoveShouldSetPanResponderCapture: (_event, gestureState) => { - if (gestureState.dy - oldYRef.current > 0 && gestureState.dy > minimumPixelDistance) { - return true; - } - oldYRef.current = gestureState.dy; - return false; - }, - - // Calls the callback when the swipe down is released; after the completion of the gesture - onPanResponderRelease: onSwipeDown, - }), - ).current; - - // eslint-disable-next-line react/jsx-props-no-spreading, react-compiler/react-compiler - return {children}; -} - -SwipeableView.displayName = 'SwipeableView'; - -export default SwipeableView; diff --git a/src/components/SwipeableView/index.tsx b/src/components/SwipeableView/index.tsx deleted file mode 100644 index d3881d2efd21..000000000000 --- a/src/components/SwipeableView/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import type SwipeableViewProps from './types'; - -// Swipeable View is available just on Android/iOS for now. -export default ({children}: SwipeableViewProps) => children; diff --git a/src/components/SwipeableView/types.ts b/src/components/SwipeableView/types.ts deleted file mode 100644 index 738e21bb73ee..000000000000 --- a/src/components/SwipeableView/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {ReactNode} from 'react'; - -type SwipeableViewProps = { - /** The content to be rendered within the SwipeableView */ - children: ReactNode; - - /** Callback to fire when the user swipes down on the child content */ - onSwipeDown: () => void; -}; - -export default SwipeableViewProps; diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 50219904d0ab..a642c787049d 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -1,7 +1,7 @@ import {Str} from 'expensify-common'; import {deepEqual} from 'fast-equals'; import React, {memo, useCallback, useEffect, useState} from 'react'; -import {Keyboard, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import AnonymousReportFooter from '@components/AnonymousReportFooter'; import ArchivedReportFooter from '@components/ArchivedReportFooter'; @@ -10,7 +10,6 @@ import BlockedReportFooter from '@components/BlockedReportFooter'; import * as Expensicons from '@components/Icon/Expensicons'; import OfflineIndicator from '@components/OfflineIndicator'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; -import SwipeableView from '@components/SwipeableView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -220,20 +219,18 @@ function ReportFooter({ )} {!shouldHideComposer && (!!shouldShowComposeInput || !shouldUseNarrowLayout) && ( - - - + )} From e21b4fa7d949847b10255b384317df3cafecf4e9 Mon Sep 17 00:00:00 2001 From: William Quayson Date: Fri, 4 Jul 2025 18:28:19 -0300 Subject: [PATCH 02/96] chore: track chat scroll using reanimated --- src/pages/home/ReportScreen.tsx | 244 ++++++++++++------ .../ComposerWithSuggestions.tsx | 4 + .../ReportActionCompose.tsx | 4 + src/pages/home/report/ReportActionsList.tsx | 36 ++- src/pages/home/report/ReportActionsView.tsx | 9 + src/pages/home/report/ReportFooter.tsx | 4 + .../useReportUnreadMessageScrollTracking.ts | 41 +-- 7 files changed, 210 insertions(+), 132 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 5ecf5098cb9a..ec2098c3b338 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -4,7 +4,9 @@ import {deepEqual} from 'fast-equals'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {FlatList, ViewStyle} from 'react-native'; import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; +import {KeyboardGestureArea, useKeyboardHandler} from 'react-native-keyboard-controller'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {useAnimatedScrollHandler, useSharedValue} from 'react-native-reanimated'; import Banner from '@components/Banner'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -133,6 +135,69 @@ function getParentReportAction(parentReportActions: OnyxEntry { + 'worklet'; + + // i. e. the keyboard was under interactive gesture, and will be showed + // again. Since iOS will not schedule layout animation for that we can't + // simply update `height` to destination and we need to listen to `onMove` + // handler to have a smooth animation + if (progress.value !== 1 && progress.value !== 0 && e.height !== 0) { + // eslint-disable-next-line react-compiler/react-compiler + shouldUseOnMoveHandler.value = true; + + return; + } + + progress.value = e.progress; + height.value = e.height; + + inset.value = e.height; + // Math.max is needed to prevent overscroll when keyboard hides (and user scrolled to the top, for example) + offset.value = Math.max(e.height + scrollY.value, 0); + }, + onInteractive: (e) => { + 'worklet'; + + progress.value = e.progress; + height.value = e.height; + }, + onMove: (e) => { + 'worklet'; + + if (shouldUseOnMoveHandler.value) { + progress.value = e.progress; + height.value = e.height; + } + }, + onEnd: (e) => { + 'worklet'; + + height.value = e.height; + progress.value = e.progress; + shouldUseOnMoveHandler.value = false; + }, + }); + + const onScroll = useAnimatedScrollHandler({ + onScroll: (e) => { + scrollY.set(e.contentOffset.y); + // scroll.value = e.contentOffset.y - inset.value; + }, + }); + + return {height, progress, onScroll, inset, offset, scrollY}; +} + function ReportScreen({route, navigation}: ReportScreenProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -148,6 +213,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const {isOffline} = useNetwork(); const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const currentReportIDValue = useCurrentReportID(); + const {scrollY, onScroll} = useKeyboardAnimation(); const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {canBeMissing: true}); const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, {canBeMissing: true}); @@ -785,93 +851,103 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, visibleTransactions ?? []); return ( - - - - + + + - - {headerView} - - {!!accountManagerReportID && isConciergeChatReport(report) && isBannerVisible && ( - - )} - - - {(!report || shouldWaitForTransactions) && } - {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( - - ) : null} - {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( - - ) : null} - {isCurrentReportLoadedFromOnyx ? ( - - ) : null} - - - - - - - + {headerView} + + {!!accountManagerReportID && isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {(!report || shouldWaitForTransactions) && } + {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( + + ) : null} + {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( + + ) : null} + {isCurrentReportLoadedFromOnyx ? ( + + ) : null} + + + + + + + + ); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index d9c14cdeae53..05749af87f58 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -144,6 +144,8 @@ type ComposerWithSuggestionsProps = Partial & { /** Whether the main composer was hidden */ didHideComposerInput?: boolean; + + nativeID?: string; }; type SwitchToCurrentReportProps = { @@ -223,6 +225,7 @@ function ComposerWithSuggestions( raiseIsScrollLikelyLayoutTriggered, onCleared = () => {}, onLayout: onLayoutProps, + nativeID, // Refs suggestionsRef, @@ -825,6 +828,7 @@ function ComposerWithSuggestions( onScroll={hideSuggestionMenu} shouldContainScroll={isMobileSafari()} isGroupPolicyReport={isGroupPolicyReport} + nativeID={nativeID} /> diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 53049408caab..35ff39e5a127 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -107,6 +107,8 @@ type ReportActionComposeProps = Pick