diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e28400505280..99df3bd4dede 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -1,12 +1,78 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef} from 'react'; -import type {FlatListProps} from 'react-native'; +import React, {forwardRef, useEffect, useRef} from 'react'; +import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import FlatList from '@components/FlatList'; +import type {InvertedFlatListProps} from './types'; const WINDOW_SIZE = 15; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; -function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { +function BaseInvertedFlatList({onScroll: onScrollProp = () => {}, onScrollEnd: onScrollEndProp = () => {}, ...props}: InvertedFlatListProps, ref: ForwardedRef) { + const lastScrollEvent = useRef(null); + const scrollEndTimeout = useRef(null); + + useEffect( + () => () => { + if (!scrollEndTimeout.current) { + return; + } + + clearTimeout(scrollEndTimeout.current); + }, + [ref], + ); + + /** + * Emits when the scrolling is in progress. Also, + * invokes the onScroll callback function from props. + */ + const onScroll = (event: NativeSyntheticEvent) => { + onScrollProp(event); + }; + + /** + * Emits when the scrolling has ended. Also, + * invokes the onScrollEnd callback function from props. + */ + const onScrollEnd = () => { + onScrollEndProp(); + }; + + /** + * Decides whether the scrolling has ended or not. If it has ended, + * then it calls the onScrollEnd function. Otherwise, it calls the + * onScroll function and pass the event to it. + * + * This is a temporary work around, since react-native-web doesn't + * support onScrollBeginDrag and onScrollEndDrag props for FlatList. + * More info: + * https://github.com/necolas/react-native-web/pull/1305 + * + * This workaround is taken from below and refactored to fit our needs: + * https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185 + * + */ + const handleScroll = (event: NativeSyntheticEvent) => { + onScroll(event); + + const timestamp = Date.now(); + + if (scrollEndTimeout.current) { + clearTimeout(scrollEndTimeout.current); + } + + scrollEndTimeout.current = setTimeout(() => { + if (lastScrollEvent.current !== timestamp) { + return; + } + // Scroll has ended + lastScrollEvent.current = null; + onScrollEnd(); + }, 250); + + lastScrollEvent.current = timestamp; + }; + return ( (props: FlatListProps, ref: ForwardedRef ); } diff --git a/src/components/InvertedFlatList/index.native.tsx b/src/components/InvertedFlatList/index.native.tsx index 70cabf5a536a..310627373df1 100644 --- a/src/components/InvertedFlatList/index.native.tsx +++ b/src/components/InvertedFlatList/index.native.tsx @@ -1,10 +1,11 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; -import type {FlatList, FlatListProps} from 'react-native'; +import type {FlatList} from 'react-native'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import CellRendererComponent from './CellRendererComponent'; +import type {InvertedFlatListProps} from './types'; -function BaseInvertedFlatListWithRef(props: FlatListProps, ref: ForwardedRef) { +function BaseInvertedFlatListWithRef(props: InvertedFlatListProps, ref: ForwardedRef) { return ( ({onScroll: onScrollProp = () => {}, ...props}: FlatListProps, ref: ForwardedRef) { - const lastScrollEvent = useRef(null); - const scrollEndTimeout = useRef(null); +function InvertedFlatList({onScroll: onScrollProp = () => {}, onScrollEnd: onScrollEndProp = () => {}, ...props}: InvertedFlatListProps, ref: ForwardedRef) { const updateInProgress = useRef(false); - useEffect( - () => () => { - if (!scrollEndTimeout.current) { - return; - } - clearTimeout(scrollEndTimeout.current); - }, - [ref], - ); - /** * Emits when the scrolling is in progress. Also, * invokes the onScroll callback function from props. * * @param event - The onScroll event from the FlatList */ - const onScroll = (event: NativeSyntheticEvent) => { + const handleScroll = (event: NativeSyntheticEvent) => { onScrollProp(event); if (!updateInProgress.current) { @@ -39,47 +28,14 @@ function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: Flat }; /** - * Emits when the scrolling has ended. + * Emits when the scrolling has ended. Also, + * invokes the onScrollEnd callback function from props. */ - const onScrollEnd = () => { + const handleScrollEnd = () => { DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, false); updateInProgress.current = false; - }; - - /** - * Decides whether the scrolling has ended or not. If it has ended, - * then it calls the onScrollEnd function. Otherwise, it calls the - * onScroll function and pass the event to it. - * - * This is a temporary work around, since react-native-web doesn't - * support onScrollBeginDrag and onScrollEndDrag props for FlatList. - * More info: - * https://github.com/necolas/react-native-web/pull/1305 - * - * This workaround is taken from below and refactored to fit our needs: - * https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185 - * - */ - const handleScroll = (event: NativeSyntheticEvent) => { - onScroll(event); - const timestamp = Date.now(); - - if (scrollEndTimeout.current) { - clearTimeout(scrollEndTimeout.current); - } - - if (lastScrollEvent.current) { - scrollEndTimeout.current = setTimeout(() => { - if (lastScrollEvent.current !== timestamp) { - return; - } - // Scroll has ended - lastScrollEvent.current = null; - onScrollEnd(); - }, 250); - } - lastScrollEvent.current = timestamp; + onScrollEndProp(); }; return ( @@ -88,6 +44,7 @@ function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: Flat {...props} ref={ref} onScroll={handleScroll} + onScrollEnd={handleScrollEnd} CellRendererComponent={CellRendererComponent} /> ); diff --git a/src/components/InvertedFlatList/types.ts b/src/components/InvertedFlatList/types.ts new file mode 100644 index 000000000000..c23d02f55e7b --- /dev/null +++ b/src/components/InvertedFlatList/types.ts @@ -0,0 +1,9 @@ +import {FlatListProps} from 'react-native'; + +type InvertedFlatListProps = FlatListProps & { + /** Handler called when the scroll actions ends */ + onScrollEnd: () => void; +}; + +export default InvertedFlatListProps; +export type {InvertedFlatListProps}; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2f9e3222206b..6a9462a3ad8c 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -159,6 +159,7 @@ function ReportActionsList({ const hasFooterRendered = useRef(false); const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated); const lastReadTimeRef = useRef(report.lastReadTime); + const isUnreadMessageFocused = useRef(false); const sortedVisibleReportActions = useMemo( () => _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors), @@ -307,31 +308,55 @@ function ReportActionsList({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [report.reportID]); + /** + * Check if the new floating message counter needs to be shown/hidden. + * @returns {boolean} + */ + const showFloatingMessageCounter = () => { + const hasUnreadMessageAndFloatingMessageIsHidden = !isFloatingMessageCounterVisible && !!currentUnreadMarker; + const isFocusOutsideUnreadMessage = isFloatingMessageCounterVisible && !isUnreadMessageFocused.current; + + if (isFloatingMessageCounterVisible && isUnreadMessageFocused.current) { + isUnreadMessageFocused.current = false; + } + + return hasUnreadMessageAndFloatingMessageIsHidden || isFocusOutsideUnreadMessage; + }; + /** * Show/hide the new floating message counter when user is scrolling back/forth in the history of messages. */ const handleUnreadFloatingButton = () => { - if (scrollingVerticalOffset.current > VERTICAL_OFFSET_THRESHOLD && !isFloatingMessageCounterVisible && !!currentUnreadMarker) { - setIsFloatingMessageCounterVisible(true); - } + const showMessageCounter = showFloatingMessageCounter(); + + setIsFloatingMessageCounterVisible(showMessageCounter); if (scrollingVerticalOffset.current < VERTICAL_OFFSET_THRESHOLD && isFloatingMessageCounterVisible) { if (readActionSkipped.current) { readActionSkipped.current = false; Report.readNewestAction(report.reportID); } - setIsFloatingMessageCounterVisible(false); } }; const trackVerticalScrolling = (event) => { scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; - handleUnreadFloatingButton(); onScroll(event); }; - const scrollToBottomAndMarkReportAsRead = () => { - reportScrollManager.scrollToBottom(); + const getScrollIndex = () => { + const bottomIndex = 0; + const firstUnreadMessageIndex = _.findIndex(sortedReportActions, (reportAction) => reportAction.reportActionID === currentUnreadMarker); + + return firstUnreadMessageIndex > -1 ? firstUnreadMessageIndex : bottomIndex; + }; + + const scrollToUnreadMessageAndMarkReportAsRead = () => { + const scrollIndex = getScrollIndex(); + + isUnreadMessageFocused.current = true; + + reportScrollManager.scrollToIndex(scrollIndex, false); readActionSkipped.current = false; Report.readNewestAction(report.reportID); }; @@ -481,7 +506,7 @@ function ReportActionsList({ <> {}} extraData={extraData} />