diff --git a/src/hooks/useFrozenScroll.js b/src/hooks/useFrozenScroll.js
new file mode 100644
index 000000000000..a37dcbf1ee4a
--- /dev/null
+++ b/src/hooks/useFrozenScroll.js
@@ -0,0 +1,10 @@
+import {useContext} from 'react';
+import {ReportActionListFrozenScrollContext} from '../pages/home/report/ReportActionListFrozenScrollContext';
+
+/**
+ * Hook for getting current state of scroll freeze and a function to set whether the scroll should be frozen
+ * @returns {Object}
+ */
+export default function useFrozenScroll() {
+ return useContext(ReportActionListFrozenScrollContext);
+}
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index 15bf25695fd3..83c10f6e4f1e 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -39,6 +39,7 @@ import DragAndDropProvider from '../../components/DragAndDrop/Provider';
import usePrevious from '../../hooks/usePrevious';
import CONST from '../../CONST';
import withCurrentReportID, {withCurrentReportIDPropTypes, withCurrentReportIDDefaultProps} from '../../components/withCurrentReportID';
+import {ReportActionListFrozenScrollContextProvider} from './report/ReportActionListFrozenScrollContext';
const propTypes = {
/** Navigation route context info provided by react navigation */
@@ -357,98 +358,100 @@ function ReportScreen({
reactionListRef,
}}
>
-
-
+
-
- {headerView}
- {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
-
-
-
-
+
+ {headerView}
+ {ReportUtils.isTaskReport(report) && isSmallScreenWidth && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
+
+
+
+
+
-
- )}
-
- {Boolean(accountManagerReportID) && ReportUtils.isConciergeChatReport(report) && isBannerVisible && (
-
- )}
-
- {
- // Rounding this value for comparison because they can look like this: 411.9999694824219
- const newSkeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height);
-
- // The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it
- // takes up so we can set the skeleton view container height.
- if (newSkeletonViewContainerHeight === 0) {
- return;
- }
- setSkeletonViewContainerHeight(newSkeletonViewContainerHeight);
- }}
- >
- {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && (
-
)}
-
- {/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then
- we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
- {(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && }
-
- {isReportReadyForDisplay && (
- <>
-
+ {Boolean(accountManagerReportID) && ReportUtils.isConciergeChatReport(report) && isBannerVisible && (
+
+ )}
+
+ {
+ // Rounding this value for comparison because they can look like this: 411.9999694824219
+ const newSkeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height);
+
+ // The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it
+ // takes up so we can set the skeleton view container height.
+ if (newSkeletonViewContainerHeight === 0) {
+ return;
+ }
+ setSkeletonViewContainerHeight(newSkeletonViewContainerHeight);
+ }}
+ >
+ {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && (
+
- >
- )}
+ )}
- {!isReportReadyForDisplay && (
-
- )}
-
-
-
-
+ {/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then
+ we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
+ {(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && }
+
+ {isReportReadyForDisplay && (
+ <>
+
+ >
+ )}
+
+ {!isReportReadyForDisplay && (
+
+ )}
+
+
+
+
+
);
}
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index 6ce826a2a34c..dfd2758f8f42 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -40,6 +40,7 @@ import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction';
import focusWithDelay from '../../../libs/focusWithDelay';
import ONYXKEYS from '../../../ONYXKEYS';
import * as Browser from '../../../libs/Browser';
+import useFrozenScroll from '../../../hooks/useFrozenScroll';
const propTypes = {
/** All the data of the action */
@@ -122,6 +123,8 @@ function ReportActionItemMessageEdit(props) {
const isFocusedRef = useRef(false);
const insertedEmojis = useRef([]);
+ const {setShouldFreezeScroll} = useFrozenScroll();
+
useEffect(() => {
// required for keeping last state of isFocused variable
isFocusedRef.current = isFocused;
@@ -235,6 +238,7 @@ function ReportActionItemMessageEdit(props) {
* Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content.
*/
const deleteDraft = useCallback(() => {
+ setShouldFreezeScroll(false);
debouncedSaveDraft.cancel();
Report.saveReportActionDraft(props.reportID, props.action.reportActionID, '');
@@ -250,7 +254,7 @@ function ReportActionItemMessageEdit(props) {
keyboardDidHideListener.remove();
});
}
- }, [props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID, reportScrollManager, isActive]);
+ }, [setShouldFreezeScroll, props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID, reportScrollManager, isActive]);
/**
* Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with
@@ -382,8 +386,9 @@ function ReportActionItemMessageEdit(props) {
maxLines={isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES} // This is the same that slack has
style={[styles.textInputCompose, styles.flex1, styles.bgTransparent]}
onFocus={() => {
- setIsFocused(true);
reportScrollManager.scrollToIndex({animated: true, index: props.index}, true);
+ setIsFocused(true);
+ setShouldFreezeScroll(true);
setShouldShowComposeInputKeyboardAware(false);
// Clear active report action when another action gets focused
@@ -396,6 +401,7 @@ function ReportActionItemMessageEdit(props) {
}}
onBlur={(event) => {
setIsFocused(false);
+ setShouldFreezeScroll(false);
const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id');
if (_.contains([messageEditInput, emojiButtonID], relatedTargetId)) {
return;
diff --git a/src/pages/home/report/ReportActionListFrozenScrollContext.js b/src/pages/home/report/ReportActionListFrozenScrollContext.js
new file mode 100644
index 000000000000..7e8fef346a90
--- /dev/null
+++ b/src/pages/home/report/ReportActionListFrozenScrollContext.js
@@ -0,0 +1,58 @@
+import React, {createContext, forwardRef, useMemo, useState} from 'react';
+import PropTypes from 'prop-types';
+import getComponentDisplayName from '../../../libs/getComponentDisplayName';
+
+const withScrollFrozenPropTypes = {
+ /** flag determining if we should freeze the scroll */
+ shouldFreezeScroll: PropTypes.bool,
+
+ /** Function to update the state */
+ setShouldFreezeScroll: PropTypes.func,
+};
+
+const ReportActionListFrozenScrollContext = createContext(null);
+
+function ReportActionListFrozenScrollContextProvider(props) {
+ const [shouldFreezeScroll, setShouldFreezeScroll] = useState(false);
+
+ /**
+ * The context this component exposes to child components
+ * @returns {Object} flag and a flag setter
+ */
+ const contextValue = useMemo(
+ () => ({
+ shouldFreezeScroll,
+ setShouldFreezeScroll,
+ }),
+ [shouldFreezeScroll, setShouldFreezeScroll],
+ );
+
+ return {props.children};
+}
+
+ReportActionListFrozenScrollContextProvider.displayName = 'ReportActionListFrozenScrollContextProvider';
+ReportActionListFrozenScrollContextProvider.propTypes = {
+ /** Actual content wrapped by this component */
+ children: PropTypes.node.isRequired,
+};
+
+function withScrollFrozen(WrappedComponent) {
+ const WithScrollFrozenState = forwardRef((props, ref) => (
+
+ {(scrollFrozenProps) => (
+
+ )}
+
+ ));
+
+ WithScrollFrozenState.displayName = `WithScrollFrozenState(${getComponentDisplayName(WrappedComponent)})`;
+ return WithScrollFrozenState;
+}
+
+export {ReportActionListFrozenScrollContext, ReportActionListFrozenScrollContextProvider, withScrollFrozenPropTypes, withScrollFrozen};
diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js
index f3f40d34a0f5..4520f4f42f76 100644
--- a/src/pages/home/report/ReportActionsList.js
+++ b/src/pages/home/report/ReportActionsList.js
@@ -21,6 +21,7 @@ import reportPropTypes from '../../reportPropTypes';
import FloatingMessageCounter from './FloatingMessageCounter';
import ReportActionsListItemRenderer from './ReportActionsListItemRenderer';
import reportActionPropTypes from './reportActionPropTypes';
+import useFrozenScroll from '../../../hooks/useFrozenScroll';
const propTypes = {
/** The report currently being looked at */
@@ -90,6 +91,10 @@ function keyExtractor(item) {
return item.reportActionID;
}
+const maintainVisibleContentPositionOptions = {
+ minIndexForVisible: 1,
+};
+
function isMessageUnread(message, lastReadTime) {
return Boolean(message && lastReadTime && message.created && lastReadTime < message.created);
}
@@ -130,6 +135,7 @@ function ReportActionsList({
opacity.value = withTiming(1, {duration: 100});
}, [opacity]);
const [skeletonViewHeight, setSkeletonViewHeight] = useState(0);
+ const {shouldFreezeScroll} = useFrozenScroll();
useEffect(() => {
// If the reportID changes, we reset the userActiveSince to null, we need to do it because
@@ -360,6 +366,7 @@ function ReportActionsList({
}}
onScroll={trackVerticalScrolling}
extraData={extraData}
+ maintainVisibleContentPosition={shouldFreezeScroll ? maintainVisibleContentPositionOptions : null}
/>
>