From 4b6941f37c22be08d55e8fbf12be68228187215e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 7 Aug 2023 08:30:09 +0200 Subject: [PATCH 001/258] Toggle label correcty --- src/components/TextInput/BaseTextInput.js | 48 +++++++++++------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 68c09e3a7f82..d97e4df1c5bc 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -22,8 +22,8 @@ import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; import withLocalize from '../withLocalize'; function BaseTextInput(props) { - const inputValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); + const initialValue = props.value || props.defaultValue || ''; + const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); @@ -168,34 +168,28 @@ function BaseTextInput(props) { [props.autoGrowHeight, props.multiline], ); - useEffect(() => { - // Handle side effects when the value gets changed programatically from the outside - - // In some cases, When the value prop is empty, it is not properly updated on the TextInput due to its uncontrolled nature, thus manually clearing the TextInput. - if (inputValue === '') { - input.current.clear(); - } + // The ref is needed when the component is uncontrolled and we don't have a value prop + const hasValueRef = useRef(initialValue.length > 0); + const inputValue = props.value || ''; + const hasValue = inputValue.length > 0 || hasValueRef.current; - if (inputValue) { + // Activate or deactivate the label when either focus changes, or for controlled + // components when the value prop changes: + useEffect(() => { + if (hasValue || isFocused) { activateLabel(); + } else if (!hasValue && !isFocused) { + deactivateLabel(); } - }, [activateLabel, inputValue]); - - // We capture whether the input has a value or not in a ref. - // It gets updated when the text gets changed. - const hasValueRef = useRef(inputValue.length > 0); + }, [activateLabel, deactivateLabel, hasValue, isFocused]); - // Activate or deactivate the label when the focus changes: + // When the value prop gets cleared externally, we need to deactivate the label: useEffect(() => { - // We can't use inputValue here directly, as it might contain - // the defaultValue, which doesn't get updated when the text changes. - // We can't use props.value either, as it might be undefined. - if (hasValueRef.current || isFocused) { - activateLabel(); - } else if (!hasValueRef.current && !isFocused) { - deactivateLabel(); + if (props.value === undefined || props.value > 0) { + return; } - }, [activateLabel, deactivateLabel, inputValue, isFocused]); + hasValueRef.current = false; + }, [props.value]); /** * Set Value & activateLabel @@ -209,9 +203,13 @@ function BaseTextInput(props) { } Str.result(props.onChangeText, value); + if (value && value.length > 0) { hasValueRef.current = true; - activateLabel(); + // When the componment is uncontrolled, we need to manually activate the label: + if (props.value === undefined) { + activateLabel(); + } } else { hasValueRef.current = false; } From 464b510a7a315c4b5d99c92038a69acd4dd6e0ec Mon Sep 17 00:00:00 2001 From: Nam Le Date: Mon, 21 Aug 2023 20:19:30 +0700 Subject: [PATCH 002/258] fix undo message after delete --- src/pages/home/report/ReportActionItem.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 323329590f3d..0dd322c68489 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -160,6 +160,10 @@ function ReportActionItem(props) { }, [isDraftEmpty]); useEffect(() => { + if (ReportActionsUtils.isDeletedAction(props.action)) { + Report.saveReportActionDraft(props.report.reportID, props.action, '') + } + if (!Permissions.canUseLinkPreviews()) { return; } From ea3c194f9cc4e6125514981e16aede350d0941b1 Mon Sep 17 00:00:00 2001 From: Nam Le Date: Mon, 21 Aug 2023 20:23:25 +0700 Subject: [PATCH 003/258] fix lint --- src/pages/home/report/ReportActionItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 0dd322c68489..821e4ac4586d 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -161,7 +161,7 @@ function ReportActionItem(props) { useEffect(() => { if (ReportActionsUtils.isDeletedAction(props.action)) { - Report.saveReportActionDraft(props.report.reportID, props.action, '') + Report.saveReportActionDraft(props.report.reportID, props.action, ''); } if (!Permissions.canUseLinkPreviews()) { From f286fff1134d35005923f00d01ab1f687eb798cb Mon Sep 17 00:00:00 2001 From: Nam Le Date: Tue, 22 Aug 2023 15:31:53 +0700 Subject: [PATCH 004/258] separate hook --- src/pages/home/report/ReportActionItem.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 821e4ac4586d..aed080fa760d 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -160,10 +160,6 @@ function ReportActionItem(props) { }, [isDraftEmpty]); useEffect(() => { - if (ReportActionsUtils.isDeletedAction(props.action)) { - Report.saveReportActionDraft(props.report.reportID, props.action, ''); - } - if (!Permissions.canUseLinkPreviews()) { return; } @@ -177,6 +173,13 @@ function ReportActionItem(props) { Report.expandURLPreview(props.report.reportID, props.action.reportActionID); }, [props.action, props.report.reportID]); + useEffect(() => { + if (isDraftEmpty || !ReportActionsUtils.isDeletedAction(props.action)) { + return; + } + Report.saveReportActionDraft(props.report.reportID, props.action, ''); + }, [isDraftEmpty, props.action, props.report.reportID]); + // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator // Removed messages should not be shown anyway and should not need this flow const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], ''); From c2d31263da67ad7ec7b81f4f6baedfb6d9f6a9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 23 Aug 2023 14:53:34 +0200 Subject: [PATCH 005/258] reapply changes --- src/components/TextInput/BaseTextInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index d97e4df1c5bc..e3ff3715aaef 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -176,9 +176,9 @@ function BaseTextInput(props) { // Activate or deactivate the label when either focus changes, or for controlled // components when the value prop changes: useEffect(() => { - if (hasValue || isFocused) { + if (hasValue || isFocused || isInputAutoFilled(input.current)) { activateLabel(); - } else if (!hasValue && !isFocused) { + } else { deactivateLabel(); } }, [activateLabel, deactivateLabel, hasValue, isFocused]); From 620831f5db18df6c6e5612e0779c0d81e73ac82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 23 Aug 2023 15:09:46 +0200 Subject: [PATCH 006/258] use isEmpty --- src/components/TextInput/BaseTextInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index e3ff3715aaef..acdb23296691 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -183,9 +183,9 @@ function BaseTextInput(props) { } }, [activateLabel, deactivateLabel, hasValue, isFocused]); - // When the value prop gets cleared externally, we need to deactivate the label: + // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { - if (props.value === undefined || props.value > 0) { + if (!_.isEmpty(props.value)) { return; } hasValueRef.current = false; From eb3796d7f652698bcc26454f0625be01b066bb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 24 Aug 2023 11:31:50 +0200 Subject: [PATCH 007/258] fix --- src/components/TextInput/BaseTextInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index acdb23296691..16e391f12215 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -185,7 +185,8 @@ function BaseTextInput(props) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { - if (!_.isEmpty(props.value)) { + // Return early when component uncontrolled, or we still have a value + if (props.value === undefined || !_.isEmpty(props.value)) { return; } hasValueRef.current = false; From 2adafc53b75157909f1656addbe2fa1af882daad Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 31 Aug 2023 08:36:32 +0100 Subject: [PATCH 008/258] migrate index.js class to function component --- .../HTMLRenderers/PreRenderer/index.js | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index efc9e432cba8..c1f33dfd56c0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -1,29 +1,14 @@ -import React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import _ from 'underscore'; + +import ControlSelection from '../../../../libs/ControlSelection'; +import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import withLocalize from '../../../withLocalize'; 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); - } - - componentDidMount() { - if (!this.ref) { - return; - } - this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode); - } - - 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 @@ -31,38 +16,51 @@ class PreRenderer extends React.Component { * @param {WheelEvent} event Wheel event * @returns {Boolean} true if user is scrolling vertically */ - isScrollingVertically(event) { + function 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; } + const debouncedIsScrollingVertically = useCallback((event) => _.debounce(isScrollingVertically(event), 100, true), []); + /** * 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 horizontalOverflow = node.scrollWidth > node.offsetWidth; - const isScrollingVertically = this.debouncedIsScrollingVertically(event); - if (event.currentTarget === node && horizontalOverflow && !isScrollingVertically) { - node.scrollLeft += event.deltaX; - event.preventDefault(); - event.stopPropagation(); + const scrollNode = useCallback( + (event) => { + const node = scrollViewRef.getScrollableNode(); + const horizontalOverflow = node.scrollWidth > node.offsetWidth; + if (event.currentTarget === node && horizontalOverflow && !debouncedIsScrollingVertically(event)) { + node.scrollLeft += event.deltaX; + event.preventDefault(); + event.stopPropagation(); + } + }, + [debouncedIsScrollingVertically], + ); + + useEffect(() => { + if (!scrollViewRef) { + return; } - } + scrollViewRef.getScrollableNode().addEventListener('wheel', scrollNode); - render() { - return ( - (this.ref = el)} - onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - /> - ); - } + return () => { + scrollViewRef.getScrollableNode().removeEventListener('wheel', scrollNode); + }; + }, [scrollNode]); + + return ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + /> + ); } PreRenderer.propTypes = htmlRendererPropTypes; From 494cf78840449b3c59a1eccfbd5fb1227284190e Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 31 Aug 2023 12:27:44 +0100 Subject: [PATCH 009/258] use arrow function and useCallback --- .../HTMLRenderers/PreRenderer/index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index c1f33dfd56c0..8899e414440a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -3,11 +3,10 @@ import _ from 'underscore'; import ControlSelection from '../../../../libs/ControlSelection'; import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; -import withLocalize from '../../../withLocalize'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; -function PreRenderer(props) { +function PreRenderer({key, style, tnode, TDefaultRenderer}) { const scrollViewRef = useRef(); /** @@ -16,11 +15,11 @@ function PreRenderer(props) { * @param {WheelEvent} event Wheel event * @returns {Boolean} true if user is scrolling vertically */ - function isScrollingVertically(event) { + const isScrollingVertically = useCallback((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; - } + }, []); const debouncedIsScrollingVertically = useCallback((event) => _.debounce(isScrollingVertically(event), 100, true), []); @@ -54,8 +53,10 @@ function PreRenderer(props) { return ( DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} @@ -64,5 +65,6 @@ function PreRenderer(props) { } PreRenderer.propTypes = htmlRendererPropTypes; +PreRenderer.displayName = 'PreRenderer'; -export default withLocalize(PreRenderer); +export default PreRenderer; From 4d748f1a0d3b34647ea6339b253624653625dd9c Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Fri, 1 Sep 2023 01:23:30 +0100 Subject: [PATCH 010/258] use current in ref and revert props destructure --- .../HTMLRenderers/PreRenderer/index.js | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index 8899e414440a..d394fb474e51 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -6,22 +6,24 @@ import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; -function PreRenderer({key, style, tnode, TDefaultRenderer}) { +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 */ - const isScrollingVertically = useCallback((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; - }, []); + const isScrollingVertically = useCallback( + (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, + [], + ); - const debouncedIsScrollingVertically = useCallback((event) => _.debounce(isScrollingVertically(event), 100, true), []); + const debouncedIsScrollingVertically = useCallback((event) => _.debounce(isScrollingVertically(event), 100, true), [isScrollingVertically]); /** * Manually scrolls the code block if code block horizontal scrollable, then prevents the event from being passed up to the parent. @@ -29,7 +31,7 @@ function PreRenderer({key, style, tnode, TDefaultRenderer}) { */ const scrollNode = useCallback( (event) => { - const node = scrollViewRef.getScrollableNode(); + const node = scrollViewRef.current.getScrollableNode(); const horizontalOverflow = node.scrollWidth > node.offsetWidth; if (event.currentTarget === node && horizontalOverflow && !debouncedIsScrollingVertically(event)) { node.scrollLeft += event.deltaX; @@ -41,25 +43,24 @@ function PreRenderer({key, style, tnode, TDefaultRenderer}) { ); useEffect(() => { - if (!scrollViewRef) { + if (!scrollViewRef.current) { return; } - scrollViewRef.getScrollableNode().addEventListener('wheel', scrollNode); + scrollViewRef.current.getScrollableNode().addEventListener('wheel', scrollNode); + const eventListenerRefValue = scrollViewRef.current; return () => { - scrollViewRef.getScrollableNode().removeEventListener('wheel', scrollNode); + eventListenerRefValue.getScrollableNode().removeEventListener('wheel', scrollNode); }; }, [scrollNode]); return ( DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} + onPressOut={ControlSelection.unblock()} /> ); } From 84f0b5292dfc6f3a28c6f5c09a955d7cabadee31 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 4 Sep 2023 09:51:56 +0100 Subject: [PATCH 011/258] Update src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js Co-authored-by: Michael (Mykhailo) Kravchenko --- .../HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index d394fb474e51..3abf2a782146 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -60,7 +60,7 @@ function PreRenderer(props) { {...props} ref={scrollViewRef} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={ControlSelection.unblock()} + onPressOut={ControlSelection.unblock} /> ); } From 6c22ffc7695d0f5743c64d080c9061e1c9074024 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 5 Sep 2023 00:20:54 +0200 Subject: [PATCH 012/258] big delay fix --- src/libs/HttpUtils.js | 21 +++++++++++++++++++++ src/libs/ReportUtils.js | 10 ++++++++-- src/libs/actions/Report.js | 11 +++++++---- src/types/onyx/Network.ts | 3 +++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 5a8185a03038..4c51403382f2 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -22,6 +22,13 @@ Onyx.connect({ // We use the AbortController API to terminate pending request in `cancelPendingRequests` let cancellationController = new AbortController(); +/** + * API comamnds we need to calculate skew, + * Regex to get API command from the command + */ +const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; +const regex = /[?&]command=([^&]+)/; + /** * Send an HTTP request, and attempt to resolve the json response. * If there is a network error, we'll set the application offline. @@ -33,12 +40,26 @@ let cancellationController = new AbortController(); * @returns {Promise} */ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { + const startTime = new Date(); + return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal: canCancel ? cancellationController.signal : undefined, method, body, }) + .then((response) => { + const match = url.match(regex)[1]; + if (addSkewList.includes(match)) { + const serverTime = new Date(response.headers.get('Date')); + const endTime = new Date(); + const latency = (endTime.valueOf() - startTime.valueOf()) / 2; + const skew = serverTime.valueOf() - startTime.valueOf() + latency; + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.merge(ONYXKEYS.NETWORK, {timeSkew: skew}); + } + return response; + }) .then((response) => { // Test mode where all requests will succeed in the server, but fail to return a response if (shouldFailAllRequests || shouldForceOffline) { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 893145a8e5fa..34cb842ec065 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -77,6 +77,12 @@ Onyx.connect({ callback: (val) => (loginList = val), }); +let networkTimeSkew; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (val) => (networkTimeSkew = lodashGet(val, 'timeSkew', 0)), +}); + function getChatType(report) { return report ? report.chatType : ''; } @@ -1775,7 +1781,7 @@ function buildOptimisticAddCommentReportAction(text, file) { // Remove HTML from text when applying optimistic offline comment const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment); - + const timestamp = new Date().valueOf() + networkTimeSkew; return { commentText, reportAction: { @@ -1791,7 +1797,7 @@ function buildOptimisticAddCommentReportAction(text, file) { ], automatic: false, avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), - created: DateUtils.getDBTime(), + created: DateUtils.getDBTime(timestamp), message: [ { translationKey: isAttachment ? CONST.TRANSLATION_KEYS.ATTACHMENT : '', diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 8b898a6aaaea..01af5976ea4e 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -75,9 +75,13 @@ Onyx.connect({ }); let isNetworkOffline = false; +let networkTimeSkew; Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: (val) => (isNetworkOffline = lodashGet(val, 'isOffline', false)), + callback: (val) => { + isNetworkOffline = lodashGet(val, 'isOffline', false); + networkTimeSkew = lodashGet(val, 'timeSkew', 0); + }, }); let allPersonalDetails; @@ -251,9 +255,8 @@ function addActions(reportID, text = '', file) { // Always prefer the file as the last action over text const lastAction = attachmentAction || reportCommentAction; - - const currentTime = DateUtils.getDBTime(); - + const timestamp = new Date().valueOf() + networkTimeSkew; + const currentTime = DateUtils.getDBTime(timestamp); const lastCommentText = ReportUtils.formatReportLastMessageText(lastAction.message[0].text); const optimisticReport = { diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts index 5af4c1170c3f..32b084bbf2f7 100644 --- a/src/types/onyx/Network.ts +++ b/src/types/onyx/Network.ts @@ -7,6 +7,9 @@ type Network = { /** Whether we should fail all network requests */ shouldFailAllRequests?: boolean; + + /** Skew between the client and server clocks */ + timeSkew?: number; }; export default Network; From ac8703b1d86d566f83dde52404da689350178ebd Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 5 Sep 2023 21:33:14 +0200 Subject: [PATCH 013/258] fixes --- src/libs/HttpUtils.js | 17 ++++++++++------- src/libs/ReportUtils.js | 2 +- src/libs/actions/Report.js | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 4c51403382f2..e6500b04419c 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -23,10 +23,13 @@ Onyx.connect({ let cancellationController = new AbortController(); /** - * API comamnds we need to calculate skew, - * Regex to get API command from the command + * The API commands that require the skew calculation */ const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; + +/** + * Regex to get API command from the command + */ const regex = /[?&]command=([^&]+)/; /** @@ -40,7 +43,7 @@ const regex = /[?&]command=([^&]+)/; * @returns {Promise} */ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { - const startTime = new Date(); + const startTime = new Date().valueOf(); return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once @@ -51,10 +54,10 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) .then((response) => { const match = url.match(regex)[1]; if (addSkewList.includes(match)) { - const serverTime = new Date(response.headers.get('Date')); - const endTime = new Date(); - const latency = (endTime.valueOf() - startTime.valueOf()) / 2; - const skew = serverTime.valueOf() - startTime.valueOf() + latency; + const serverTime = new Date(response.headers.get('Date')).valueOf(); + const endTime = new Date().valueOf(); + const latency = (endTime - startTime) / 2; + const skew = serverTime - startTime + latency; // eslint-disable-next-line rulesdir/prefer-actions-set-data Onyx.merge(ONYXKEYS.NETWORK, {timeSkew: skew}); } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 34cb842ec065..f0936fe084b4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -77,7 +77,7 @@ Onyx.connect({ callback: (val) => (loginList = val), }); -let networkTimeSkew; +let networkTimeSkew = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (val) => (networkTimeSkew = lodashGet(val, 'timeSkew', 0)), diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 01af5976ea4e..98f4c03ed132 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -75,7 +75,7 @@ Onyx.connect({ }); let isNetworkOffline = false; -let networkTimeSkew; +let networkTimeSkew = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (val) => { From 58b55b5351fab3a2d45770b504f3f5890b089c48 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 5 Sep 2023 22:24:06 +0200 Subject: [PATCH 014/258] adjustments, add getDBTimeWithSkew function --- src/libs/DateUtils.js | 17 +++++++++++++++++ src/libs/ReportUtils.js | 9 +-------- src/libs/actions/Report.js | 5 +---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index b33a1b1b2a73..8c0d8fb7f72c 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -47,6 +47,12 @@ Onyx.connect({ }, }); +let networkTimeSkew = 0; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (val) => (networkTimeSkew = lodashGet(val, 'timeSkew', 0)), +}); + /** * Gets the locale string and setting default locale for date-fns * @@ -267,6 +273,16 @@ function getDBTime(timestamp = '') { return datetime.toISOString().replace('T', ' ').replace('Z', ''); } +/** + * Returns the current time in milliseconds in the format expected by the database + * + * @param {String|Number} [timestamp] + * @returns {String} + */ +function getDBTimeWithSkew() { + return getDBTime(new Date().valueOf() + networkTimeSkew); +} + /** * @param {String} dateTime * @param {Number} milliseconds @@ -341,6 +357,7 @@ const DateUtils = { setTimezoneUpdated, getMicroseconds, getDBTime, + getDBTimeWithSkew, subtractMillisecondsFromDateTime, getDateStringFromISOTimestamp, getStatusUntilDate, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f0936fe084b4..0914b8bd71a9 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -77,12 +77,6 @@ Onyx.connect({ callback: (val) => (loginList = val), }); -let networkTimeSkew = 0; -Onyx.connect({ - key: ONYXKEYS.NETWORK, - callback: (val) => (networkTimeSkew = lodashGet(val, 'timeSkew', 0)), -}); - function getChatType(report) { return report ? report.chatType : ''; } @@ -1781,7 +1775,6 @@ function buildOptimisticAddCommentReportAction(text, file) { // Remove HTML from text when applying optimistic offline comment const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment); - const timestamp = new Date().valueOf() + networkTimeSkew; return { commentText, reportAction: { @@ -1797,7 +1790,7 @@ function buildOptimisticAddCommentReportAction(text, file) { ], automatic: false, avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), - created: DateUtils.getDBTime(timestamp), + created: DateUtils.getDBTimeWithSkew(), message: [ { translationKey: isAttachment ? CONST.TRANSLATION_KEYS.ATTACHMENT : '', diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 98f4c03ed132..a403c689c872 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -75,12 +75,10 @@ Onyx.connect({ }); let isNetworkOffline = false; -let networkTimeSkew = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (val) => { isNetworkOffline = lodashGet(val, 'isOffline', false); - networkTimeSkew = lodashGet(val, 'timeSkew', 0); }, }); @@ -255,8 +253,7 @@ function addActions(reportID, text = '', file) { // Always prefer the file as the last action over text const lastAction = attachmentAction || reportCommentAction; - const timestamp = new Date().valueOf() + networkTimeSkew; - const currentTime = DateUtils.getDBTime(timestamp); + const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(lastAction.message[0].text); const optimisticReport = { From 6f40a7c08dd607ca4a58487aeddd3e3959ae05b7 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 5 Sep 2023 22:26:39 +0200 Subject: [PATCH 015/258] adjustments --- src/libs/DateUtils.js | 3 +-- src/libs/ReportUtils.js | 1 + src/libs/actions/Report.js | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 8c0d8fb7f72c..8a32ad67b897 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -274,9 +274,8 @@ function getDBTime(timestamp = '') { } /** - * Returns the current time in milliseconds in the format expected by the database + * Returns the current time plus skew in milliseconds in the format expected by the database * - * @param {String|Number} [timestamp] * @returns {String} */ function getDBTimeWithSkew() { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 0914b8bd71a9..16a0a092138a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1775,6 +1775,7 @@ function buildOptimisticAddCommentReportAction(text, file) { // Remove HTML from text when applying optimistic offline comment const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment); + return { commentText, reportAction: { diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index a403c689c872..cb3436f07d33 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -253,7 +253,9 @@ function addActions(reportID, text = '', file) { // Always prefer the file as the last action over text const lastAction = attachmentAction || reportCommentAction; + const currentTime = DateUtils.getDBTimeWithSkew(); + const lastCommentText = ReportUtils.formatReportLastMessageText(lastAction.message[0].text); const optimisticReport = { From cb999869528220bccb3d4eb85b81cc4c23101fa8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Sep 2023 11:50:51 +0200 Subject: [PATCH 016/258] create withRoute --- src/components/withRoute.js | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/components/withRoute.js diff --git a/src/components/withRoute.js b/src/components/withRoute.js new file mode 100644 index 000000000000..a50aa79f45c4 --- /dev/null +++ b/src/components/withRoute.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {useRoute} from '@react-navigation/native'; +import getComponentDisplayName from '../libs/getComponentDisplayName'; +import refPropTypes from './refPropTypes'; + +const withRoutePropTypes = { + route: PropTypes.object.isRequired, +}; + +export default function withRoute(WrappedComponent) { + function WithRoute(props) { + const route = useRoute(); + return ( + + ); + } + + WithRoute.displayName = `withRoute(${getComponentDisplayName(WrappedComponent)})`; + WithRoute.propTypes = { + forwardedRef: refPropTypes, + }; + WithRoute.defaultProps = { + forwardedRef: () => {}, + }; + return React.forwardRef((props, ref) => ( + + )); +} + +export {withRoutePropTypes}; From 9ae049cbce074a0cd29c442719bb9a068a3a7f91 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Sep 2023 11:53:24 +0200 Subject: [PATCH 017/258] highlight background color If needed --- src/pages/home/report/ReportActionItem.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 8425f78a3a10..dc78d8b777e1 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; -import React, {useState, useRef, useEffect, memo, useCallback, useContext} from 'react'; +import React, {useState, useRef, useEffect, memo, useCallback, useContext, useMemo} from 'react'; import {InteractionManager, View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; @@ -67,6 +67,8 @@ import * as BankAccounts from '../../../libs/actions/BankAccounts'; import usePrevious from '../../../hooks/usePrevious'; import ReportScreenContext from '../ReportScreenContext'; import Permissions from '../../../libs/Permissions'; +import themeColors from '../../../styles/themes/default'; +import withRoute from '../../../components/withRoute'; const propTypes = { ...windowDimensionsPropTypes, @@ -135,6 +137,11 @@ function ReportActionItem(props) { const prevDraftMessage = usePrevious(props.draftMessage); const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); + const reportActionID = lodashGet(props.route, 'params.reportActionID'); + + const highlightedBackgroundColorIfNeeded = useMemo(() => { + return reportActionID === props.action.reportActionID ? {backgroundColor: themeColors.highlightBG} : {}; + }, [reportActionID]); useEffect( () => () => { @@ -557,7 +564,7 @@ function ReportActionItem(props) { > {(hovered) => ( - + {props.shouldDisplayNewMarker && } `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, }, }), + withRoute, )( memo( ReportActionItem, From 8d34f8799b90c11c9cfd9b4d89c088c2c162c80c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Sep 2023 14:05:27 +0200 Subject: [PATCH 018/258] lint --- src/pages/home/report/ReportActionItem.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index dc78d8b777e1..7a04ed55801d 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -139,9 +139,7 @@ function ReportActionItem(props) { const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); const reportActionID = lodashGet(props.route, 'params.reportActionID'); - const highlightedBackgroundColorIfNeeded = useMemo(() => { - return reportActionID === props.action.reportActionID ? {backgroundColor: themeColors.highlightBG} : {}; - }, [reportActionID]); + const highlightedBackgroundColorIfNeeded = useMemo(() => (reportActionID === props.action.reportActionID ? {backgroundColor: themeColors.highlightBG} : {}), [reportActionID]); useEffect( () => () => { From 58dbaf317cc5bf7154200d006ce73f62cfcbc405 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Sep 2023 14:16:05 +0200 Subject: [PATCH 019/258] add dependency --- src/pages/home/report/ReportActionItem.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 7a04ed55801d..077568f26e6f 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -139,7 +139,10 @@ function ReportActionItem(props) { const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); const reportActionID = lodashGet(props.route, 'params.reportActionID'); - const highlightedBackgroundColorIfNeeded = useMemo(() => (reportActionID === props.action.reportActionID ? {backgroundColor: themeColors.highlightBG} : {}), [reportActionID]); + const highlightedBackgroundColorIfNeeded = useMemo( + () => (reportActionID === props.action.reportActionID ? {backgroundColor: themeColors.highlightBG} : {}), + [reportActionID, props.action.reportActionID], + ); useEffect( () => () => { From 67c0c0216db32eee81c1f684e308e3479801f0de Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Sep 2023 09:37:06 +0200 Subject: [PATCH 020/258] ref: move openReportActionComposeViewWhenClosingMessageEdit to TS --- .../{index.native.js => index.native.ts} | 0 .../{index.js => index.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/libs/openReportActionComposeViewWhenClosingMessageEdit/{index.native.js => index.native.ts} (100%) rename src/libs/openReportActionComposeViewWhenClosingMessageEdit/{index.js => index.ts} (100%) diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.js b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts similarity index 100% rename from src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.js rename to src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.js b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts similarity index 100% rename from src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.js rename to src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts From a787c98985d18df7c53d5b127b62649c143dd545 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Sep 2023 12:27:14 +0200 Subject: [PATCH 021/258] fix: use TS standart files when there are platform specific files --- .../index.native.ts | 5 ++++- .../index.ts | 5 ++++- .../types.ts | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts index 488769741715..b2e5149bf4f4 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts @@ -1,9 +1,12 @@ import {Keyboard} from 'react-native'; import * as Composer from '../actions/Composer'; +import OpenReportActionComposeViewWhenClosingVMessageEdit from './types'; -export default () => { +const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingVMessageEdit = () => { const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { Composer.setShouldShowComposeInput(true); keyboardDidHideListener.remove(); }); }; + +export default openReportActionComposeViewWhenClosingVMessageEdit; diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts index 4f3e8c5de2c8..67c88252b9d5 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts @@ -1,5 +1,8 @@ import * as Composer from '../actions/Composer'; +import OpenReportActionComposeViewWhenClosingVMessageEdit from './types'; -export default () => { +const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingVMessageEdit = () => { Composer.setShouldShowComposeInput(true); }; + +export default openReportActionComposeViewWhenClosingVMessageEdit; diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts new file mode 100644 index 000000000000..cd128a982ff3 --- /dev/null +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts @@ -0,0 +1,3 @@ +type OpenReportActionComposeViewWhenClosingVMessageEdit = () => void; + +export default OpenReportActionComposeViewWhenClosingVMessageEdit; From acbfcb32441d003f747013ce6f46cbbe985392c5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Sep 2023 12:47:48 +0200 Subject: [PATCH 022/258] fix: fixed typo --- .../index.native.ts | 4 ++-- .../index.ts | 4 ++-- .../types.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts index b2e5149bf4f4..6d4d82bbc5bd 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts @@ -1,8 +1,8 @@ import {Keyboard} from 'react-native'; import * as Composer from '../actions/Composer'; -import OpenReportActionComposeViewWhenClosingVMessageEdit from './types'; +import OpenReportActionComposeViewWhenClosingMessageEdit from './types'; -const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingVMessageEdit = () => { +const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingMessageEdit = () => { const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { Composer.setShouldShowComposeInput(true); keyboardDidHideListener.remove(); diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts index 67c88252b9d5..d5b98cfbc8c3 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts @@ -1,7 +1,7 @@ import * as Composer from '../actions/Composer'; -import OpenReportActionComposeViewWhenClosingVMessageEdit from './types'; +import OpenReportActionComposeViewWhenClosingMessageEdit from './types'; -const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingVMessageEdit = () => { +const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingMessageEdit = () => { Composer.setShouldShowComposeInput(true); }; diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts index cd128a982ff3..4a7803c10508 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/types.ts @@ -1,3 +1,3 @@ -type OpenReportActionComposeViewWhenClosingVMessageEdit = () => void; +type OpenReportActionComposeViewWhenClosingMessageEdit = () => void; -export default OpenReportActionComposeViewWhenClosingVMessageEdit; +export default OpenReportActionComposeViewWhenClosingMessageEdit; From 0d3d8ce87eee6ac44877c2ffaacb5362790ebe43 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Sep 2023 12:50:32 +0200 Subject: [PATCH 023/258] fix: typo --- .../index.native.ts | 4 ++-- .../index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts index 6d4d82bbc5bd..c77f6bd1ad68 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.ts @@ -2,11 +2,11 @@ import {Keyboard} from 'react-native'; import * as Composer from '../actions/Composer'; import OpenReportActionComposeViewWhenClosingMessageEdit from './types'; -const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingMessageEdit = () => { +const openReportActionComposeViewWhenClosingMessageEdit: OpenReportActionComposeViewWhenClosingMessageEdit = () => { const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { Composer.setShouldShowComposeInput(true); keyboardDidHideListener.remove(); }); }; -export default openReportActionComposeViewWhenClosingVMessageEdit; +export default openReportActionComposeViewWhenClosingMessageEdit; diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts index d5b98cfbc8c3..a9960f9a325c 100644 --- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts +++ b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.ts @@ -1,8 +1,8 @@ import * as Composer from '../actions/Composer'; import OpenReportActionComposeViewWhenClosingMessageEdit from './types'; -const openReportActionComposeViewWhenClosingVMessageEdit: OpenReportActionComposeViewWhenClosingMessageEdit = () => { +const openReportActionComposeViewWhenClosingMessageEdit: OpenReportActionComposeViewWhenClosingMessageEdit = () => { Composer.setShouldShowComposeInput(true); }; -export default openReportActionComposeViewWhenClosingVMessageEdit; +export default openReportActionComposeViewWhenClosingMessageEdit; From beca6d038768470d45ced7a91f45e28f50845ae9 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 13 Sep 2023 14:26:16 +0100 Subject: [PATCH 024/258] chore: wip --- src/libs/OptionsListUtils.js | 13 +++---- .../MoneyRequestParticipantsSelector.js | 36 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 7629a1acc0a6..cf519fb75d14 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1212,32 +1212,33 @@ function getShareDestinationOptions( * Format personalDetails or userToInvite to be shown in the list * * @param {Object} member - personalDetails or userToInvite - * @param {Boolean} isSelected - whether the item is selected + * @param {Object} config - keys to overwrite the default values * @returns {Object} */ -function formatMemberForList(member, isSelected) { +function formatMemberForList(member, config = {}) { if (!member) { return undefined; } - const avatarSource = lodashGet(member, 'participantsList[0].avatar', '') || lodashGet(member, 'avatar', ''); + const avatarSource = lodashGet(member, 'participantsList[0].avatar', '') || lodashGet(member, 'icons[0].source', '') || lodashGet(member, 'avatar', ''); const accountID = lodashGet(member, 'accountID', ''); return { text: lodashGet(member, 'text', '') || lodashGet(member, 'displayName', ''), alternateText: lodashGet(member, 'alternateText', '') || lodashGet(member, 'login', ''), keyForList: lodashGet(member, 'keyForList', '') || String(accountID), - isSelected, + isSelected: false, isDisabled: false, accountID, login: lodashGet(member, 'login', ''), rightElement: null, avatar: { source: UserUtils.getAvatar(avatarSource, accountID), - name: lodashGet(member, 'participantsList[0].login', '') || lodashGet(member, 'displayName', ''), - type: 'avatar', + name: lodashGet(member, 'participantsList[0].login', '') || lodashGet(member, 'icons[0].name', '') || lodashGet(member, 'displayName', ''), + type: CONST.ICON_TYPE_AVATAR, }, pendingAction: lodashGet(member, 'pendingAction'), + ...config, }; } diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index ef1d6565f595..390eb0187e90 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -4,13 +4,13 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import * as OptionsListUtils from '../../../../libs/OptionsListUtils'; import * as ReportUtils from '../../../../libs/ReportUtils'; -import OptionsSelector from '../../../../components/OptionsSelector'; import ONYXKEYS from '../../../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; import compose from '../../../../libs/compose'; import CONST from '../../../../CONST'; import personalDetailsPropType from '../../../personalDetailsPropType'; import reportPropTypes from '../../../reportPropTypes'; +import SelectionList from '../../../../components/SelectionList'; const propTypes = { /** Beta features list */ @@ -28,9 +28,6 @@ const propTypes = { /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - /** padding bottom style of safe area */ - safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /** The type of IOU report, i.e. bill, request, send */ iouType: PropTypes.string.isRequired, @@ -41,7 +38,6 @@ const propTypes = { }; const defaultProps = { - safeAreaPaddingBottomStyle: {}, personalDetails: {}, reports: {}, betas: [], @@ -57,10 +53,14 @@ class MoneyRequestParticipantsSelector extends Component { const {recentReports, personalDetails, userToInvite} = this.getRequestOptions(); + const formattedRecentReports = _.map(recentReports, (report) => OptionsListUtils.formatMemberForList(report)); + const formattedPersonalDetails = _.map(personalDetails, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); + const formattedUserToInvite = OptionsListUtils.formatMemberForList(userToInvite); + this.state = { - recentReports, - personalDetails, - userToInvite, + recentReports: formattedRecentReports, + personalDetails: formattedPersonalDetails, + userToInvite: formattedUserToInvite, searchTerm: '', }; } @@ -133,11 +133,15 @@ class MoneyRequestParticipantsSelector extends Component { updateOptionsWithSearchTerm(searchTerm = '') { const {recentReports, personalDetails, userToInvite} = this.getRequestOptions(searchTerm); + const formattedRecentReports = _.map(recentReports, (report) => OptionsListUtils.formatMemberForList(report)); + const formattedPersonalDetails = _.map(personalDetails, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); + const formattedUserToInvite = OptionsListUtils.formatMemberForList(userToInvite); + this.setState({ + recentReports: formattedRecentReports, + personalDetails: formattedPersonalDetails, + userToInvite: formattedUserToInvite, searchTerm, - recentReports, - userToInvite, - personalDetails, }); } @@ -160,16 +164,14 @@ class MoneyRequestParticipantsSelector extends Component { const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); return ( - ); } From 37f66e44d67d94049f6a7715db92001f5f150297 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 13 Sep 2023 20:12:49 +0100 Subject: [PATCH 025/258] refactor(selection-list): create BaseListItem --- src/components/SelectionList/BaseListItem.js | 95 +++++++++++++++++++ .../SelectionList/BaseSelectionList.js | 40 +++++--- src/components/SelectionList/RadioListItem.js | 46 ++------- src/components/SelectionList/UserListItem.js | 82 ++++------------ 4 files changed, 147 insertions(+), 116 deletions(-) create mode 100644 src/components/SelectionList/BaseListItem.js diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js new file mode 100644 index 000000000000..c6a3a948eab9 --- /dev/null +++ b/src/components/SelectionList/BaseListItem.js @@ -0,0 +1,95 @@ +import React from 'react'; +import {View} from 'react-native'; +import PressableWithFeedback from '../Pressable/PressableWithFeedback'; +import styles from '../../styles/styles'; +import Icon from '../Icon'; +import * as Expensicons from '../Icon/Expensicons'; +import themeColors from '../../styles/themes/default'; +import {radioListItemPropTypes} from './selectionListPropTypes'; +import * as StyleUtils from '../../styles/StyleUtils'; +import UserListItem from './UserListItem'; +import RadioListItem from './RadioListItem'; +import OfflineWithFeedback from '../OfflineWithFeedback'; + +function BaseListItem({item, isFocused = false, isDisabled = false, showTooltip, canSelectMultiple, onSelectRow, onDismissError = () => {}}) { + const isUserItem = Boolean(item.avatar); + const ListItem = isUserItem ? UserListItem : RadioListItem; + + return ( + onDismissError(item)} + pendingAction={item.pendingAction} + errors={item.errors} + errorRowStyles={styles.ph5} + > + onSelectRow(item)} + disabled={isDisabled} + accessibilityLabel={item.text} + accessibilityRole="button" + hoverDimmingValue={1} + hoverStyle={styles.hoveredComponentBG} + focusStyle={styles.hoveredComponentBG} + > + + {canSelectMultiple && ( + + {item.isSelected && ( + + )} + + )} + + + + {!canSelectMultiple && item.isSelected && ( + + + + + + )} + + + + ); +} + +BaseListItem.displayName = 'BaseListItem'; +BaseListItem.propTypes = radioListItemPropTypes; + +export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 2cf0d2d72695..2ed0ecca1962 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -24,6 +24,7 @@ import useLocalize from '../../hooks/useLocalize'; import Log from '../../libs/Log'; import OptionsListSkeletonView from '../OptionsListSkeletonView'; import useActiveElement from '../../hooks/useActiveElement'; +import BaseListItem from './BaseListItem'; const propTypes = { ...keyboardStatePropTypes, @@ -243,31 +244,42 @@ function BaseSelectionList({ const renderItem = ({item, index, section}) => { const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); - const isDisabled = section.isDisabled; + const isDisabled = section.isDisabled || item.isDisabled; const isFocused = !isDisabled && focusedIndex === normalizedIndex; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = normalizedIndex < 10; - if (canSelectMultiple) { - return ( - selectRow(item, index)} - onDismissError={onDismissError} - showTooltip={showTooltip} - /> - ); - } - return ( - selectRow(item, index)} /> ); + + // if (canSelectMultiple) { + // return ( + // selectRow(item, index)} + // onDismissError={onDismissError} + // showTooltip={showTooltip} + // /> + // ); + // } + // + // return ( + // selectRow(item, index)} + // /> + // ); }; /** Focuses the text input when the component comes into focus and after any navigation animations finish. */ diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.js index 92e3e84b66c8..83d0fc922f08 100644 --- a/src/components/SelectionList/RadioListItem.js +++ b/src/components/SelectionList/RadioListItem.js @@ -1,50 +1,18 @@ import React from 'react'; import {View} from 'react-native'; -import PressableWithFeedback from '../Pressable/PressableWithFeedback'; import styles from '../../styles/styles'; import Text from '../Text'; -import Icon from '../Icon'; -import * as Expensicons from '../Icon/Expensicons'; -import themeColors from '../../styles/themes/default'; import {radioListItemPropTypes} from './selectionListPropTypes'; -function RadioListItem({item, isFocused = false, isDisabled = false, onSelectRow}) { +function RadioListItem({item, isFocused = false}) { return ( - onSelectRow(item)} - disabled={isDisabled} - accessibilityLabel={item.text} - accessibilityRole="button" - hoverDimmingValue={1} - hoverStyle={styles.hoveredComponentBG} - focusStyle={styles.hoveredComponentBG} - > - - - - {item.text} - + + {item.text} - {Boolean(item.alternateText) && ( - {item.alternateText} - )} - - - {item.isSelected && ( - - - - - - )} - - + {Boolean(item.alternateText) && ( + {item.alternateText} + )} + ); } diff --git a/src/components/SelectionList/UserListItem.js b/src/components/SelectionList/UserListItem.js index dd90fc750510..64c2dae2a78c 100644 --- a/src/components/SelectionList/UserListItem.js +++ b/src/components/SelectionList/UserListItem.js @@ -1,24 +1,15 @@ import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import lodashGet from 'lodash/get'; -import PressableWithFeedback from '../Pressable/PressableWithFeedback'; import styles from '../../styles/styles'; import Text from '../Text'; import {userListItemPropTypes} from './selectionListPropTypes'; import Avatar from '../Avatar'; -import OfflineWithFeedback from '../OfflineWithFeedback'; import CONST from '../../CONST'; -import * as StyleUtils from '../../styles/StyleUtils'; -import Icon from '../Icon'; -import * as Expensicons from '../Icon/Expensicons'; -import themeColors from '../../styles/themes/default'; import Tooltip from '../Tooltip'; import UserDetailsTooltip from '../UserDetailsTooltip'; -function UserListItem({item, isFocused = false, showTooltip, onSelectRow, onDismissError = () => {}}) { - const hasError = !_.isEmpty(item.errors); - +function UserListItem({item, isFocused = false, showTooltip}) { const avatar = ( onDismissError(item)} - pendingAction={item.pendingAction} - errors={item.errors} - errorRowStyles={styles.ph5} - > - onSelectRow(item)} - disabled={item.isDisabled} - accessibilityLabel={item.text} - accessibilityRole="checkbox" - accessibilityState={{checked: item.isSelected}} - hoverDimmingValue={1} - hoverStyle={styles.hoveredComponentBG} - focusStyle={styles.hoveredComponentBG} - > - - {item.isSelected && ( - - )} - - {Boolean(item.avatar) && - (showTooltip ? ( - - {avatar} - - ) : ( - avatar - ))} - - {showTooltip ? {text} : text} - {Boolean(item.alternateText) && (showTooltip ? {alternateText} : alternateText)} - - {Boolean(item.rightElement) && item.rightElement} - - + <> + {Boolean(item.avatar) && + (showTooltip ? ( + + {avatar} + + ) : ( + avatar + ))} + + {showTooltip ? {text} : text} + {Boolean(item.alternateText) && (showTooltip ? {alternateText} : alternateText)} + + {Boolean(item.rightElement) && item.rightElement} + ); } From ebe4249e5063f4565c10afea8c0547b42b59cf2c Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Fri, 15 Sep 2023 18:07:11 +0200 Subject: [PATCH 026/258] fix tests --- src/libs/HttpUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index e6500b04419c..9d00f1e31869 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -53,7 +53,7 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) }) .then((response) => { const match = url.match(regex)[1]; - if (addSkewList.includes(match)) { + if (addSkewList.includes(match) && response.headers) { const serverTime = new Date(response.headers.get('Date')).valueOf(); const endTime = new Date().valueOf(); const latency = (endTime - startTime) / 2; From 9d3cb2ea931660ba0ea92657adcb42105dd0e463 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Mon, 18 Sep 2023 15:31:01 +0100 Subject: [PATCH 027/258] feat(selection-list): request money --- .../MoneyRequestConfirmationList.js | 55 ++-- src/components/SelectionList/BaseListItem.js | 11 +- .../SelectionList/BaseSelectionList.js | 255 +++++++++--------- src/components/SelectionList/UserListItem.js | 27 +- .../SelectionList/selectionListPropTypes.js | 71 +++-- src/components/SubscriptAvatar.js | 91 ++++--- src/libs/OptionsListUtils.js | 118 ++++---- .../iou/steps/MoneyRequestConfirmPage.js | 33 ++- .../MoneyRequestParticipantsSelector.js | 18 +- src/pages/workspace/WorkspaceMembersPage.js | 12 +- 10 files changed, 369 insertions(+), 322 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index da98d324681e..c4cee1e23b48 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -9,7 +9,6 @@ import styles from '../styles/styles'; import * as ReportUtils from '../libs/ReportUtils'; import * as OptionsListUtils from '../libs/OptionsListUtils'; import Permissions from '../libs/Permissions'; -import OptionsSelector from './OptionsSelector'; import ONYXKEYS from '../ONYXKEYS'; import compose from '../libs/compose'; import CONST from '../CONST'; @@ -29,12 +28,14 @@ import themeColors from '../styles/themes/default'; import Image from './Image'; import useLocalize from '../hooks/useLocalize'; import * as ReceiptUtils from '../libs/ReceiptUtils'; +import * as LocalePhoneNumber from '../libs/LocalePhoneNumber'; import categoryPropTypes from './categoryPropTypes'; import tagPropTypes from './tagPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; import transactionPropTypes from './transactionPropTypes'; import DistanceRequestUtils from '../libs/DistanceRequestUtils'; import * as IOU from '../libs/actions/IOU'; +import SelectionList from './SelectionList'; const propTypes = { /** Callback to inform parent modal of success */ @@ -221,7 +222,10 @@ function MoneyRequestConfirmationList(props) { const getParticipantsWithAmount = useCallback( (participantsList) => { const iouAmount = IOUUtils.calculateAmount(participantsList.length, props.iouAmount, props.iouCurrencyCode); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode)); + + return _.map(participantsList, (participant) => + OptionsListUtils.formatMemberForList(participant, {descriptiveText: CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode)}), + ); }, [props.iouAmount, props.iouCurrencyCode], ); @@ -251,10 +255,12 @@ function MoneyRequestConfirmationList(props) { const optionSelectorSections = useMemo(() => { const sections = []; + + // TODO: REVIEW const unselectedParticipants = _.filter(props.selectedParticipants, (participant) => !participant.selected); if (props.hasMultipleParticipants) { const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants); + let formattedParticipantsList = _.map(_.union(formattedSelectedParticipants, unselectedParticipants), OptionsListUtils.formatMemberForList); if (!canModifyParticipants) { formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({ @@ -264,10 +270,10 @@ function MoneyRequestConfirmationList(props) { } const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, props.iouCurrencyCode, true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, - CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode), - ); + const formattedPayeeOption = OptionsListUtils.formatMemberForList(payeePersonalDetails, { + descriptiveText: CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode), + login: LocalePhoneNumber.formatPhoneNumber(payeePersonalDetails.login), + }); sections.push( { @@ -291,7 +297,8 @@ function MoneyRequestConfirmationList(props) { })); sections.push({ title: translate('common.to'), - data: formattedSelectedParticipants, + // Here we know we only have 1 participant, so it's safe to get the first index + data: [OptionsListUtils.formatMemberForList(formattedSelectedParticipants[0])], shouldShow: true, indexOffset: 0, }); @@ -310,13 +317,6 @@ function MoneyRequestConfirmationList(props) { canModifyParticipants, ]); - const selectedOptions = useMemo(() => { - if (!props.hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]); - useEffect(() => { if (!props.isDistanceRequest) { return; @@ -378,6 +378,7 @@ function MoneyRequestConfirmationList(props) { [selectedParticipants, onSendMoney, onConfirm, props.iouType], ); + // TODO: See footer const footerContent = useMemo(() => { if (props.isReadOnly) { return; @@ -387,6 +388,8 @@ function MoneyRequestConfirmationList(props) { const shouldDisableButton = selectedParticipants.length === 0; const recipient = props.selectedParticipants[0] || {}; + console.log('shouldShowSettlementButton', shouldShowSettlementButton); + return shouldShowSettlementButton ? ( {props.isDistanceRequest && ( @@ -532,7 +529,7 @@ function MoneyRequestConfirmationList(props) { )} )} - + ); } diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js index 49035fc56a5b..e5ccdd70bbe1 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.js @@ -1,18 +1,19 @@ import React from 'react'; import {View} from 'react-native'; +import lodashGet from 'lodash/get'; import PressableWithFeedback from '../Pressable/PressableWithFeedback'; import styles from '../../styles/styles'; import Icon from '../Icon'; import * as Expensicons from '../Icon/Expensicons'; import themeColors from '../../styles/themes/default'; -import {radioListItemPropTypes} from './selectionListPropTypes'; +import {baseListItemPropTypes} from './selectionListPropTypes'; import * as StyleUtils from '../../styles/StyleUtils'; import UserListItem from './UserListItem'; import RadioListItem from './RadioListItem'; import OfflineWithFeedback from '../OfflineWithFeedback'; function BaseListItem({item, isFocused = false, isDisabled = false, showTooltip, canSelectMultiple, onSelectRow, onDismissError = () => {}}) { - const isUserItem = Boolean(item.avatar); + const isUserItem = lodashGet(item, 'icons.length', 0) > 0; const ListItem = isUserItem ? UserListItem : RadioListItem; return ( @@ -29,7 +30,6 @@ function BaseListItem({item, isFocused = false, isDisabled = false, showTooltip, accessibilityRole="button" hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} - focusStyle={styles.hoveredComponentBG} > @@ -92,6 +93,6 @@ function BaseListItem({item, isFocused = false, isDisabled = false, showTooltip, } BaseListItem.displayName = 'BaseListItem'; -BaseListItem.propTypes = radioListItemPropTypes; +BaseListItem.propTypes = baseListItemPropTypes; export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 9219600dd85b..ee8fb05706f6 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -11,8 +11,6 @@ import ArrowKeyFocusManager from '../ArrowKeyFocusManager'; import CONST from '../../CONST'; import variables from '../../styles/variables'; import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; -import RadioListItem from './RadioListItem'; -import UserListItem from './UserListItem'; import useKeyboardShortcut from '../../hooks/useKeyboardShortcut'; import SafeAreaConsumer from '../SafeAreaConsumer'; import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; @@ -25,6 +23,7 @@ import Log from '../../libs/Log'; import OptionsListSkeletonView from '../OptionsListSkeletonView'; import useActiveElement from '../../hooks/useActiveElement'; import BaseListItem from './BaseListItem'; +import useArrowKeyFocusManager from '../../hooks/useArrowKeyFocusManager'; const propTypes = { ...keyboardStatePropTypes, @@ -49,10 +48,13 @@ function BaseSelectionList({ headerMessage = '', confirmButtonText = '', onConfirm, + footerContent, showScrollIndicator = false, showLoadingPlaceholder = false, showConfirmButton = false, isKeyboardShown = false, + disableKeyboardShortcuts = false, + children, }) { const {translate} = useLocalize(); const firstLayoutRef = useRef(true); @@ -136,16 +138,13 @@ function BaseSelectionList({ }; }, [canSelectMultiple, sections]); - // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member - const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); - /** * Scrolls to the desired item index in the section list * * @param {Number} index - the index of the item to scroll to * @param {Boolean} animated - whether to animate the scroll */ - const scrollToIndex = (index, animated) => { + const scrollToIndex = useCallback((index, animated = true) => { const item = flattenedSections.allOptions[index]; if (!listRef.current || !item) { @@ -166,7 +165,20 @@ function BaseSelectionList({ } listRef.current.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight}); - }; + + // If this function changes, it causes `useArrowKeyFocusManager` to fire `onFocusedIndexChange`, + // making the list scroll back to the focused index when the keyboard disappears. If we don't disable + // dependencies here, we would need to make sure that the `sections` is stable in every usage of this component. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + maxIndex: flattenedSections.allOptions.length - 1, + onFocusedIndexChange: scrollToIndex, + initialFocusedIndex: _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey), + disabledIndexes: flattenedSections.disabledOptionsIndexes, + isActive: !disableKeyboardShortcuts, + }); const selectRow = (item, index) => { // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item @@ -219,6 +231,14 @@ function BaseSelectionList({ const getItemLayout = (data, flatDataArrayIndex) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; + if (!targetItem) { + return { + length: 0, + offset: 0, + index: flatDataArrayIndex, + }; + } + return { length: targetItem.length, offset: targetItem.offset, @@ -257,29 +277,9 @@ function BaseSelectionList({ showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} onSelectRow={() => selectRow(item, index)} + onDismissError={onDismissError} /> ); - - // if (canSelectMultiple) { - // return ( - // selectRow(item, index)} - // onDismissError={onDismissError} - // showTooltip={showTooltip} - // /> - // ); - // } - // - // return ( - // selectRow(item, index)} - // /> - // ); }; /** Focuses the text input when the component comes into focus and after any navigation animations finish. */ @@ -301,120 +301,113 @@ function BaseSelectionList({ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: !activeElement, + isActive: !disableKeyboardShortcuts && !activeElement, }); /** Calls confirm action when pressing CTRL (CMD) + Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { captureOnInputs: true, shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: Boolean(onConfirm), + isActive: !disableKeyboardShortcuts && Boolean(onConfirm), }); return ( - { - setFocusedIndex(newFocusedIndex); - scrollToIndex(newFocusedIndex, true); - }} - > - - {({safeAreaPaddingBottomStyle}) => ( - - {shouldShowTextInput && ( - - - - )} - {Boolean(headerMessage) && ( - - {headerMessage} - - )} - {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( - - ) : ( - <> - {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - + {({safeAreaPaddingBottomStyle}) => ( + + {shouldShowTextInput && ( + + + + )} + {Boolean(headerMessage) && ( + + {headerMessage} + + )} + {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( + + ) : ( + <> + {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( + + - - - {translate('workspace.people.selectAll')} - - - )} - item.keyForList} - extraData={focusedIndex} - indicatorStyle="white" - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={showScrollIndicator} - initialNumToRender={12} - maxToRenderPerBatch={5} - windowSize={5} - viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} - testID="selection-list" - onLayout={() => { - if (!firstLayoutRef.current) { - return; - } - scrollToIndex(focusedIndex, false); - firstLayoutRef.current = false; - }} - /> - - )} - {showConfirmButton && ( - -