From e14ba5d874daa03a3c244c7e97aa0aa70970a455 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 27 Sep 2023 15:31:00 +0700 Subject: [PATCH 001/106] don't allow go to next step if empty waypoint --- .../MoneyRequestParticipantsPage.js | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 8d745903eb40..c6d0cd541944 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -17,6 +17,7 @@ import * as IOU from '../../../../libs/actions/IOU'; import * as MoneyRequestUtils from '../../../../libs/MoneyRequestUtils'; import {iouPropTypes, iouDefaultProps} from '../../propTypes'; import useLocalize from '../../../../hooks/useLocalize'; +import compose from '../../../../libs/compose'; const propTypes = { /** React Navigation route */ @@ -42,7 +43,7 @@ const defaultProps = { iou: iouDefaultProps, }; -function MoneyRequestParticipantsPage({iou, selectedTab, route}) { +function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { const {translate} = useLocalize(); const prevMoneyRequestId = useRef(iou.id); const isNewReportIDSelectedLocally = useRef(false); @@ -53,7 +54,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); const isSplitRequest = iou.id === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT; const [headerTitle, setHeaderTitle] = useState(); - + const isEmptyWaypoint = _.isEmpty(lodashGet(transaction, 'comment.waypoints.waypoint0', {})); useEffect(() => { if (isDistanceRequest) { setHeaderTitle(translate('common.distance')); @@ -85,10 +86,12 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { }; useEffect(() => { + const isInvalidDistanceRequest = !isDistanceRequest || isEmptyWaypoint; + // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request if (prevMoneyRequestId.current !== iou.id) { // The ID is cleared on completing a request. In that case, we will do nothing - if (iou.id && !isDistanceRequest && !isSplitRequest && !isNewReportIDSelectedLocally.current) { + if (iou.id && isInvalidDistanceRequest && !isSplitRequest && !isNewReportIDSelectedLocally.current) { navigateBack(true); } return; @@ -100,14 +103,14 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { if (shouldReset) { IOU.resetMoneyRequestInfo(moneyRequestId); } - if (!isDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) { + if (isInvalidDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) { navigateBack(true); } return () => { prevMoneyRequestId.current = iou.id; }; - }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest]); + }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, isEmptyWaypoint]); return ( `${ONYXKEYS.COLLECTION.TRANSACTION}${iou.transactionID}`, + }, + }), +)(MoneyRequestParticipantsPage); From 0e23a7b844036cd1e9218d6c8b18c97d6d093326 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 27 Sep 2023 15:45:57 +0700 Subject: [PATCH 002/106] fix lint --- .../MoneyRequestParticipantsPage.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index c6d0cd541944..98e5e0976b7a 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -18,6 +18,7 @@ import * as MoneyRequestUtils from '../../../../libs/MoneyRequestUtils'; import {iouPropTypes, iouDefaultProps} from '../../propTypes'; import useLocalize from '../../../../hooks/useLocalize'; import compose from '../../../../libs/compose'; +import transactionPropTypes from '../../../../components/transactionPropTypes'; const propTypes = { /** React Navigation route */ @@ -37,10 +38,14 @@ const propTypes = { /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ selectedTab: PropTypes.oneOf([CONST.TAB.DISTANCE, CONST.TAB.MANUAL, CONST.TAB.SCAN]).isRequired, + + /** Transaction that stores the distance request data */ + transaction: transactionPropTypes, }; const defaultProps = { iou: iouDefaultProps, + transaction: {}, }; function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { From 537af3d2e4826966db8cc7a1bb46f344ad3901c5 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 27 Sep 2023 17:56:23 +0700 Subject: [PATCH 003/106] fix lint issue --- .../MoneyRequestParticipantsPage.js | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 98e5e0976b7a..31785081f6ab 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -17,7 +17,6 @@ import * as IOU from '../../../../libs/actions/IOU'; import * as MoneyRequestUtils from '../../../../libs/MoneyRequestUtils'; import {iouPropTypes, iouDefaultProps} from '../../propTypes'; import useLocalize from '../../../../hooks/useLocalize'; -import compose from '../../../../libs/compose'; import transactionPropTypes from '../../../../components/transactionPropTypes'; const propTypes = { @@ -151,18 +150,14 @@ MoneyRequestParticipantsPage.displayName = 'IOUParticipantsPage'; MoneyRequestParticipantsPage.propTypes = propTypes; MoneyRequestParticipantsPage.defaultProps = defaultProps; -export default compose( - withOnyx({ - iou: { - key: ONYXKEYS.IOU, - }, - selectedTab: { - key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, - }, - }), - withOnyx({ - transaction: { - key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou.transactionID}`, - }, - }), -)(MoneyRequestParticipantsPage); +export default withOnyx({ + iou: { + key: ONYXKEYS.IOU, + }, + selectedTab: { + key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, + }, + transaction: { + key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou.transactionID}`, + }, +})(MoneyRequestParticipantsPage); From add086352c19883fe1b34cad8c2243b31be674d2 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 28 Sep 2023 02:58:18 +0700 Subject: [PATCH 004/106] fix cannot read property undefined transactionID --- .../MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 31785081f6ab..87b27b27622a 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -158,6 +158,6 @@ export default withOnyx({ key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, }, transaction: { - key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou.transactionID}`, + key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou?.transactionID}`, }, })(MoneyRequestParticipantsPage); From bab3fc8c1ae4ca870c06eb253870631ad6f3731d Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 28 Sep 2023 03:10:00 +0700 Subject: [PATCH 005/106] fix optional chaining --- .../MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 87b27b27622a..98682e1af490 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -158,6 +158,6 @@ export default withOnyx({ key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, }, transaction: { - key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou?.transactionID}`, + key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou ? iou.transactionID : ''}`, }, })(MoneyRequestParticipantsPage); From 8c3526d404d96f513fc6c093b97ca467c24af1f2 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 28 Sep 2023 13:30:40 +0700 Subject: [PATCH 006/106] make logic more accurate --- .../MoneyRequestParticipantsPage.js | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 98682e1af490..0059a6037fec 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import _ from 'underscore'; +import compose from '../../../../libs/compose'; import CONST from '../../../../CONST'; import ONYXKEYS from '../../../../ONYXKEYS'; import ROUTES from '../../../../ROUTES'; @@ -14,6 +15,7 @@ import Navigation from '../../../../libs/Navigation/Navigation'; import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; import * as IOU from '../../../../libs/actions/IOU'; +import * as TransactionUtils from '../../../../libs/TransactionUtils'; import * as MoneyRequestUtils from '../../../../libs/MoneyRequestUtils'; import {iouPropTypes, iouDefaultProps} from '../../propTypes'; import useLocalize from '../../../../hooks/useLocalize'; @@ -58,7 +60,9 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); const isSplitRequest = iou.id === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT; const [headerTitle, setHeaderTitle] = useState(); - const isEmptyWaypoint = _.isEmpty(lodashGet(transaction, 'comment.waypoints.waypoint0', {})); + const waypoints = lodashGet(transaction, 'comment.waypoints', {}); + const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); + const isInvalidWaypoint = _.size(validatedWaypoints) < 2; useEffect(() => { if (isDistanceRequest) { setHeaderTitle(translate('common.distance')); @@ -90,7 +94,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { }; useEffect(() => { - const isInvalidDistanceRequest = !isDistanceRequest || isEmptyWaypoint; + const isInvalidDistanceRequest = !isDistanceRequest || isInvalidWaypoint; // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request if (prevMoneyRequestId.current !== iou.id) { @@ -114,7 +118,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { return () => { prevMoneyRequestId.current = iou.id; }; - }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, isEmptyWaypoint]); + }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, isInvalidWaypoint]); return ( `${ONYXKEYS.COLLECTION.TRANSACTION}${iou ? iou.transactionID : ''}`, - }, -})(MoneyRequestParticipantsPage); +export default compose( + withOnyx({ + iou: { + key: ONYXKEYS.IOU, + }, + selectedTab: { + key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, + }, + }), + // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file + withOnyx({ + transaction: { + key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${iou.transactionID}`, + }, + }), +)(MoneyRequestParticipantsPage); From 3ecb3ef3587f347c41e4a7651854bab14cc3701d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 28 Sep 2023 13:54:25 +0100 Subject: [PATCH 007/106] update index.js --- .../HTMLRenderers/PreRenderer/index.js | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index efc9e432cba8..e4aec1446135 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -1,70 +1,66 @@ -import React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import _ from 'underscore'; -import withLocalize from '../../../withLocalize'; + +import ControlSelection from '../../../../libs/ControlSelection'; +import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; -import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; -import ControlSelection from '../../../../libs/ControlSelection'; - -class PreRenderer extends React.Component { - constructor(props) { - super(props); - this.scrollNode = this.scrollNode.bind(this); - this.debouncedIsScrollingVertically = _.debounce(this.isScrollingVertically.bind(this), 100, true); - } +const isScrollingVertically = (event) => + // Mark as vertical scrolling only when absolute value of deltaY is more than the double of absolute + // value of deltaX, so user can use trackpad scroll on the code block horizontally at a wide angle. + Math.abs(event.deltaY) > Math.abs(event.deltaX) * 2; - componentDidMount() { - if (!this.ref) { - return; - } - this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode); - } +const debouncedIsScrollingVertically = (event) => _.debounce(isScrollingVertically(event), 100, true); - componentWillUnmount() { - this.ref.getScrollableNode().removeEventListener('wheel', this.scrollNode); - } +function PreRenderer(props) { + const scrollViewRef = useRef(); /** - * Check if user is scrolling vertically based on deltaX and deltaY. We debounce this - * method in the constructor to make sure it's called only for the first event. + * Checks if user is scrolling vertically based on deltaX and deltaY. We debounce this + * method in order to make sure it's called only for the first event. * @param {WheelEvent} event Wheel event * @returns {Boolean} true if user is scrolling vertically */ - isScrollingVertically(event) { - // Mark as vertical scrolling only when absolute value of deltaY is more than the double of absolute - // value of deltaX, so user can use trackpad scroll on the code block horizontally at a wide angle. - return Math.abs(event.deltaY) > Math.abs(event.deltaX) * 2; - } /** * Manually scrolls the code block if code block horizontal scrollable, then prevents the event from being passed up to the parent. * @param {Object} event native event */ - scrollNode(event) { - const node = this.ref.getScrollableNode(); + const scrollNode = useCallback((event) => { + const node = scrollViewRef.current.getScrollableNode(); const horizontalOverflow = node.scrollWidth > node.offsetWidth; - const isScrollingVertically = this.debouncedIsScrollingVertically(event); - if (event.currentTarget === node && horizontalOverflow && !isScrollingVertically) { + if (event.currentTarget === node && horizontalOverflow && !debouncedIsScrollingVertically(event)) { node.scrollLeft += event.deltaX; event.preventDefault(); event.stopPropagation(); } - } + }, []); + + useEffect(() => { + const eventListenerRefValue = scrollViewRef.current; + if (!eventListenerRefValue) { + return; + } + eventListenerRefValue.getScrollableNode().addEventListener('wheel', scrollNode); + + return () => { + eventListenerRefValue.getScrollableNode().removeEventListener('wheel', scrollNode); + }; + }, [scrollNode]); - render() { - return ( - (this.ref = el)} - onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - /> - ); - } + return ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={ControlSelection.unblock} + /> + ); } PreRenderer.propTypes = htmlRendererPropTypes; +PreRenderer.displayName = 'PreRenderer'; -export default withLocalize(PreRenderer); +export default PreRenderer; \ No newline at end of file From eb52c3e5d55cc62c2a50d4ce067ff3caf3d92a62 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 07:28:32 +0100 Subject: [PATCH 008/106] migrate clipboard to TS --- src/libs/Clipboard/index.native.js | 18 ----- src/libs/Clipboard/index.native.ts | 19 ++++++ src/libs/Clipboard/{index.js => index.ts} | 83 +++++++++++++++-------- src/libs/Clipboard/types.ts | 9 +++ src/types/modules/react-native-web.d.ts | 7 ++ 5 files changed, 88 insertions(+), 48 deletions(-) delete mode 100644 src/libs/Clipboard/index.native.js create mode 100644 src/libs/Clipboard/index.native.ts rename src/libs/Clipboard/{index.js => index.ts} (53%) create mode 100644 src/libs/Clipboard/types.ts create mode 100644 src/types/modules/react-native-web.d.ts diff --git a/src/libs/Clipboard/index.native.js b/src/libs/Clipboard/index.native.js deleted file mode 100644 index d6345ac94a36..000000000000 --- a/src/libs/Clipboard/index.native.js +++ /dev/null @@ -1,18 +0,0 @@ -import Clipboard from '@react-native-community/clipboard'; - -/** - * Sets a string on the Clipboard object via @react-native-community/clipboard - * - * @param {String} text - */ -const setString = (text) => { - Clipboard.setString(text); -}; - -export default { - setString, - - // We don't want to set HTML on native platforms so noop them. - canSetHtml: () => false, - setHtml: () => {}, -}; diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts new file mode 100644 index 000000000000..bc7da7ad9d3f --- /dev/null +++ b/src/libs/Clipboard/index.native.ts @@ -0,0 +1,19 @@ +import RNCClipboard from '@react-native-community/clipboard'; +import {SetString, Clipboard} from './types'; + +/** + * Sets a string on the Clipboard object via @react-native-community/clipboard + */ +const setString: SetString = (text) => { + RNCClipboard.setString(text); +}; + +const clipboard: Clipboard = { + setString, + + // We don't want to set HTML on native platforms so noop them. + canSetHtml: () => false, + setHtml: () => {}, +}; + +export default clipboard; diff --git a/src/libs/Clipboard/index.js b/src/libs/Clipboard/index.ts similarity index 53% rename from src/libs/Clipboard/index.js rename to src/libs/Clipboard/index.ts index b770b2f2c787..fe0515edc585 100644 --- a/src/libs/Clipboard/index.js +++ b/src/libs/Clipboard/index.ts @@ -1,17 +1,37 @@ // on Web/desktop this import will be replaced with `react-native-web` -import {Clipboard} from 'react-native-web'; -import lodashGet from 'lodash/get'; +import {Clipboard as RNWClipboard} from 'react-native-web'; import CONST from '../../CONST'; import * as Browser from '../Browser'; +import {SetString, Clipboard} from './types'; -const canSetHtml = () => lodashGet(navigator, 'clipboard.write'); +type ComposerSelection = { + start: number; + end: number; + direction: 'forward' | 'backward' | 'none'; +}; + +type AnchorSelection = { + anchorOffset: number; + focusOffset: number; + anchorNode: Node; + focusNode: Node; +}; + +type Nullable = {[K in keyof T]: T[K] | null}; +type OriginalSelection = ComposerSelection | Partial>; + +/* +* @param {this: void} object The object to query. +*/ + +const canSetHtml = () => (...args: ClipboardItems) => navigator?.clipboard?.write([...args]); /** * Deprecated method to write the content as HTML to clipboard. - * @param {String} html HTML representation - * @param {String} text Plain text representation + * @param HTML representation + * @param Plain text representation */ -function setHTMLSync(html, text) { +function setHTMLSync(html: string, text: string) { const node = document.createElement('span'); node.textContent = html; node.style.all = 'unset'; @@ -22,16 +42,16 @@ function setHTMLSync(html, text) { node.addEventListener('copy', (e) => { e.stopPropagation(); e.preventDefault(); - e.clipboardData.clearData(); - e.clipboardData.setData('text/html', html); - e.clipboardData.setData('text/plain', text); + e.clipboardData?.clearData(); + e.clipboardData?.setData('text/html', html); + e.clipboardData?.setData('text/plain', text); }); document.body.appendChild(node); - const selection = window.getSelection(); - const firstAnchorChild = selection.anchorNode && selection.anchorNode.firstChild; + const selection = window?.getSelection(); + const firstAnchorChild = selection?.anchorNode?.firstChild; const isComposer = firstAnchorChild instanceof HTMLTextAreaElement; - let originalSelection = null; + let originalSelection: OriginalSelection | null = null; if (isComposer) { originalSelection = { start: firstAnchorChild.selectionStart, @@ -40,17 +60,17 @@ function setHTMLSync(html, text) { }; } else { originalSelection = { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, + anchorNode: selection?.anchorNode, + anchorOffset: selection?.anchorOffset, + focusNode: selection?.focusNode, + focusOffset: selection?.focusOffset, }; } - selection.removeAllRanges(); + selection?.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(node); - selection.addRange(range); + selection?.addRange(range); try { document.execCommand('copy'); @@ -59,12 +79,14 @@ function setHTMLSync(html, text) { // See https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#the-copy-command for more details. } - selection.removeAllRanges(); + selection?.removeAllRanges(); if (isComposer) { - firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction); + const composerSelection = originalSelection as ComposerSelection; + firstAnchorChild.setSelectionRange(composerSelection.start, composerSelection.end, composerSelection.direction); } else { - selection.setBaseAndExtent(originalSelection.anchorNode, originalSelection.anchorOffset, originalSelection.focusNode, originalSelection.focusOffset); + const anchorSelection = originalSelection as AnchorSelection; + selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); } document.body.removeChild(node); @@ -72,10 +94,10 @@ function setHTMLSync(html, text) { /** * Writes the content as HTML if the web client supports it. - * @param {String} html HTML representation - * @param {String} text Plain text representation + * @param HTML representation + * @param Plain text representation */ -const setHtml = (html, text) => { +const setHtml = (html: string, text: string) => { if (!html || !text) { return; } @@ -92,9 +114,10 @@ const setHtml = (html, text) => { setHTMLSync(html, text); } else { navigator.clipboard.write([ - // eslint-disable-next-line no-undef new ClipboardItem({ + // eslint-disable-next-line @typescript-eslint/naming-convention 'text/html': new Blob([html], {type: 'text/html'}), + // eslint-disable-next-line @typescript-eslint/naming-convention 'text/plain': new Blob([text], {type: 'text/plain'}), }), ]); @@ -103,15 +126,15 @@ const setHtml = (html, text) => { /** * Sets a string on the Clipboard object via react-native-web - * - * @param {String} text */ -const setString = (text) => { - Clipboard.setString(text); +const setString: SetString = (text) => { + RNWClipboard.setString(text); }; -export default { +const clipboard: Clipboard = { setString, canSetHtml, setHtml, }; + +export default clipboard; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts new file mode 100644 index 000000000000..e4b7cb1b5332 --- /dev/null +++ b/src/libs/Clipboard/types.ts @@ -0,0 +1,9 @@ +type SetString = (text: string) => void; + +type Clipboard = { + setString: SetString; + canSetHtml: () => void; + setHtml: (html: string, text: string) => void; +} + +export type {SetString, Clipboard}; \ No newline at end of file diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts new file mode 100644 index 000000000000..067e2f95e07a --- /dev/null +++ b/src/types/modules/react-native-web.d.ts @@ -0,0 +1,7 @@ +declare module 'react-native-web' { + type SetString = (text: string) => void; + + const Clipboard: { + setString: SetString; + } +} \ No newline at end of file From 82abacaa4985587dd730465d75652aa37a12b00d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 07:56:04 +0100 Subject: [PATCH 009/106] fix lint --- src/libs/Clipboard/index.ts | 9 ++++++--- src/libs/Clipboard/types.ts | 4 ++-- src/types/modules/react-native-web.d.ts | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index fe0515edc585..8c2d1be1afd5 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -21,10 +21,13 @@ type Nullable = {[K in keyof T]: T[K] | null}; type OriginalSelection = ComposerSelection | Partial>; /* -* @param {this: void} object The object to query. -*/ + * @param {this: void} object The object to query. + */ -const canSetHtml = () => (...args: ClipboardItems) => navigator?.clipboard?.write([...args]); +const canSetHtml = + () => + (...args: ClipboardItems) => + navigator?.clipboard?.write([...args]); /** * Deprecated method to write the content as HTML to clipboard. diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index e4b7cb1b5332..92a90df85e02 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -4,6 +4,6 @@ type Clipboard = { setString: SetString; canSetHtml: () => void; setHtml: (html: string, text: string) => void; -} +}; -export type {SetString, Clipboard}; \ No newline at end of file +export type {SetString, Clipboard}; diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts index 067e2f95e07a..da723e9a811d 100644 --- a/src/types/modules/react-native-web.d.ts +++ b/src/types/modules/react-native-web.d.ts @@ -1,7 +1,7 @@ declare module 'react-native-web' { type SetString = (text: string) => void; - + const Clipboard: { setString: SetString; - } -} \ No newline at end of file + }; +} From 8ba2000e210551890e44f576250d50678c6bc9c2 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:11:43 +0200 Subject: [PATCH 010/106] create MVCPScrollView --- .../MVCPScrollView/MVCPScrollView.js | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js diff --git a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js new file mode 100644 index 000000000000..f0139a4ec39c --- /dev/null +++ b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js @@ -0,0 +1,128 @@ +import React, {forwardRef, useEffect, useRef} from 'react'; +import {ScrollView, StyleSheet} from 'react-native'; +import PropTypes from 'prop-types'; + + +const MVCPScrollView = forwardRef(({maintainVisibleContentPosition, horizontal, ...props}, ref) => { + const scrollViewRef = useRef(null); + const prevFirstVisibleOffset = useRef(null); + const firstVisibleView = useRef(null); + const mutationObserver = useRef(null); + + const getContentView = () => scrollViewRef.current?.childNodes[0]; + + const prepareForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null) { + return; + } + + const contentView = getContentView(); + const minIdx = maintainVisibleContentPosition.minIndexForVisible; + for (let ii = minIdx; ii < contentView.childNodes.length; ii++) { + const subview = contentView.childNodes[ii]; + const hasNewView = horizontal ? subview.offsetLeft > scrollViewRef.current.scrollLeft : subview.offsetTop > scrollViewRef.current.scrollTop; + if (hasNewView || ii === contentView.childNodes.length - 1) { + prevFirstVisibleOffset.current = horizontal ? subview.offsetLeft : subview.offsetTop; + firstVisibleView.current = subview; + break; + } + } + }; + const scrollEventListener = useRef(() => { + prepareForMaintainVisibleContentPosition(); + }); + + const adjustForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null || firstVisibleView.current == null || prevFirstVisibleOffset.current == null) { + return; + } + + const autoscrollThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold; + if (horizontal) { + const deltaX = firstVisibleView.current.offsetLeft - prevFirstVisibleOffset.current; + if (Math.abs(deltaX) > 0.5) { + const x = scrollViewRef.current.scrollLeft; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetLeft; + scrollViewRef.current.scrollTo({x: x + deltaX, animated: false}); + if (autoscrollThreshold != null && x <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({x: 0, animated: true}); + } + } + } else { + const deltaY = firstVisibleView.current.offsetTop - prevFirstVisibleOffset.current; + if (Math.abs(deltaY) > 0.5) { + const y = scrollViewRef.current.scrollTop; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetTop; + scrollViewRef.current.scrollTo({y: y + deltaY, animated: false}); + if (autoscrollThreshold != null && y <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({y: 0, animated: true}); + } + } + } + }; + + if (mutationObserver.current == null) { + mutationObserver.current = new MutationObserver(() => { + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(adjustForMaintainVisibleContentPosition); + }); + } + + const onRef = (newRef) => { + scrollViewRef.current = newRef; + if (typeof ref === 'function') { + ref(newRef); + } else { + // eslint-disable-next-line no-param-reassign + ref.current = newRef; + } + prepareForMaintainVisibleContentPosition(); + mutationObserver.current.disconnect(); + mutationObserver.current.observe(getContentView(), { + attributes: true, + childList: true, + subtree: true, + }); + newRef.removeEventListener('scroll', scrollEventListener.current); + newRef.addEventListener('scroll', scrollEventListener.current); + }; + + useEffect(() => { + const currentObserver = mutationObserver.current; + const currentScrollEventListener = scrollEventListener.current; + return () => { + currentObserver.disconnect(); + scrollViewRef.current.removeEventListener('scroll', currentScrollEventListener); + }; + }, []); + + return ( + + ); +}); + +const styles = StyleSheet.create({ + inverted: { + transform: [{ scaleY: -1 }], + }, +}); + + +MVCPScrollView.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +export default MVCPScrollView; From ef49188d8f4c82342f51b3b830b289b5c4e682aa Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:12:26 +0200 Subject: [PATCH 011/106] use renderScrollComponent --- src/components/InvertedFlatList/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index d46cd5801605..caa49eaccf58 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -2,6 +2,7 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; +import MVCPScrollView from './MVCPScrollView/MVCPScrollView'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; import CONST from '../../CONST'; @@ -122,6 +123,10 @@ function InvertedFlatList(props) { // We need to keep batch size to one to workaround a bug in react-native-web. // This can be removed once https://github.com/Expensify/App/pull/24482 is merged. maxToRenderPerBatch={1} + + // We need to use our own scroll component to workaround a maintainVisibleContentPosition for web + // eslint-disable-next-line react/jsx-props-no-spreading + renderScrollComponent={(_props) => } /> ); } From 40226f38242c79e2cf02d535af3713ce9d402d53 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 09:16:21 +0100 Subject: [PATCH 012/106] remove redundant JSDOC and Move Nullable to utils --- src/libs/Clipboard/index.ts | 15 +++------------ src/types/utils/Nullable.ts | 3 +++ 2 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 src/types/utils/Nullable.ts diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 8c2d1be1afd5..3166cab1bed9 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -3,6 +3,7 @@ import {Clipboard as RNWClipboard} from 'react-native-web'; import CONST from '../../CONST'; import * as Browser from '../Browser'; import {SetString, Clipboard} from './types'; +import Nullable from '../../types/utils/Nullable'; type ComposerSelection = { start: number; @@ -17,13 +18,8 @@ type AnchorSelection = { focusNode: Node; }; -type Nullable = {[K in keyof T]: T[K] | null}; type OriginalSelection = ComposerSelection | Partial>; -/* - * @param {this: void} object The object to query. - */ - const canSetHtml = () => (...args: ClipboardItems) => @@ -31,8 +27,6 @@ const canSetHtml = /** * Deprecated method to write the content as HTML to clipboard. - * @param HTML representation - * @param Plain text representation */ function setHTMLSync(html: string, text: string) { const node = document.createElement('span'); @@ -84,9 +78,8 @@ function setHTMLSync(html: string, text: string) { selection?.removeAllRanges(); - if (isComposer) { - const composerSelection = originalSelection as ComposerSelection; - firstAnchorChild.setSelectionRange(composerSelection.start, composerSelection.end, composerSelection.direction); + if (isComposer && 'start' in originalSelection) { + firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction); } else { const anchorSelection = originalSelection as AnchorSelection; selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); @@ -97,8 +90,6 @@ function setHTMLSync(html: string, text: string) { /** * Writes the content as HTML if the web client supports it. - * @param HTML representation - * @param Plain text representation */ const setHtml = (html: string, text: string) => { if (!html || !text) { diff --git a/src/types/utils/Nullable.ts b/src/types/utils/Nullable.ts new file mode 100644 index 000000000000..caba46ef5b58 --- /dev/null +++ b/src/types/utils/Nullable.ts @@ -0,0 +1,3 @@ +type Nullable = {[K in keyof T]: T[K] | null}; + +export default Nullable; From dfc7be9bd67acee2ba2253ab4afa41170e065bd9 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 09:25:30 +0100 Subject: [PATCH 013/106] Update src/types/modules/react-native-web.d.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/types/modules/react-native-web.d.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts index da723e9a811d..f7db951eadad 100644 --- a/src/types/modules/react-native-web.d.ts +++ b/src/types/modules/react-native-web.d.ts @@ -1,7 +1,11 @@ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ declare module 'react-native-web' { - type SetString = (text: string) => void; + class Clipboard { + static isAvailable(): boolean; + static getString(): Promise; + static setString(text: string): boolean; + } - const Clipboard: { - setString: SetString; - }; + export {Clipboard}; } From 4bf68670f7af143b6c576cc8bf83076d6cd607f4 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 10:07:42 +0100 Subject: [PATCH 014/106] fix reviews --- src/libs/Clipboard/index.native.ts | 20 ++++++++++---------- src/libs/Clipboard/index.ts | 19 +++++++++---------- src/libs/Clipboard/types.ts | 10 +++------- src/types/utils/Nullable.ts | 3 --- 4 files changed, 22 insertions(+), 30 deletions(-) delete mode 100644 src/types/utils/Nullable.ts diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts index bc7da7ad9d3f..4a805b466d5b 100644 --- a/src/libs/Clipboard/index.native.ts +++ b/src/libs/Clipboard/index.native.ts @@ -1,19 +1,19 @@ -import RNCClipboard from '@react-native-community/clipboard'; -import {SetString, Clipboard} from './types'; +import Clipboard from '@react-native-community/clipboard'; +import {SetString, CanSetHtml, SetHtml} from './types'; /** * Sets a string on the Clipboard object via @react-native-community/clipboard */ const setString: SetString = (text) => { - RNCClipboard.setString(text); + Clipboard.setString(text); }; -const clipboard: Clipboard = { - setString, +// We don't want to set HTML on native platforms so noop them. +const canSetHtml: CanSetHtml = () => false; +const setHtml: SetHtml = () => {}; - // We don't want to set HTML on native platforms so noop them. - canSetHtml: () => false, - setHtml: () => {}, +export default { + setString, + canSetHtml, + setHtml, }; - -export default clipboard; diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 3166cab1bed9..975780f561a3 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,9 +1,8 @@ // on Web/desktop this import will be replaced with `react-native-web` -import {Clipboard as RNWClipboard} from 'react-native-web'; +import {Clipboard} from 'react-native-web'; import CONST from '../../CONST'; import * as Browser from '../Browser'; -import {SetString, Clipboard} from './types'; -import Nullable from '../../types/utils/Nullable'; +import {SetString, SetHtml, CanSetHtml} from './types'; type ComposerSelection = { start: number; @@ -18,9 +17,11 @@ type AnchorSelection = { focusNode: Node; }; -type OriginalSelection = ComposerSelection | Partial>; +type NullableObject = {[K in keyof T]: T[K] | null}; -const canSetHtml = +type OriginalSelection = ComposerSelection | Partial>; + +const canSetHtml: CanSetHtml = () => (...args: ClipboardItems) => navigator?.clipboard?.write([...args]); @@ -91,7 +92,7 @@ function setHTMLSync(html: string, text: string) { /** * Writes the content as HTML if the web client supports it. */ -const setHtml = (html: string, text: string) => { +const setHtml: SetHtml = (html: string, text: string) => { if (!html || !text) { return; } @@ -122,13 +123,11 @@ const setHtml = (html: string, text: string) => { * Sets a string on the Clipboard object via react-native-web */ const setString: SetString = (text) => { - RNWClipboard.setString(text); + Clipboard.setString(text); }; -const clipboard: Clipboard = { +export default { setString, canSetHtml, setHtml, }; - -export default clipboard; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index 92a90df85e02..1d899144a2ba 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,9 +1,5 @@ type SetString = (text: string) => void; +type SetHtml = (html: string, text: string) => void; +type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean); -type Clipboard = { - setString: SetString; - canSetHtml: () => void; - setHtml: (html: string, text: string) => void; -}; - -export type {SetString, Clipboard}; +export type {SetString, CanSetHtml, SetHtml}; diff --git a/src/types/utils/Nullable.ts b/src/types/utils/Nullable.ts deleted file mode 100644 index caba46ef5b58..000000000000 --- a/src/types/utils/Nullable.ts +++ /dev/null @@ -1,3 +0,0 @@ -type Nullable = {[K in keyof T]: T[K] | null}; - -export default Nullable; From b2a9994d3c1b8e146e2d2571a4748ce2f68f6d9e Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Tue, 10 Oct 2023 12:58:32 +0200 Subject: [PATCH 015/106] Bump onyx --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddebbe8a3832..3b267a21489f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.98", + "react-native-onyx": "1.0.104", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -44710,17 +44710,17 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.98", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.98.tgz", - "integrity": "sha512-2wJNmZVBJs2Y0p1G/es4tQZnplJR8rOyVbHv9KZaq/SXluLUnIovttf1MMhVXidDLT+gcE+u20Mck/Gpb8bY0w==", + "version": "1.0.104", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.104.tgz", + "integrity": "sha512-XUrDUZNP9vJIvcCo9NC1PIjgCckgYlTqNL/ksZV2f7EN5HOXLhHqGnp7U5Dpd5qrNcU1jOyfI9AEci2bklevCA==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.1" }, "engines": { - "node": "16.15.1", - "npm": "8.11.0" + "node": ">=16.15.1 <=18.17.1", + "npm": ">=8.11.0 <=9.6.7" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -85286,9 +85286,9 @@ } }, "react-native-onyx": { - "version": "1.0.98", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.98.tgz", - "integrity": "sha512-2wJNmZVBJs2Y0p1G/es4tQZnplJR8rOyVbHv9KZaq/SXluLUnIovttf1MMhVXidDLT+gcE+u20Mck/Gpb8bY0w==", + "version": "1.0.104", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.104.tgz", + "integrity": "sha512-XUrDUZNP9vJIvcCo9NC1PIjgCckgYlTqNL/ksZV2f7EN5HOXLhHqGnp7U5Dpd5qrNcU1jOyfI9AEci2bklevCA==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 9a3b9ed3af86..e3d480962032 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.98", + "react-native-onyx": "1.0.104", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", From 4ef562b24f78f214e7d7e3b830cbed899dc4e694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 13 Oct 2023 12:54:59 +0200 Subject: [PATCH 016/106] memoize useDebounce return value --- src/hooks/useDebounce.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.js index 8995a0443b85..3435a2699e93 100644 --- a/src/hooks/useDebounce.js +++ b/src/hooks/useDebounce.js @@ -1,4 +1,4 @@ -import {useEffect, useRef} from 'react'; +import {useCallback, useEffect, useRef} from 'react'; import lodashDebounce from 'lodash/debounce'; /** @@ -27,11 +27,13 @@ export default function useDebounce(func, wait, options) { return debouncedFn.cancel; }, [func, wait, leading, maxWait, trailing]); - return (...args) => { + const debounceCallback = useCallback((...args) => { const debouncedFn = debouncedFnRef.current; if (debouncedFn) { debouncedFn(...args); } - }; + }, []); + + return debounceCallback; } From 4a18a1ffe6d075fc3688c9f36948b8f6c2f62ba6 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 13 Oct 2023 15:04:17 +0200 Subject: [PATCH 017/106] [TS migration] Migrate 'withToggleVisibilityView.js' HOC to TypeScript --- .eslintrc.js | 2 +- src/components/withToggleVisibilityView.js | 47 --------------------- src/components/withToggleVisibilityView.tsx | 27 ++++++++++++ src/libs/getComponentDisplayName.ts | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 6 +-- 5 files changed, 32 insertions(+), 52 deletions(-) delete mode 100644 src/components/withToggleVisibilityView.js create mode 100644 src/components/withToggleVisibilityView.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 75a74ed371c4..83e9479ce0c4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -116,7 +116,7 @@ module.exports = { }, { selector: ['parameter', 'method'], - format: ['camelCase'], + format: ['camelCase', 'PascalCase'], }, ], '@typescript-eslint/ban-types': [ diff --git a/src/components/withToggleVisibilityView.js b/src/components/withToggleVisibilityView.js deleted file mode 100644 index eef5135d02b6..000000000000 --- a/src/components/withToggleVisibilityView.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import styles from '../styles/styles'; -import getComponentDisplayName from '../libs/getComponentDisplayName'; -import refPropTypes from './refPropTypes'; - -const toggleVisibilityViewPropTypes = { - /** Whether the content is visible. */ - isVisible: PropTypes.bool, -}; - -export default function (WrappedComponent) { - function WithToggleVisibilityView(props) { - return ( - - - - ); - } - - WithToggleVisibilityView.displayName = `WithToggleVisibilityView(${getComponentDisplayName(WrappedComponent)})`; - WithToggleVisibilityView.propTypes = { - forwardedRef: refPropTypes, - - /** Whether the content is visible. */ - isVisible: PropTypes.bool, - }; - WithToggleVisibilityView.defaultProps = { - forwardedRef: undefined, - isVisible: false, - }; - return React.forwardRef((props, ref) => ( - - )); -} - -export {toggleVisibilityViewPropTypes}; diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx new file mode 100644 index 000000000000..bdff40be3deb --- /dev/null +++ b/src/components/withToggleVisibilityView.tsx @@ -0,0 +1,27 @@ +import React, {ComponentType, ForwardedRef, RefAttributes} from 'react'; +import {View} from 'react-native'; +import styles from '../styles/styles'; +import getComponentDisplayName from '../libs/getComponentDisplayName'; + +type ToggleVisibilityViewPropTypes = { + /** Whether the content is visible. */ + isVisible: boolean; +}; + +export default function withToggleVisibilityView(WrappedComponent: ComponentType>): ComponentType { + function WithToggleVisibilityView(props: Omit, ref: ForwardedRef) { + return ( + + + + ); + } + + WithToggleVisibilityView.displayName = `WithToggleVisibilityView(${getComponentDisplayName(WrappedComponent)})`; + return React.forwardRef(WithToggleVisibilityView); +} diff --git a/src/libs/getComponentDisplayName.ts b/src/libs/getComponentDisplayName.ts index fd1bbcaea521..0bf52d543a84 100644 --- a/src/libs/getComponentDisplayName.ts +++ b/src/libs/getComponentDisplayName.ts @@ -1,6 +1,6 @@ import {ComponentType} from 'react'; /** Returns the display name of a component */ -export default function getComponentDisplayName(component: ComponentType): string { +export default function getComponentDisplayName(component: ComponentType): string { return component.displayName ?? component.name ?? 'Component'; } diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 2c65b5ff5d37..14d9ee3f5dfc 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -16,7 +16,7 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import TextInput from '../../../components/TextInput'; import * as ValidationUtils from '../../../libs/ValidationUtils'; import * as LoginUtils from '../../../libs/LoginUtils'; -import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../../components/withToggleVisibilityView'; +import withToggleVisibilityView from '../../../components/withToggleVisibilityView'; import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton'; import {withNetwork} from '../../../components/OnyxProvider'; import networkPropTypes from '../../../components/networkPropTypes'; @@ -66,12 +66,12 @@ const propTypes = { /** Whether or not the sign in page is being rendered in the RHP modal */ isInModal: PropTypes.bool, + isVisible: PropTypes.bool.isRequired, + ...windowDimensionsPropTypes, ...withLocalizePropTypes, - ...toggleVisibilityViewPropTypes, - ...withNavigationFocusPropTypes, }; From 659831b526af983f91d76e3393465c2e0f6cf8e6 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 13 Oct 2023 15:07:41 +0200 Subject: [PATCH 018/106] Add default value for isVisible --- src/components/withToggleVisibilityView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index bdff40be3deb..b3c8d9bfb221 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -11,12 +11,12 @@ type ToggleVisibilityViewPropTypes = { export default function withToggleVisibilityView(WrappedComponent: ComponentType>): ComponentType { function WithToggleVisibilityView(props: Omit, ref: ForwardedRef) { return ( - + ); From 547ab90addaf9bb3db18b5527ff9a2d8c8d445c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 13 Oct 2023 15:11:18 +0200 Subject: [PATCH 019/106] reuse personalDetails prop by introducing context instead of using withOnyx --- .../PersonalDetailsContext.js | 7 + .../ReportActionCompose.js | 223 +++++++++--------- .../ReportActionCompose/SuggestionMention.js | 45 +--- 3 files changed, 131 insertions(+), 144 deletions(-) create mode 100644 src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js diff --git a/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js b/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js new file mode 100644 index 000000000000..83ce73404e2d --- /dev/null +++ b/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js @@ -0,0 +1,7 @@ +import {createContext} from 'react'; + +const PersonalDetailsContext = createContext({}); + +PersonalDetailsContext.displayName = 'PersonalDetailsContext'; + +export default PersonalDetailsContext; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index dd4d51653546..831d03d2871c 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -38,6 +38,7 @@ import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; import getDraftComment from '../../../../libs/ComposerUtils/getDraftComment'; import updatePropsPaperWorklet from '../../../../libs/updatePropsPaperWorklet'; +import PersonalDetailsContext from './PersonalDetailsContext'; const propTypes = { /** A method to call when the form is submitted */ @@ -342,121 +343,123 @@ function ReportActionCompose({ }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); return ( - - - + - {shouldShowReportRecipientLocalTime && hasReportRecipient && } - + - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} + {shouldShowReportRecipientLocalTime && hasReportRecipient && } + - {({displayFileInModal}) => ( - <> - - - { - if (isAttachmentPreviewActive) { - return; - } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); - displayFileInModal(data); - }} - /> - + setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} + > + {({displayFileInModal}) => ( + <> + + + { + if (isAttachmentPreviewActive) { + return; + } + const data = lodashGet(e, ['dataTransfer', 'items', 0]); + displayFileInModal(data); + }} + /> + + )} + + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + composerRef.current.replaceSelectionWithText(...args)} + emojiPickerID={report.reportID} + /> )} - - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} + - )} - - - - {!isSmallScreenWidth && } - - - - - + + + {!isSmallScreenWidth && } + + + + + + ); } diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 84bee9c80c7f..e8dfab4eb9ac 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -1,7 +1,6 @@ -import React, {useState, useCallback, useRef, useImperativeHandle, useEffect} from 'react'; +import React, {useState, useCallback, useRef, useImperativeHandle, useEffect, useContext} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import {withOnyx} from 'react-native-onyx'; import CONST from '../../../../CONST'; import useArrowKeyFocusManager from '../../../../hooks/useArrowKeyFocusManager'; import MentionSuggestions from '../../../../components/MentionSuggestions'; @@ -9,9 +8,8 @@ import * as UserUtils from '../../../../libs/UserUtils'; import * as Expensicons from '../../../../components/Icon/Expensicons'; import * as SuggestionsUtils from '../../../../libs/SuggestionUtils'; import useLocalize from '../../../../hooks/useLocalize'; -import ONYXKEYS from '../../../../ONYXKEYS'; -import personalDetailsPropType from '../../../personalDetailsPropType'; import * as SuggestionProps from './suggestionProps'; +import PersonalDetailsContext from './PersonalDetailsContext'; /** * Check if this piece of string looks like a mention @@ -28,9 +26,6 @@ const defaultSuggestionsValues = { }; const propTypes = { - /** Personal details of all users */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** A ref to this component */ forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), @@ -38,23 +33,11 @@ const propTypes = { }; const defaultProps = { - personalDetails: {}, forwardedRef: null, }; -function SuggestionMention({ - value, - setValue, - selection, - setSelection, - isComposerFullSize, - personalDetails, - updateComment, - composerHeight, - forwardedRef, - isAutoSuggestionPickerLarge, - measureParentContainer, -}) { +function SuggestionMention({value, setValue, selection, setSelection, isComposerFullSize, updateComment, composerHeight, forwardedRef, isAutoSuggestionPickerLarge, measureParentContainer}) { + const personalDetails = useContext(PersonalDetailsContext); const {translate} = useLocalize(); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); @@ -296,16 +279,10 @@ SuggestionMention.propTypes = propTypes; SuggestionMention.defaultProps = defaultProps; SuggestionMention.displayName = 'SuggestionMention'; -export default withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, -})( - React.forwardRef((props, ref) => ( - - )), -); +export default React.forwardRef((props, ref) => ( + +)); From 956d7175a6700caf91bbe7fd86a77ea835e8bf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 13 Oct 2023 18:50:14 +0200 Subject: [PATCH 020/106] Remove draft comment dependency from OptionRowLHNData component --- .../LHNOptionsList/OptionRowLHNData.js | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 3386dbe8c8cd..b2dcffa7b9bd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -2,14 +2,12 @@ import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import PropTypes from 'prop-types'; -import React, {useEffect, useRef, useMemo} from 'react'; +import React, {useRef, useMemo} from 'react'; import {deepEqual} from 'fast-equals'; -import {withReportCommentDrafts} from '../OnyxProvider'; import SidebarUtils from '../../libs/SidebarUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import OptionRowLHN, {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './OptionRowLHN'; -import * as Report from '../../libs/actions/Report'; import * as UserUtils from '../../libs/UserUtils'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; import * as TransactionUtils from '../../libs/TransactionUtils'; @@ -70,19 +68,7 @@ const defaultProps = { * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({ - isFocused, - fullReport, - reportActions, - personalDetails, - preferredLocale, - comment, - policy, - receiptTransactions, - parentReportActions, - transaction, - ...propsToForward -}) { +function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails, preferredLocale, policy, receiptTransactions, parentReportActions, transaction, ...propsToForward}) { const reportID = propsToForward.reportID; const parentReportAction = parentReportActions[fullReport.parentReportActionID]; @@ -109,14 +95,6 @@ function OptionRowLHNData({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); - useEffect(() => { - if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { - return; - } - Report.setReportWithDraft(reportID, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( */ export default React.memo( compose( - withReportCommentDrafts({ - propName: 'comment', - transformValue: (drafts, props) => { - const draftKey = `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${props.reportID}`; - return lodashGet(drafts, draftKey, ''); - }, - }), withOnyx({ fullReport: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, From b0fc9a48e4871b72de0522194c80cc98bca0b165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 16 Oct 2023 16:53:48 +0200 Subject: [PATCH 021/106] remove unused variable --- src/components/LHNOptionsList/OptionRowLHNData.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index b2dcffa7b9bd..b00fb65e33c7 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -69,8 +69,6 @@ const defaultProps = { * re-render if the data really changed. */ function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails, preferredLocale, policy, receiptTransactions, parentReportActions, transaction, ...propsToForward}) { - const reportID = propsToForward.reportID; - const parentReportAction = parentReportActions[fullReport.parentReportActionID]; const optionItemRef = useRef(); From a53c07a5122eafd4c85dc10315ad61d04fcd3da9 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 16 Oct 2023 19:28:46 +0200 Subject: [PATCH 022/106] Fixes --- src/components/withToggleVisibilityView.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index b3c8d9bfb221..3f5524913874 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -1,22 +1,25 @@ -import React, {ComponentType, ForwardedRef, RefAttributes} from 'react'; +import React, {ComponentType, ForwardedRef, ReactElement, RefAttributes} from 'react'; import {View} from 'react-native'; import styles from '../styles/styles'; import getComponentDisplayName from '../libs/getComponentDisplayName'; +import {SetOptional} from 'type-fest'; type ToggleVisibilityViewPropTypes = { /** Whether the content is visible. */ isVisible: boolean; }; -export default function withToggleVisibilityView(WrappedComponent: ComponentType>): ComponentType { - function WithToggleVisibilityView(props: Omit, ref: ForwardedRef) { +export default function withToggleVisibilityView( + WrappedComponent: ComponentType>, +): (props: TProps & RefAttributes) => ReactElement | null { + function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional, ref: ForwardedRef) { return ( - + ); From f3dfe0a606237a0af01fd2ee1282d722923c5358 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 16 Oct 2023 19:29:13 +0200 Subject: [PATCH 023/106] Rename propTypes --- src/components/withToggleVisibilityView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index 3f5524913874..5cabdc5ffc0e 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -4,12 +4,12 @@ import styles from '../styles/styles'; import getComponentDisplayName from '../libs/getComponentDisplayName'; import {SetOptional} from 'type-fest'; -type ToggleVisibilityViewPropTypes = { +type ToggleVisibilityViewProp = { /** Whether the content is visible. */ isVisible: boolean; }; -export default function withToggleVisibilityView( +export default function withToggleVisibilityView( WrappedComponent: ComponentType>, ): (props: TProps & RefAttributes) => ReactElement | null { function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional, ref: ForwardedRef) { From d36a76282ef0aab476dc4538f8d523102844c963 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 17 Oct 2023 21:20:02 +0700 Subject: [PATCH 024/106] remove error when all workspace is deleted --- src/libs/actions/Policy.js | 87 +++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 53753e193fb1..bd234d22a826 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -66,6 +66,12 @@ Onyx.connect({ callback: (val) => (allPersonalDetails = val), }); +let reimbursementAccount; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + callback: (val) => (reimbursementAccount = val), +}); + let allRecentlyUsedCategories = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES, @@ -81,6 +87,36 @@ function updateLastAccessedWorkspace(policyID) { Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); } +/** + * Check if the user has any active free policies (aka workspaces) + * + * @param {Array} policies + * @returns {Boolean} + */ +function hasActiveFreePolicy(policies) { + const adminFreePolicies = _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); + + if (adminFreePolicies.length === 0) { + return false; + } + + if (_.some(adminFreePolicies, (policy) => !policy.pendingAction)) { + return true; + } + + if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) { + return true; + } + + if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { + return false; + } + + // If there are no add or delete pending actions the only option left is an update + // pendingAction, in which case we should return true. + return true; +} + /** * Delete the workspace * @@ -89,6 +125,10 @@ function updateLastAccessedWorkspace(policyID) { * @param {String} policyName */ function deleteWorkspace(policyID, reports, policyName) { + const filteredPolicies = _.filter(allPolicies, (policy) => policy.id !== policyID); + const hasActivePolicy = hasActiveFreePolicy(filteredPolicies); + const oldReimbursementAccount = reimbursementAccount; + const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -125,6 +165,18 @@ function deleteWorkspace(policyID, reports, policyName) { value: optimisticReportActions, }; }), + + ...(!hasActivePolicy + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + errors: null, + }, + }, + ] + : []), ]; // Restore the old report stateNum and statusNum @@ -139,6 +191,11 @@ function deleteWorkspace(policyID, reports, policyName) { oldPolicyName, }, })), + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: oldReimbursementAccount, + }, ]; // We don't need success data since the push notification will update @@ -162,36 +219,6 @@ function isAdminOfFreePolicy(policies) { return _.some(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); } -/** - * Check if the user has any active free policies (aka workspaces) - * - * @param {Array} policies - * @returns {Boolean} - */ -function hasActiveFreePolicy(policies) { - const adminFreePolicies = _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); - - if (adminFreePolicies.length === 0) { - return false; - } - - if (_.some(adminFreePolicies, (policy) => !policy.pendingAction)) { - return true; - } - - if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) { - return true; - } - - if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { - return false; - } - - // If there are no add or delete pending actions the only option left is an update - // pendingAction, in which case we should return true. - return true; -} - /** * Remove the passed members from the policy employeeList * From 991662b0c61c0ca03a24f765eff8beed4b397d76 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 18 Oct 2023 13:03:39 +0200 Subject: [PATCH 025/106] ref: moved PopoverProvider to TS --- .../{index.native.js => index.native.tsx} | 18 ++----- .../PopoverProvider/{index.js => index.tsx} | 47 +++++++------------ src/components/PopoverProvider/types.ts | 18 +++++++ 3 files changed, 40 insertions(+), 43 deletions(-) rename src/components/PopoverProvider/{index.native.js => index.native.tsx} (57%) rename src/components/PopoverProvider/{index.js => index.tsx} (65%) create mode 100644 src/components/PopoverProvider/types.ts diff --git a/src/components/PopoverProvider/index.native.js b/src/components/PopoverProvider/index.native.tsx similarity index 57% rename from src/components/PopoverProvider/index.native.js rename to src/components/PopoverProvider/index.native.tsx index f34abcb1fa62..d4ca6813f408 100644 --- a/src/components/PopoverProvider/index.native.js +++ b/src/components/PopoverProvider/index.native.tsx @@ -1,26 +1,20 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import {PopoverContextProps, PopoverContextValue} from './types'; -const propTypes = { - children: PropTypes.node.isRequired, -}; - -const defaultProps = {}; - -const PopoverContext = React.createContext({ +const PopoverContext = React.createContext({ onOpen: () => {}, - popover: {}, + popover: null, close: () => {}, isOpen: false, }); -function PopoverContextProvider(props) { +function PopoverContextProvider(props: PopoverContextProps) { return ( {}, close: () => {}, - popover: {}, + popover: null, isOpen: false, }} > @@ -29,8 +23,6 @@ function PopoverContextProvider(props) { ); } -PopoverContextProvider.defaultProps = defaultProps; -PopoverContextProvider.propTypes = propTypes; PopoverContextProvider.displayName = 'PopoverContextProvider'; export default PopoverContextProvider; diff --git a/src/components/PopoverProvider/index.js b/src/components/PopoverProvider/index.tsx similarity index 65% rename from src/components/PopoverProvider/index.js rename to src/components/PopoverProvider/index.tsx index efa230d920d5..728f9a207121 100644 --- a/src/components/PopoverProvider/index.js +++ b/src/components/PopoverProvider/index.tsx @@ -1,24 +1,18 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; -const propTypes = { - children: PropTypes.node.isRequired, -}; - -const defaultProps = {}; - -const PopoverContext = React.createContext({ +const PopoverContext = React.createContext({ onOpen: () => {}, - popover: {}, + popover: null, close: () => {}, isOpen: false, }); -function PopoverContextProvider(props) { +function PopoverContextProvider(props: PopoverContextProps) { const [isOpen, setIsOpen] = React.useState(false); - const activePopoverRef = React.useRef(null); + const activePopoverRef = React.useRef(null); - const closePopover = React.useCallback((anchorRef) => { + const closePopover = React.useCallback((anchorRef?: React.RefObject) => { if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) { return; } @@ -28,17 +22,12 @@ function PopoverContextProvider(props) { }, []); React.useEffect(() => { - const listener = (e) => { - if ( - !activePopoverRef.current || - !activePopoverRef.current.ref || - !activePopoverRef.current.ref.current || - activePopoverRef.current.ref.current.contains(e.target) || - (activePopoverRef.current.anchorRef && activePopoverRef.current.anchorRef.current && activePopoverRef.current.anchorRef.current.contains(e.target)) - ) { + const listener = (e: Event) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) { return; } - const ref = activePopoverRef.current.anchorRef; + const ref = activePopoverRef.current?.anchorRef; closePopover(ref); }; document.addEventListener('click', listener, true); @@ -48,8 +37,8 @@ function PopoverContextProvider(props) { }, [closePopover]); React.useEffect(() => { - const listener = (e) => { - if (!activePopoverRef.current || !activePopoverRef.current.ref || !activePopoverRef.current.ref.current || activePopoverRef.current.ref.current.contains(e.target)) { + const listener = (e: Event) => { + if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { return; } closePopover(); @@ -61,7 +50,7 @@ function PopoverContextProvider(props) { }, [closePopover]); React.useEffect(() => { - const listener = (e) => { + const listener = (e: KeyboardEvent) => { if (e.key !== 'Escape') { return; } @@ -87,8 +76,8 @@ function PopoverContextProvider(props) { }, [closePopover]); React.useEffect(() => { - const listener = (e) => { - if (activePopoverRef.current && activePopoverRef.current.ref && activePopoverRef.current.ref.current && activePopoverRef.current.ref.current.contains(e.target)) { + const listener = (e: Event) => { + if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { return; } @@ -101,8 +90,8 @@ function PopoverContextProvider(props) { }, [closePopover]); const onOpen = React.useCallback( - (popoverParams) => { - if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams.ref) { + (popoverParams: AnchorRef) => { + if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) { closePopover(activePopoverRef.current.anchorRef); } activePopoverRef.current = popoverParams; @@ -125,8 +114,6 @@ function PopoverContextProvider(props) { ); } -PopoverContextProvider.defaultProps = defaultProps; -PopoverContextProvider.propTypes = propTypes; PopoverContextProvider.displayName = 'PopoverContextProvider'; export default PopoverContextProvider; diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts new file mode 100644 index 000000000000..733e363bce00 --- /dev/null +++ b/src/components/PopoverProvider/types.ts @@ -0,0 +1,18 @@ +type PopoverContextProps = { + children: React.ReactNode; +}; + +type PopoverContextValue = { + onOpen?: (popoverParams: AnchorRef) => void; + popover?: AnchorRef | null; + close: (anchorRef?: React.RefObject) => void; + isOpen: boolean; +}; + +type AnchorRef = { + ref: React.RefObject; + close: (anchorRef?: React.RefObject) => void; + anchorRef?: React.RefObject; +}; + +export type {PopoverContextProps, PopoverContextValue, AnchorRef}; From 2cdea51fd549f0d1dc7ea3b4a94747bb3fc8b897 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 18 Oct 2023 15:01:00 +0200 Subject: [PATCH 026/106] ref: moved InlineErrorText to TS --- src/components/InlineErrorText.js | 31 ------------------------------ src/components/InlineErrorText.tsx | 19 ++++++++++++++++++ 2 files changed, 19 insertions(+), 31 deletions(-) delete mode 100644 src/components/InlineErrorText.js create mode 100644 src/components/InlineErrorText.tsx diff --git a/src/components/InlineErrorText.js b/src/components/InlineErrorText.js deleted file mode 100644 index ea701a3f6e8e..000000000000 --- a/src/components/InlineErrorText.js +++ /dev/null @@ -1,31 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from '../styles/styles'; -import Text from './Text'; - -const propTypes = { - /** Text to display */ - children: PropTypes.string.isRequired, - - /** Styling for inline error text */ - // eslint-disable-next-line react/forbid-prop-types - styles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - styles: [], -}; - -function InlineErrorText(props) { - if (_.isEmpty(props.children)) { - return null; - } - - return {props.children}; -} - -InlineErrorText.propTypes = propTypes; -InlineErrorText.defaultProps = defaultProps; -InlineErrorText.displayName = 'InlineErrorText'; -export default InlineErrorText; diff --git a/src/components/InlineErrorText.tsx b/src/components/InlineErrorText.tsx new file mode 100644 index 000000000000..109acfc1b893 --- /dev/null +++ b/src/components/InlineErrorText.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {TextStyle} from 'react-native'; +import styles from '../styles/styles'; +import Text from './Text'; + +type InlineErrorTextProps = { + children: React.ReactNode; + styles: TextStyle[]; +}; +function InlineErrorText(props: InlineErrorTextProps) { + if (!props.children) { + return null; + } + + return {props.children}; +} + +InlineErrorText.displayName = 'InlineErrorText'; +export default InlineErrorText; From 419e7be5f20b363254974576a8032f9be0cbb274 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 18 Oct 2023 16:23:30 +0200 Subject: [PATCH 027/106] Rename OpacityView --- src/components/{OpacityView.js => OpacityView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/{OpacityView.js => OpacityView.tsx} (100%) diff --git a/src/components/OpacityView.js b/src/components/OpacityView.tsx similarity index 100% rename from src/components/OpacityView.js rename to src/components/OpacityView.tsx From 427f99168a34b73c6a795beb499cbab9b3941b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 19 Oct 2023 10:54:33 +0200 Subject: [PATCH 028/106] wrap ComposerWithSuggestions with context instead of entire ReportActionCompose --- .../ReportActionCompose.js | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 831d03d2871c..2ca3103bad93 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -343,54 +343,54 @@ function ReportActionCompose({ }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); return ( - - + + - - } + - {shouldShowReportRecipientLocalTime && hasReportRecipient && } - setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} > - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} - > - {({displayFileInModal}) => ( - <> - + {({displayFileInModal}) => ( + <> + + - { - if (isAttachmentPreviewActive) { - return; - } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); - displayFileInModal(data); - }} - /> - - )} - - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} - /> + + { + if (isAttachmentPreviewActive) { + return; + } + const data = lodashGet(e, ['dataTransfer', 'items', 0]); + displayFileInModal(data); + }} + /> + )} - + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + composerRef.current.replaceSelectionWithText(...args)} + emojiPickerID={report.reportID} /> - - - {!isSmallScreenWidth && } - - - - - - + )} + + + + {!isSmallScreenWidth && } + + + + + ); } From 1dff6dd3fd3d1582d7eae15f3e9aa668c1c3fe2e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 20 Oct 2023 12:20:53 +0700 Subject: [PATCH 029/106] using lodash instead of underscore --- src/libs/actions/Policy.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index bd234d22a826..4417d5f00853 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import filter from 'lodash/filter'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; import lodashUnion from 'lodash/union'; @@ -125,8 +126,7 @@ function hasActiveFreePolicy(policies) { * @param {String} policyName */ function deleteWorkspace(policyID, reports, policyName) { - const filteredPolicies = _.filter(allPolicies, (policy) => policy.id !== policyID); - const hasActivePolicy = hasActiveFreePolicy(filteredPolicies); + const filteredPolicies = filter(allPolicies, (policy) => policy.id !== policyID); const oldReimbursementAccount = reimbursementAccount; const optimisticData = [ @@ -166,7 +166,7 @@ function deleteWorkspace(policyID, reports, policyName) { }; }), - ...(!hasActivePolicy + ...(!hasActiveFreePolicy(filteredPolicies) ? [ { onyxMethod: Onyx.METHOD.MERGE, @@ -194,7 +194,9 @@ function deleteWorkspace(policyID, reports, policyName) { { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: oldReimbursementAccount, + value: { + errors: lodashGet(oldReimbursementAccount, 'errors', null), + }, }, ]; From b9316bf5e22eecaf3136ef0f955436d27c8cf478 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 20 Oct 2023 09:52:35 +0200 Subject: [PATCH 030/106] Fix prettier --- src/components/withToggleVisibilityView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index 5cabdc5ffc0e..066cbae223b1 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -1,8 +1,8 @@ import React, {ComponentType, ForwardedRef, ReactElement, RefAttributes} from 'react'; +import {SetOptional} from 'type-fest'; import {View} from 'react-native'; import styles from '../styles/styles'; import getComponentDisplayName from '../libs/getComponentDisplayName'; -import {SetOptional} from 'type-fest'; type ToggleVisibilityViewProp = { /** Whether the content is visible. */ From 722043e9115259eb26153b817a114ad84ee5d165 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 20 Oct 2023 14:38:38 +0200 Subject: [PATCH 031/106] Resolve problem with styles --- src/components/OpacityView.tsx | 36 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx index daef93cdc09b..74ed2a0c92ba 100644 --- a/src/components/OpacityView.tsx +++ b/src/components/OpacityView.tsx @@ -1,69 +1,61 @@ import React from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; -import PropTypes from 'prop-types'; +import {ViewStyle} from 'react-native'; import variables from '../styles/variables'; import * as StyleUtils from '../styles/StyleUtils'; import shouldRenderOffscreen from '../libs/shouldRenderOffscreen'; -const propTypes = { +type OpacityViewProps = { /** * Should we dim the view */ - shouldDim: PropTypes.bool.isRequired, + shouldDim: boolean; /** * Content to render */ - children: PropTypes.node.isRequired, + children: React.ReactNode; /** * Array of style objects * @default [] */ // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + style: ViewStyle | ViewStyle[]; /** * The value to use for the opacity when the view is dimmed * @default 0.5 */ - dimmingValue: PropTypes.number, + dimmingValue?: number; /** Whether the view needs to be rendered offscreen (for Android only) */ - needsOffscreenAlphaCompositing: PropTypes.bool, + needsOffscreenAlphaCompositing?: boolean; }; -const defaultProps = { - style: [], - dimmingValue: variables.hoverDimValue, - needsOffscreenAlphaCompositing: false, -}; - -function OpacityView(props) { +function OpacityView({shouldDim, children, style = [], dimmingValue = variables.hoverDimValue, needsOffscreenAlphaCompositing = false}: OpacityViewProps) { const opacity = useSharedValue(1); const opacityStyle = useAnimatedStyle(() => ({ opacity: opacity.value, })); React.useEffect(() => { - if (props.shouldDim) { - opacity.value = withTiming(props.dimmingValue, {duration: 50}); + if (shouldDim) { + opacity.value = withTiming(dimmingValue, {duration: 50}); } else { opacity.value = withTiming(1, {duration: 50}); } - }, [props.shouldDim, props.dimmingValue, opacity]); + }, [shouldDim, dimmingValue, opacity]); return ( - {props.children} + {children} ); } OpacityView.displayName = 'OpacityView'; -OpacityView.propTypes = propTypes; -OpacityView.defaultProps = defaultProps; export default OpacityView; From 3cc63f83d8953d7719429b15814f23847721e777 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:11:43 +0200 Subject: [PATCH 032/106] create MVCPScrollView --- .../MVCPScrollView/MVCPScrollView.js | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js diff --git a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js new file mode 100644 index 000000000000..f0139a4ec39c --- /dev/null +++ b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js @@ -0,0 +1,128 @@ +import React, {forwardRef, useEffect, useRef} from 'react'; +import {ScrollView, StyleSheet} from 'react-native'; +import PropTypes from 'prop-types'; + + +const MVCPScrollView = forwardRef(({maintainVisibleContentPosition, horizontal, ...props}, ref) => { + const scrollViewRef = useRef(null); + const prevFirstVisibleOffset = useRef(null); + const firstVisibleView = useRef(null); + const mutationObserver = useRef(null); + + const getContentView = () => scrollViewRef.current?.childNodes[0]; + + const prepareForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null) { + return; + } + + const contentView = getContentView(); + const minIdx = maintainVisibleContentPosition.minIndexForVisible; + for (let ii = minIdx; ii < contentView.childNodes.length; ii++) { + const subview = contentView.childNodes[ii]; + const hasNewView = horizontal ? subview.offsetLeft > scrollViewRef.current.scrollLeft : subview.offsetTop > scrollViewRef.current.scrollTop; + if (hasNewView || ii === contentView.childNodes.length - 1) { + prevFirstVisibleOffset.current = horizontal ? subview.offsetLeft : subview.offsetTop; + firstVisibleView.current = subview; + break; + } + } + }; + const scrollEventListener = useRef(() => { + prepareForMaintainVisibleContentPosition(); + }); + + const adjustForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null || firstVisibleView.current == null || prevFirstVisibleOffset.current == null) { + return; + } + + const autoscrollThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold; + if (horizontal) { + const deltaX = firstVisibleView.current.offsetLeft - prevFirstVisibleOffset.current; + if (Math.abs(deltaX) > 0.5) { + const x = scrollViewRef.current.scrollLeft; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetLeft; + scrollViewRef.current.scrollTo({x: x + deltaX, animated: false}); + if (autoscrollThreshold != null && x <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({x: 0, animated: true}); + } + } + } else { + const deltaY = firstVisibleView.current.offsetTop - prevFirstVisibleOffset.current; + if (Math.abs(deltaY) > 0.5) { + const y = scrollViewRef.current.scrollTop; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetTop; + scrollViewRef.current.scrollTo({y: y + deltaY, animated: false}); + if (autoscrollThreshold != null && y <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({y: 0, animated: true}); + } + } + } + }; + + if (mutationObserver.current == null) { + mutationObserver.current = new MutationObserver(() => { + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(adjustForMaintainVisibleContentPosition); + }); + } + + const onRef = (newRef) => { + scrollViewRef.current = newRef; + if (typeof ref === 'function') { + ref(newRef); + } else { + // eslint-disable-next-line no-param-reassign + ref.current = newRef; + } + prepareForMaintainVisibleContentPosition(); + mutationObserver.current.disconnect(); + mutationObserver.current.observe(getContentView(), { + attributes: true, + childList: true, + subtree: true, + }); + newRef.removeEventListener('scroll', scrollEventListener.current); + newRef.addEventListener('scroll', scrollEventListener.current); + }; + + useEffect(() => { + const currentObserver = mutationObserver.current; + const currentScrollEventListener = scrollEventListener.current; + return () => { + currentObserver.disconnect(); + scrollViewRef.current.removeEventListener('scroll', currentScrollEventListener); + }; + }, []); + + return ( + + ); +}); + +const styles = StyleSheet.create({ + inverted: { + transform: [{ scaleY: -1 }], + }, +}); + + +MVCPScrollView.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +export default MVCPScrollView; From d82cf206706c0b6c566d54a1bb3fdfaf29693898 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:12:26 +0200 Subject: [PATCH 033/106] use renderScrollComponent --- src/components/InvertedFlatList/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index 564db6296c9b..e7f6c14f52d8 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -2,6 +2,7 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; +import MVCPScrollView from './MVCPScrollView/MVCPScrollView'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; import CONST from '../../CONST'; @@ -121,6 +122,10 @@ function InvertedFlatList(props) { // We need to keep batch size to one to workaround a bug in react-native-web. // This can be removed once https://github.com/Expensify/App/pull/24482 is merged. maxToRenderPerBatch={1} + + // We need to use our own scroll component to workaround a maintainVisibleContentPosition for web + // eslint-disable-next-line react/jsx-props-no-spreading + renderScrollComponent={(_props) => } /> ); } From 3c5d2796c9e926cb7accd3396c43cda722f8851a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 20 Oct 2023 13:08:06 -0400 Subject: [PATCH 034/106] update implementation --- src/components/FlatList/MVCPFlatList.js | 190 ++++++++++++++++++ src/components/FlatList/index.web.js | 3 + .../MVCPScrollView/MVCPScrollView.js | 128 ------------ src/components/InvertedFlatList/index.js | 5 - 4 files changed, 193 insertions(+), 133 deletions(-) create mode 100644 src/components/FlatList/MVCPFlatList.js create mode 100644 src/components/FlatList/index.web.js delete mode 100644 src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js new file mode 100644 index 000000000000..357db1535e6b --- /dev/null +++ b/src/components/FlatList/MVCPFlatList.js @@ -0,0 +1,190 @@ +/* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ + +import React from 'react'; +import {FlatList} from 'react-native'; + +function mergeRefs(...args) { + return function forwardRef(node) { + args.forEach((ref) => { + if (ref == null) { + return; + } + if (typeof ref === 'function') { + ref(node); + return; + } + if (typeof ref === 'object') { + // eslint-disable-next-line no-param-reassign + ref.current = node; + return; + } + console.error(`mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`); + }); + }; +} + +function useMergeRefs(...args) { + return React.useMemo( + () => mergeRefs(...args), + // eslint-disable-next-line + [...args], + ); +} + +const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, inverted, onScroll, ...props}, forwardedRef) => { + const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {}; + const scrollRef = React.useRef(null); + const prevFirstVisibleOffsetRef = React.useRef(null); + const firstVisibleViewRef = React.useRef(null); + const mutationObserverRef = React.useRef(null); + const lastScrollOffsetRef = React.useRef(0); + + const getScrollOffset = React.useCallback(() => { + if (scrollRef.current == null) { + return 0; + } + return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + }, [horizontal]); + + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + + const scrollToOffset = React.useCallback( + (offset, animated) => { + const behavior = animated ? 'smooth' : 'instant'; + scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + }, + [horizontal], + ); + + const prepareForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const contentView = getContentView(); + if (contentView == null) { + return; + } + + const scrollOffset = getScrollOffset(); + + for (let i = mvcpMinIndexForVisible; i < contentView.childNodes.length; i++) { + const subview = contentView.childNodes[i]; + const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; + if (subviewOffset > scrollOffset || i === contentView.childNodes.length - 1) { + prevFirstVisibleOffsetRef.current = subviewOffset; + firstVisibleViewRef.current = subview; + break; + } + } + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); + + const adjustForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const firstVisibleView = firstVisibleViewRef.current; + const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; + if (firstVisibleView == null || prevFirstVisibleOffset == null) { + return; + } + + const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; + const delta = firstVisibleViewOffset - prevFirstVisibleOffset; + if (Math.abs(delta) > 0.5) { + const scrollOffset = getScrollOffset(); + prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; + scrollToOffset(scrollOffset + delta, false); + if (mvcpAutoscrollToTopThreshold != null && scrollOffset <= mvcpAutoscrollToTopThreshold) { + scrollToOffset(0, true); + } + } + }, [getScrollOffset, scrollToOffset, mvcpMinIndexForVisible, mvcpAutoscrollToTopThreshold, horizontal]); + + const setupMutationObserver = React.useCallback(() => { + const contentView = getContentView(); + if (contentView == null) { + return; + } + + mutationObserverRef.current?.disconnect(); + + const mutationObserver = new MutationObserver(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(() => { + adjustForMaintainVisibleContentPosition(); + }); + }); + mutationObserver.observe(contentView, { + attributes: true, + childList: true, + subtree: true, + }); + + mutationObserverRef.current = mutationObserver; + }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + + React.useEffect(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); + + const setMergedRef = useMergeRefs(scrollRef, forwardedRef); + + const onRef = React.useCallback( + (newRef) => { + // Make sure to only call refs and re-attach listeners if the node changed. + if (newRef == null || newRef === scrollRef.current) { + return; + } + + setMergedRef(newRef); + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, + [prepareForMaintainVisibleContentPosition, setMergedRef, setupMutationObserver], + ); + + React.useEffect(() => { + const mutationObserver = mutationObserverRef.current; + return () => { + mutationObserver?.disconnect(); + }; + }, []); + + const onScrollInternal = React.useCallback( + (ev) => { + lastScrollOffsetRef.current = getScrollOffset(); + + prepareForMaintainVisibleContentPosition(); + + onScroll?.(ev); + }, + [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + ); + + return ( + + ); +}); + +export default MVCPFlatList; diff --git a/src/components/FlatList/index.web.js b/src/components/FlatList/index.web.js new file mode 100644 index 000000000000..7299776db9bc --- /dev/null +++ b/src/components/FlatList/index.web.js @@ -0,0 +1,3 @@ +import MVCPFlatList from './MVCPFlatList'; + +export default MVCPFlatList; diff --git a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js deleted file mode 100644 index f0139a4ec39c..000000000000 --- a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js +++ /dev/null @@ -1,128 +0,0 @@ -import React, {forwardRef, useEffect, useRef} from 'react'; -import {ScrollView, StyleSheet} from 'react-native'; -import PropTypes from 'prop-types'; - - -const MVCPScrollView = forwardRef(({maintainVisibleContentPosition, horizontal, ...props}, ref) => { - const scrollViewRef = useRef(null); - const prevFirstVisibleOffset = useRef(null); - const firstVisibleView = useRef(null); - const mutationObserver = useRef(null); - - const getContentView = () => scrollViewRef.current?.childNodes[0]; - - const prepareForMaintainVisibleContentPosition = () => { - if (maintainVisibleContentPosition == null || scrollViewRef.current == null) { - return; - } - - const contentView = getContentView(); - const minIdx = maintainVisibleContentPosition.minIndexForVisible; - for (let ii = minIdx; ii < contentView.childNodes.length; ii++) { - const subview = contentView.childNodes[ii]; - const hasNewView = horizontal ? subview.offsetLeft > scrollViewRef.current.scrollLeft : subview.offsetTop > scrollViewRef.current.scrollTop; - if (hasNewView || ii === contentView.childNodes.length - 1) { - prevFirstVisibleOffset.current = horizontal ? subview.offsetLeft : subview.offsetTop; - firstVisibleView.current = subview; - break; - } - } - }; - const scrollEventListener = useRef(() => { - prepareForMaintainVisibleContentPosition(); - }); - - const adjustForMaintainVisibleContentPosition = () => { - if (maintainVisibleContentPosition == null || scrollViewRef.current == null || firstVisibleView.current == null || prevFirstVisibleOffset.current == null) { - return; - } - - const autoscrollThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold; - if (horizontal) { - const deltaX = firstVisibleView.current.offsetLeft - prevFirstVisibleOffset.current; - if (Math.abs(deltaX) > 0.5) { - const x = scrollViewRef.current.scrollLeft; - prevFirstVisibleOffset.current = firstVisibleView.current.offsetLeft; - scrollViewRef.current.scrollTo({x: x + deltaX, animated: false}); - if (autoscrollThreshold != null && x <= autoscrollThreshold) { - scrollViewRef.current.scrollTo({x: 0, animated: true}); - } - } - } else { - const deltaY = firstVisibleView.current.offsetTop - prevFirstVisibleOffset.current; - if (Math.abs(deltaY) > 0.5) { - const y = scrollViewRef.current.scrollTop; - prevFirstVisibleOffset.current = firstVisibleView.current.offsetTop; - scrollViewRef.current.scrollTo({y: y + deltaY, animated: false}); - if (autoscrollThreshold != null && y <= autoscrollThreshold) { - scrollViewRef.current.scrollTo({y: 0, animated: true}); - } - } - } - }; - - if (mutationObserver.current == null) { - mutationObserver.current = new MutationObserver(() => { - // This needs to execute after scroll events are dispatched, but - // in the same tick to avoid flickering. rAF provides the right timing. - requestAnimationFrame(adjustForMaintainVisibleContentPosition); - }); - } - - const onRef = (newRef) => { - scrollViewRef.current = newRef; - if (typeof ref === 'function') { - ref(newRef); - } else { - // eslint-disable-next-line no-param-reassign - ref.current = newRef; - } - prepareForMaintainVisibleContentPosition(); - mutationObserver.current.disconnect(); - mutationObserver.current.observe(getContentView(), { - attributes: true, - childList: true, - subtree: true, - }); - newRef.removeEventListener('scroll', scrollEventListener.current); - newRef.addEventListener('scroll', scrollEventListener.current); - }; - - useEffect(() => { - const currentObserver = mutationObserver.current; - const currentScrollEventListener = scrollEventListener.current; - return () => { - currentObserver.disconnect(); - scrollViewRef.current.removeEventListener('scroll', currentScrollEventListener); - }; - }, []); - - return ( - - ); -}); - -const styles = StyleSheet.create({ - inverted: { - transform: [{ scaleY: -1 }], - }, -}); - - -MVCPScrollView.propTypes = { - maintainVisibleContentPosition: PropTypes.shape({ - minIndexForVisible: PropTypes.number.isRequired, - autoscrollToTopThreshold: PropTypes.number, - }), - horizontal: PropTypes.bool, -}; - -export default MVCPScrollView; diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index e7f6c14f52d8..564db6296c9b 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -2,7 +2,6 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; -import MVCPScrollView from './MVCPScrollView/MVCPScrollView'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; import CONST from '../../CONST'; @@ -122,10 +121,6 @@ function InvertedFlatList(props) { // We need to keep batch size to one to workaround a bug in react-native-web. // This can be removed once https://github.com/Expensify/App/pull/24482 is merged. maxToRenderPerBatch={1} - - // We need to use our own scroll component to workaround a maintainVisibleContentPosition for web - // eslint-disable-next-line react/jsx-props-no-spreading - renderScrollComponent={(_props) => } /> ); } From 4a67723154ac01bc139a5a06e5c0ea229a6dae07 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sun, 22 Oct 2023 05:53:01 +0530 Subject: [PATCH 035/106] fix: append whitespace after emoji --- src/libs/ComposerUtils/index.ts | 5 +- .../ComposerWithSuggestions.js | 59 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 5e2a42fc65dd..987615f17695 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -32,7 +32,10 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo */ function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + if(str1.length===0||str2.length===0){ + return 0; + } + while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index e194d0870885..a1950ad2a96e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -116,6 +116,7 @@ function ComposerWithSuggestions({ return draft; }); const commentRef = useRef(value); + const lastTextRef = useRef(value); const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; @@ -194,6 +195,31 @@ function ComposerWithSuggestions({ RNTextInputReset.resetKeyboardInput(findNodeHandle(textInputRef.current)); }, [textInputRef]); + const findNewlyAddedChars = useCallback( + (prevText, newText) => { + const isTextReplace = selection.end - selection.start > 0; + const commonSuffixLength =ComposerUtils.getCommonSuffixLength(prevText, newText); + let startIndex = -1; + let endIndex = -1; + let i = 0; + + while (i < newText.length && prevText.charAt(i) === newText.charAt(i) && selection.start > i) { + i++; + } + + if (i < newText.length) { + startIndex = i; + // if text is getting pasted over find length of common suffix and subtract it from new text length + endIndex = isTextReplace ? newText.length-commonSuffixLength : i + (newText.length - prevText.length); + } + + return {startIndex, endIndex, diff: newText.substring(startIndex, endIndex)}; + }, + [selection.end, selection.start], + ); + + const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`; + const debouncedSaveReportComment = useMemo( () => _.debounce((selectedReportID, newComment) => { @@ -211,7 +237,13 @@ function ComposerWithSuggestions({ const updateComment = useCallback( (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); + const isEmojiInserted = diff.length && endIndex > startIndex && EmojiUtils.containsOnlyEmojis(diff); + const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( + isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, + preferredSkinTone, + preferredLocale, + ); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); @@ -255,16 +287,7 @@ function ComposerWithSuggestions({ debouncedBroadcastUserIsTyping(reportID); } }, - [ - debouncedUpdateFrequentlyUsedEmojis, - preferredLocale, - preferredSkinTone, - reportID, - setIsCommentEmpty, - suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, - debouncedSaveReportComment, - ], + [raiseIsScrollLikelyLayoutTriggered, findNewlyAddedChars, preferredSkinTone, preferredLocale, setIsCommentEmpty, debouncedUpdateFrequentlyUsedEmojis, suggestionsRef, reportID, debouncedSaveReportComment], ); /** @@ -313,14 +336,8 @@ function ComposerWithSuggestions({ * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { - const updatedText = shouldAddTrailSpace ? `${text} ` : text; - const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; - updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); - setSelection((prevSelection) => ({ - start: prevSelection.start + text.length + selectionSpaceLength, - end: prevSelection.start + text.length + selectionSpaceLength, - })); + (text) => { + updateComment(ComposerUtils.insertText(commentRef.current, selection, text)); }, [selection, updateComment], ); @@ -508,6 +525,10 @@ function ComposerWithSuggestions({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + lastTextRef.current = value; + }, [value]); + useImperativeHandle( forwardedRef, () => ({ From 6db20146c0d0d11ff350255c0ba8e6e02c5020b3 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sun, 22 Oct 2023 06:18:40 +0530 Subject: [PATCH 036/106] fix: prevent infinite loop --- src/libs/ComposerUtils/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 987615f17695..bf22fcb04a49 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -35,7 +35,8 @@ function getCommonSuffixLength(str1: string, str2: string): number { if(str1.length===0||str2.length===0){ return 0; } - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + const minLen = Math.min(str1.length, str2.length); + while (i>minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; From a8bd00576b2efad25380615217eb9904203dcda4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sun, 22 Oct 2023 06:24:05 +0530 Subject: [PATCH 037/106] fix: clean lint --- src/libs/ComposerUtils/index.ts | 4 ++-- .../ComposerWithSuggestions.js | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index bf22fcb04a49..3167ce851e60 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -32,11 +32,11 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo */ function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; - if(str1.length===0||str2.length===0){ + if (str1.length === 0 || str2.length === 0) { return 0; } const minLen = Math.min(str1.length, str2.length); - while (i>minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + while (i < minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index a1950ad2a96e..d8b3bc8f820a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -198,7 +198,7 @@ function ComposerWithSuggestions({ const findNewlyAddedChars = useCallback( (prevText, newText) => { const isTextReplace = selection.end - selection.start > 0; - const commonSuffixLength =ComposerUtils.getCommonSuffixLength(prevText, newText); + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); let startIndex = -1; let endIndex = -1; let i = 0; @@ -210,7 +210,7 @@ function ComposerWithSuggestions({ if (i < newText.length) { startIndex = i; // if text is getting pasted over find length of common suffix and subtract it from new text length - endIndex = isTextReplace ? newText.length-commonSuffixLength : i + (newText.length - prevText.length); + endIndex = isTextReplace ? newText.length - commonSuffixLength : i + (newText.length - prevText.length); } return {startIndex, endIndex, diff: newText.substring(startIndex, endIndex)}; @@ -287,7 +287,17 @@ function ComposerWithSuggestions({ debouncedBroadcastUserIsTyping(reportID); } }, - [raiseIsScrollLikelyLayoutTriggered, findNewlyAddedChars, preferredSkinTone, preferredLocale, setIsCommentEmpty, debouncedUpdateFrequentlyUsedEmojis, suggestionsRef, reportID, debouncedSaveReportComment], + [ + raiseIsScrollLikelyLayoutTriggered, + findNewlyAddedChars, + preferredSkinTone, + preferredLocale, + setIsCommentEmpty, + debouncedUpdateFrequentlyUsedEmojis, + suggestionsRef, + reportID, + debouncedSaveReportComment, + ], ); /** From 69272012a274d2c350b165573ca2d24bc29f85cd Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 23 Oct 2023 11:14:29 +0200 Subject: [PATCH 038/106] WIP testing maintainVisibleContentPosition --- src/components/InvertedFlatList/index.js | 325 ++++++++++++++--------- 1 file changed, 194 insertions(+), 131 deletions(-) diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index 564db6296c9b..f2414d577222 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -1,140 +1,203 @@ -import React, {forwardRef, useEffect, useRef} from 'react'; +import React, {forwardRef, useEffect, useRef, useState} from 'react'; import PropTypes from 'prop-types'; -import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; +import {DeviceEventEmitter, StyleSheet, View, Text, Button} from 'react-native'; import _ from 'underscore'; import BaseInvertedFlatList from './BaseInvertedFlatList'; -import styles from '../../styles/styles'; +// import styles from '../../styles/styles'; import CONST from '../../CONST'; +import FlatList from '../FlatList/index.web'; + +// const propTypes = { +// /** Passed via forwardRef so we can access the FlatList ref */ +// innerRef: PropTypes.shape({ +// current: PropTypes.instanceOf(FlatList), +// }).isRequired, + +// /** Any additional styles to apply */ +// // eslint-disable-next-line react/forbid-prop-types +// contentContainerStyle: PropTypes.any, + +// /** Same as for FlatList */ +// onScroll: PropTypes.func, +// }; + +// // This is adapted from https://codesandbox.io/s/react-native-dsyse +// // It's a HACK alert since FlatList has inverted scrolling on web +// function InvertedFlatList(props) { +// const {innerRef, contentContainerStyle} = props; +// const listRef = React.createRef(); + +// const lastScrollEvent = useRef(null); +// const scrollEndTimeout = useRef(null); +// const updateInProgress = useRef(false); +// const eventHandler = useRef(null); + +// useEffect(() => { +// if (!_.isFunction(innerRef)) { +// // eslint-disable-next-line no-param-reassign +// innerRef.current = listRef.current; +// } else { +// innerRef(listRef); +// } + +// return () => { +// if (scrollEndTimeout.current) { +// clearTimeout(scrollEndTimeout.current); +// } + +// if (eventHandler.current) { +// eventHandler.current.remove(); +// } +// }; +// }, [innerRef, listRef]); + +// /** +// * Emits when the scrolling is in progress. Also, +// * invokes the onScroll callback function from props. +// * +// * @param {Event} event - The onScroll event from the FlatList +// */ +// const onScroll = (event) => { +// props.onScroll(event); + +// if (!updateInProgress.current) { +// updateInProgress.current = true; +// eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, true); +// } +// }; + +// /** +// * Emits when the scrolling has ended. +// */ +// const onScrollEnd = () => { +// eventHandler.current = 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 +// * +// * @param {Event} event - The onScroll event from the FlatList +// */ +// const handleScroll = (event) => { +// 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; +// }; + +// return ( +// +// ); +// } + +// InvertedFlatList.propTypes = propTypes; +// InvertedFlatList.defaultProps = { +// contentContainerStyle: {}, +// onScroll: () => {}, +// }; + +// export default forwardRef((props, ref) => ( +// +// )); + + + +function ReportScreen() { + const [data, setData] = useState(generatePosts(15)); + + const loadNewerChats = () => { + const lastId = data[0].id - 1; + setData([...generatePosts(5, lastId - 4), ...data]); + }; + + const renderItem = ({ item }) => ; + const keyExtractor = (item) => item.id.toString(); + + return ( + <> + +