From 0f02054475e0451a19494746992a98b987aa3827 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 26 Sep 2023 15:25:43 +0700 Subject: [PATCH 001/230] fix: composer not focused on click --- src/components/Composer/index.js | 21 ------------- .../report/ReportActionItemMessageEdit.js | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 44075a4ec1eb..f93e8c24b472 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -18,7 +18,6 @@ import Text from '../Text'; import isEnterWhileComposition from '../../libs/KeyboardShortcut/isEnterWhileComposition'; import CONST from '../../CONST'; import withNavigation from '../withNavigation'; -import ReportActionComposeFocusManager from '../../libs/ReportActionComposeFocusManager'; const propTypes = { /** Maximum number of lines in the text input */ @@ -80,9 +79,6 @@ const propTypes = { /** Function to check whether composer is covered up or not */ checkComposerVisibility: PropTypes.func, - /** Whether this is the report action compose */ - isReportActionCompose: PropTypes.bool, - /** Whether the sull composer is open */ isComposerFullSize: PropTypes.bool, @@ -113,7 +109,6 @@ const defaultProps = { setIsFullComposerAvailable: () => {}, shouldCalculateCaretPosition: false, checkComposerVisibility: () => false, - isReportActionCompose: false, isComposerFullSize: false, }; @@ -164,7 +159,6 @@ function Composer({ setIsFullComposerAvailable, checkComposerVisibility, selection: selectionProp, - isReportActionCompose, isComposerFullSize, ...props }) { @@ -388,9 +382,6 @@ function Composer({ } return () => { - if (!isReportActionCompose) { - ReportActionComposeFocusManager.clear(); - } unsubscribeFocus(); unsubscribeBlur(); document.removeEventListener('paste', handlePaste); @@ -468,18 +459,6 @@ function Composer({ numberOfLines={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} - onFocus={(e) => { - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!textInput.current) { - return; - } - - textInput.current.focus(); - }); - if (props.onFocus) { - props.onFocus(e); - } - }} /> {shouldCalculateCaretPosition && renderElementForCaretPosition} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 6ce826a2a34c..262e84e86065 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -40,6 +40,7 @@ import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction'; import focusWithDelay from '../../../libs/focusWithDelay'; import ONYXKEYS from '../../../ONYXKEYS'; import * as Browser from '../../../libs/Browser'; +import willBlurTextInputOnTapOutsideFunc from "../../../libs/willBlurTextInputOnTapOutside"; const propTypes = { /** All the data of the action */ @@ -87,6 +88,7 @@ const emojiButtonID = 'emojiButton'; const messageEditInput = 'messageEditInput'; const isMobileSafari = Browser.isMobileSafari(); +const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionItemMessageEdit(props) { const reportScrollManager = useReportScrollManager(); @@ -133,6 +135,26 @@ function ReportActionItemMessageEdit(props) { [props.action.reportActionID], ); + /** + * Focus the composer text input + * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer + * @memberof ReportActionCompose + */ + const focus = useCallback((shouldDelay = false) => { + focusWithDelay(textInputRef.current)(shouldDelay); + }, []); + + const setUpComposeFocusManager = useCallback(() => { + // This callback is used in the contextMenuActions to manage giving focus back to the compose input. + ReportActionComposeFocusManager.onComposerFocus(() => { + if (!willBlurTextInputOnTapOutside) { + return; + } + + focus(true); + }); + }, [focus]); + useEffect(() => { // For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), @@ -325,11 +347,6 @@ function ReportActionItemMessageEdit(props) { [deleteDraft, isKeyboardShown, isSmallScreenWidth, publishDraft], ); - /** - * Focus the composer text input - */ - const focus = focusWithDelay(textInputRef.current); - return ( <> @@ -385,6 +402,7 @@ function ReportActionItemMessageEdit(props) { setIsFocused(true); reportScrollManager.scrollToIndex({animated: true, index: props.index}, true); setShouldShowComposeInputKeyboardAware(false); + setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(props.action.reportActionID)) { @@ -410,8 +428,7 @@ function ReportActionItemMessageEdit(props) { { - setIsFocused(true); - focus(true); + ReportActionComposeFocusManager.focus(); }} onEmojiSelected={addEmojiToTextBox} nativeID={emojiButtonID} From 86d30a41842e7067260ceee1e366145d4f5aa09d Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 26 Sep 2023 16:14:06 +0700 Subject: [PATCH 002/230] add focus check --- src/libs/ReportActionComposeFocusManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index ca4f9d77898b..a54648f846f2 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -28,8 +28,8 @@ function onComposerFocus(callback: FocusCallback, isMainComposer = false) { * Request focus on the ReportActionComposer */ function focus() { - if (typeof focusCallback !== 'function') { - if (typeof mainComposerFocusCallback !== 'function') { + if ((typeof focusCallback !== 'function') || (composerRef.current && composerRef.current.isFocused())) { + if (typeof mainComposerFocusCallback !== 'function' || ((editComposerRef.current && editComposerRef.current.isFocused()))) { return; } From 27f6a0538db3bf052fd8dbc0364eaa8f2cdf27fa Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 26 Sep 2023 17:13:37 +0700 Subject: [PATCH 003/230] reuse existing functions --- src/libs/ReportActionComposeFocusManager.ts | 43 +++++++++++---------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index a54648f846f2..515a73a6a72e 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -10,6 +10,20 @@ const editComposerRef = React.createRef(); let focusCallback: FocusCallback | null = null; let mainComposerFocusCallback: FocusCallback | null = null; +/** + * Exposes the current focus state of the report action composer. + */ +function isFocused(): boolean { + return !!composerRef.current?.isFocused(); +} + +/** + * Exposes the current focus state of the edit message composer. + */ +function isEditFocused(): boolean { + return !!editComposerRef.current?.isFocused(); +} + /** * Register a callback to be called when focus is requested. * Typical uses of this would be call the focus on the ReportActionComposer. @@ -28,16 +42,17 @@ function onComposerFocus(callback: FocusCallback, isMainComposer = false) { * Request focus on the ReportActionComposer */ function focus() { - if ((typeof focusCallback !== 'function') || (composerRef.current && composerRef.current.isFocused())) { - if (typeof mainComposerFocusCallback !== 'function' || ((editComposerRef.current && editComposerRef.current.isFocused()))) { - return; - } - - mainComposerFocusCallback(); + if (typeof focusCallback !== 'function' && typeof mainComposerFocusCallback !== 'function') { return; } - focusCallback(); + if (typeof focusCallback === 'function' && !isFocused()) { + focusCallback(); + return; + } + if (typeof mainComposerFocusCallback === 'function' && !isEditFocused()) { + mainComposerFocusCallback(); + } } /** @@ -51,20 +66,6 @@ function clear(isMainComposer = false) { } } -/** - * Exposes the current focus state of the report action composer. - */ -function isFocused(): boolean { - return !!composerRef.current?.isFocused(); -} - -/** - * Exposes the current focus state of the edit message composer. - */ -function isEditFocused(): boolean { - return !!editComposerRef.current?.isFocused(); -} - export default { composerRef, onComposerFocus, From 24d1fa98f7da439482dc6d4b03bc6f904a33b839 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 26 Sep 2023 17:15:39 +0700 Subject: [PATCH 004/230] clear FocusManager on unmount --- src/pages/home/report/ReportActionItemMessageEdit.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 262e84e86065..3bca209cb06d 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -40,7 +40,7 @@ import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction'; import focusWithDelay from '../../../libs/focusWithDelay'; import ONYXKEYS from '../../../ONYXKEYS'; import * as Browser from '../../../libs/Browser'; -import willBlurTextInputOnTapOutsideFunc from "../../../libs/willBlurTextInputOnTapOutside"; +import willBlurTextInputOnTapOutsideFunc from '../../../libs/willBlurTextInputOnTapOutside'; const propTypes = { /** All the data of the action */ @@ -156,6 +156,8 @@ function ReportActionItemMessageEdit(props) { }, [focus]); useEffect(() => { + setUpComposeFocusManager(); + // For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), // so we need to ensure that it is only updated after focus. @@ -170,6 +172,8 @@ function ReportActionItemMessageEdit(props) { } return () => { + ReportActionComposeFocusManager.clear(); + // Skip if the current report action is not active if (!isActive()) { return; @@ -402,7 +406,6 @@ function ReportActionItemMessageEdit(props) { setIsFocused(true); reportScrollManager.scrollToIndex({animated: true, index: props.index}, true); setShouldShowComposeInputKeyboardAware(false); - setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(props.action.reportActionID)) { From 801066cc37616cb09d63f990564333ca90ba8a3f Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 4 Oct 2023 09:36:48 +0700 Subject: [PATCH 005/230] focus main composer --- src/libs/ReportActionComposeFocusManager.ts | 33 ++++++++++--------- .../ComposerWithSuggestions.js | 26 +++++++++------ .../report/ReportActionItemMessageEdit.js | 2 +- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 515a73a6a72e..14a4139600c7 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -10,25 +10,12 @@ const editComposerRef = React.createRef(); let focusCallback: FocusCallback | null = null; let mainComposerFocusCallback: FocusCallback | null = null; -/** - * Exposes the current focus state of the report action composer. - */ -function isFocused(): boolean { - return !!composerRef.current?.isFocused(); -} - -/** - * Exposes the current focus state of the edit message composer. - */ -function isEditFocused(): boolean { - return !!editComposerRef.current?.isFocused(); -} - /** * Register a callback to be called when focus is requested. * Typical uses of this would be call the focus on the ReportActionComposer. * * @param callback callback to register + * @param isMainComposer */ function onComposerFocus(callback: FocusCallback, isMainComposer = false) { if (isMainComposer) { @@ -46,11 +33,11 @@ function focus() { return; } - if (typeof focusCallback === 'function' && !isFocused()) { + if (typeof focusCallback === 'function') { focusCallback(); return; } - if (typeof mainComposerFocusCallback === 'function' && !isEditFocused()) { + if (typeof mainComposerFocusCallback === 'function') { mainComposerFocusCallback(); } } @@ -66,6 +53,20 @@ function clear(isMainComposer = false) { } } +/** + * Exposes the current focus state of the report action composer. + */ +function isFocused(): boolean { + return !!composerRef.current?.isFocused(); +} + +/** + * Exposes the current focus state of the edit message composer. + */ +function isEditFocused(): boolean { + return !!editComposerRef.current?.isFocused(); +} + export default { composerRef, onComposerFocus, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 6e103f77c068..ae0445122bca 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -384,16 +384,19 @@ function ComposerWithSuggestions({ focusWithDelay(textInputRef.current)(shouldDelay); }, []); - const setUpComposeFocusManager = useCallback(() => { - // This callback is used in the contextMenuActions to manage giving focus back to the compose input. - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!willBlurTextInputOnTapOutside || !isFocused) { - return; - } + const setUpComposeFocusManager = useCallback( + (isMainComposer = true) => { + // This callback is used in the contextMenuActions to manage giving focus back to the compose input. + ReportActionComposeFocusManager.onComposerFocus(() => { + if (!willBlurTextInputOnTapOutside || !isFocused) { + return; + } - focus(false); - }, true); - }, [focus, isFocused]); + focus(false); + }, isMainComposer); + }, + [focus, isFocused], + ); /** * Check if the composer is visible. Returns true if the composer is not covered up by emoji picker or menu. False otherwise. @@ -517,7 +520,10 @@ function ComposerWithSuggestions({ onKeyPress={triggerHotkeyActions} style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.flex4]} maxLines={maxComposerLines} - onFocus={onFocus} + onFocus={() => { + setUpComposeFocusManager(false); + onFocus(); + }} onBlur={onBlur} onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={displayFileInModal} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 0860b73b6df6..467efee48fd2 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -142,7 +142,6 @@ function ReportActionItemMessageEdit(props) { }, []); const setUpComposeFocusManager = useCallback(() => { - // This callback is used in the contextMenuActions to manage giving focus back to the compose input. ReportActionComposeFocusManager.onComposerFocus(() => { if (!willBlurTextInputOnTapOutside) { return; @@ -386,6 +385,7 @@ function ReportActionItemMessageEdit(props) { setIsFocused(true); reportScrollManager.scrollToIndex({animated: true, index: props.index}, true); setShouldShowComposeInputKeyboardAware(false); + setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(props.action.reportActionID)) { From 047281254c561e7d3903cc18531a526ebba323bd Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 4 Oct 2023 11:50:48 +0700 Subject: [PATCH 006/230] re-focus the right composer on emoji modal hide --- src/components/EmojiPicker/EmojiPickerButton.js | 7 ++++++- src/libs/ReportActionComposeFocusManager.ts | 1 + src/pages/home/report/ReportActionItemMessageEdit.js | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index cbfc3517117c..faa04ff4b92e 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -20,6 +20,9 @@ const propTypes = { /** Unique id for emoji picker */ emojiPickerID: PropTypes.string, + /** A callback function when the button is pressed */ + onPress: PropTypes.func, + ...withLocalizePropTypes, }; @@ -27,6 +30,7 @@ const defaultProps = { isDisabled: false, nativeID: '', emojiPickerID: '', + onPress: () => {}, }; function EmojiPickerButton(props) { @@ -40,12 +44,13 @@ function EmojiPickerButton(props) { ref={emojiPopoverAnchor} style={({hovered, pressed}) => [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} disabled={props.isDisabled} - onPress={() => { + onPress={(e) => { if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor.current, undefined, () => {}, props.emojiPickerID); } else { EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } + props.onPress(e); }} nativeID={props.nativeID} accessibilityLabel={props.translate('reportActionCompose.emoji')} diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 14a4139600c7..9c83b312a25c 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -37,6 +37,7 @@ function focus() { focusCallback(); return; } + if (typeof mainComposerFocusCallback === 'function') { mainComposerFocusCallback(); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 467efee48fd2..6a29240679de 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -385,7 +385,6 @@ function ReportActionItemMessageEdit(props) { setIsFocused(true); reportScrollManager.scrollToIndex({animated: true, index: props.index}, true); setShouldShowComposeInputKeyboardAware(false); - setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(props.action.reportActionID)) { @@ -416,6 +415,9 @@ function ReportActionItemMessageEdit(props) { onEmojiSelected={addEmojiToTextBox} nativeID={emojiButtonID} emojiPickerID={props.action.reportActionID} + onPress={() => { + setUpComposeFocusManager(); + }} /> From b2dede84cd566d3d7646c997a268889bc264eb55 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 4 Oct 2023 15:58:20 +0700 Subject: [PATCH 007/230] refocus composer on context menu close --- src/pages/home/report/ReportActionItemMessageEdit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 6a29240679de..525f8d7c2a58 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -385,6 +385,7 @@ function ReportActionItemMessageEdit(props) { setIsFocused(true); reportScrollManager.scrollToIndex({animated: true, index: props.index}, true); setShouldShowComposeInputKeyboardAware(false); + setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(props.action.reportActionID)) { From 3f4e9d7dfc7d639463688d85fda80d0edb226df4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 5 Oct 2023 01:35:22 +0700 Subject: [PATCH 008/230] remove willBlurOnTapOutside check to re-focus the correct composer --- src/pages/home/report/ReportActionItemMessageEdit.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 525f8d7c2a58..1354645f4a23 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -37,7 +37,6 @@ import useReportScrollManager from '../../../hooks/useReportScrollManager'; import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction'; import focusWithDelay from '../../../libs/focusWithDelay'; import * as Browser from '../../../libs/Browser'; -import willBlurTextInputOnTapOutsideFunc from '../../../libs/willBlurTextInputOnTapOutside'; const propTypes = { /** All the data of the action */ @@ -78,7 +77,6 @@ const emojiButtonID = 'emojiButton'; const messageEditInput = 'messageEditInput'; const isMobileSafari = Browser.isMobileSafari(); -const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionItemMessageEdit(props) { const reportScrollManager = useReportScrollManager(); @@ -143,10 +141,6 @@ function ReportActionItemMessageEdit(props) { const setUpComposeFocusManager = useCallback(() => { ReportActionComposeFocusManager.onComposerFocus(() => { - if (!willBlurTextInputOnTapOutside) { - return; - } - focus(true); }); }, [focus]); From 6bcd0897a460e0590c7ba7626c02f32883a283ff Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 6 Oct 2023 17:42:26 +0700 Subject: [PATCH 009/230] clear focus callback on unmount --- src/libs/ReportActionComposeFocusManager.ts | 1 - src/pages/home/report/ReportActionItemMessageEdit.js | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 9c83b312a25c..73a8333b7ea7 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -15,7 +15,6 @@ let mainComposerFocusCallback: FocusCallback | null = null; * Typical uses of this would be call the focus on the ReportActionComposer. * * @param callback callback to register - * @param isMainComposer */ function onComposerFocus(callback: FocusCallback, isMainComposer = false) { if (isMainComposer) { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 1354645f4a23..446d0edf4fc3 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -145,9 +145,14 @@ function ReportActionItemMessageEdit(props) { }); }, [focus]); - useEffect(() => { - setUpComposeFocusManager(); + useEffect( + () => () => { + ReportActionComposeFocusManager.clear(); + }, + [], + ); + useEffect(() => { // For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), // so we need to ensure that it is only updated after focus. @@ -162,8 +167,6 @@ function ReportActionItemMessageEdit(props) { } return () => { - ReportActionComposeFocusManager.clear(); - // Skip if the current report action is not active if (!isActive()) { return; From b1025c19062312605dc45722d6cb0e73b2f2438d Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 6 Oct 2023 17:44:58 +0700 Subject: [PATCH 010/230] add comment --- src/pages/home/report/ReportActionItemMessageEdit.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 446d0edf4fc3..57dd8b154669 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -142,7 +142,7 @@ function ReportActionItemMessageEdit(props) { const setUpComposeFocusManager = useCallback(() => { ReportActionComposeFocusManager.onComposerFocus(() => { focus(true); - }); + }, false); }, [focus]); useEffect( @@ -413,9 +413,8 @@ function ReportActionItemMessageEdit(props) { onEmojiSelected={addEmojiToTextBox} nativeID={emojiButtonID} emojiPickerID={props.action.reportActionID} - onPress={() => { - setUpComposeFocusManager(); - }} + // Let the pressed composer re-gain focus on modal hide + onPress={setUpComposeFocusManager} /> From 3439d4943b7998c4e3335a1f44e7c1de00a15a47 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 6 Oct 2023 19:30:36 +0700 Subject: [PATCH 011/230] add comment for removing callback --- src/pages/home/report/ReportActionItemMessageEdit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 57dd8b154669..a4110bc680fb 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -146,6 +146,7 @@ function ReportActionItemMessageEdit(props) { }, [focus]); useEffect( + // Remove focus callback on unmount to avoid stale callbacks () => () => { ReportActionComposeFocusManager.clear(); }, @@ -413,7 +414,7 @@ function ReportActionItemMessageEdit(props) { onEmojiSelected={addEmojiToTextBox} nativeID={emojiButtonID} emojiPickerID={props.action.reportActionID} - // Let the pressed composer re-gain focus on modal hide + // Set this composer the focus target when its Emoji Picker is opened onPress={setUpComposeFocusManager} /> From 759b4a54311f28874db58277ac65d8bc6ab5b219 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 8 Dec 2023 17:32:47 +0700 Subject: [PATCH 012/230] implement callback priority approach --- src/libs/ReportActionComposeFocusManager.ts | 28 +++++++++---------- .../ComposerWithSuggestions.js | 15 ++++++---- .../report/ReportActionItemMessageEdit.js | 13 +++++---- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 8e96be9a4d3d..579e44339ddf 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -7,10 +7,10 @@ type FocusCallback = () => void; const composerRef = React.createRef(); const editComposerRef = React.createRef(); -// There are two types of composer: general composer (edit composer) and main composer. -// The general composer callback will take priority if it exists. +// There are two types of focus callbacks: priority and general +// Priority callback would take priority if it existed +let priorityFocusCallback: FocusCallback | null = null; let focusCallback: FocusCallback | null = null; -let mainComposerFocusCallback: FocusCallback | null = null; /** * Register a callback to be called when focus is requested. @@ -18,9 +18,9 @@ let mainComposerFocusCallback: FocusCallback | null = null; * * @param callback callback to register */ -function onComposerFocus(callback: FocusCallback, isMainComposer = false) { - if (isMainComposer) { - mainComposerFocusCallback = callback; +function onComposerFocus(callback: FocusCallback, isPriorityCallback = false) { + if (isPriorityCallback) { + priorityFocusCallback = callback; } else { focusCallback = callback; } @@ -35,26 +35,26 @@ function focus() { return; } - if (typeof focusCallback !== 'function' && typeof mainComposerFocusCallback !== 'function') { + if (typeof priorityFocusCallback !== 'function' && typeof focusCallback !== 'function') { return; } - if (typeof focusCallback === 'function') { - focusCallback(); + if (typeof priorityFocusCallback === 'function') { + priorityFocusCallback(); return; } - if (typeof mainComposerFocusCallback === 'function') { - mainComposerFocusCallback(); + if (typeof focusCallback === 'function') { + focusCallback(); } } /** * Clear the registered focus callback */ -function clear(isMainComposer = false) { - if (isMainComposer) { - mainComposerFocusCallback = null; +function clear(isPriorityCallback = false) { + if (isPriorityCallback) { + priorityFocusCallback = null; } else { focusCallback = null; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 2b03dce3d3b2..d2ae58592641 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -400,16 +400,19 @@ function ComposerWithSuggestions({ focusComposerWithDelay(textInputRef.current)(shouldDelay); }, []); + /** + * Set focus callback + * @param {Boolean} [shouldTakeOverFocus=false] Whether this composer should gain focus priority + */ const setUpComposeFocusManager = useCallback( - (isMainComposer = true) => { - // This callback is used in the contextMenuActions to manage giving focus back to the compose input. + (shouldTakeOverFocus = false) => { ReportActionComposeFocusManager.onComposerFocus(() => { if (!willBlurTextInputOnTapOutside || !isFocused) { return; } focus(false); - }, isMainComposer); + }, shouldTakeOverFocus); }, [focus, isFocused], ); @@ -464,8 +467,7 @@ function ComposerWithSuggestions({ setUpComposeFocusManager(); return () => { - ReportActionComposeFocusManager.clear(true); - + ReportActionComposeFocusManager.clear(); KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress); unsubscribeNavigationBlur(); unsubscribeNavigationFocus(); @@ -534,7 +536,8 @@ function ComposerWithSuggestions({ style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} maxLines={maxComposerLines} onFocus={() => { - setUpComposeFocusManager(false); + // The last composer that had focus should re-gain focus + setUpComposeFocusManager(true); onFocus(); }} onBlur={onBlur} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index fd384ea03677..77884a270e93 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -182,16 +182,17 @@ function ReportActionItemMessageEdit(props) { focusComposerWithDelay(textInputRef.current)(shouldDelay); }, []); + // Take over focus priority const setUpComposeFocusManager = useCallback(() => { ReportActionComposeFocusManager.onComposerFocus(() => { focus(true); - }, false); + }, true); }, [focus]); useEffect( // Remove focus callback on unmount to avoid stale callbacks () => () => { - ReportActionComposeFocusManager.clear(); + ReportActionComposeFocusManager.clear(true); }, [], ); @@ -316,8 +317,9 @@ function ReportActionItemMessageEdit(props) { Report.saveReportActionDraft(props.reportID, props.action, ''); if (isActive()) { - ReportActionComposeFocusManager.clear(); - ReportActionComposeFocusManager.focus(); + ReportActionComposeFocusManager.clear(true); + // Wait for report action compose re-mounting + InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus()); } // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report. @@ -435,6 +437,7 @@ function ReportActionItemMessageEdit(props) { setIsFocused(true); reportScrollManager.scrollToIndex(props.index, true); setShouldShowComposeInputKeyboardAware(false); + // The last composer that had focus should re-gain focus setUpComposeFocusManager(); // Clear active report action when another action gets focused @@ -466,7 +469,7 @@ function ReportActionItemMessageEdit(props) { onEmojiSelected={addEmojiToTextBox} id={emojiButtonID} emojiPickerID={props.action.reportActionID} - // Set this composer the focus target when its Emoji Picker is opened + // Edit composer should be focused after emoji picker closed onPress={setUpComposeFocusManager} /> From a20eace1e4fb17ce4230185839e431635bd2b750 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 8 Dec 2023 17:34:41 +0700 Subject: [PATCH 013/230] modify comment --- src/pages/home/report/ReportActionItemMessageEdit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 77884a270e93..7b665f5e3779 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -318,7 +318,7 @@ function ReportActionItemMessageEdit(props) { if (isActive()) { ReportActionComposeFocusManager.clear(true); - // Wait for report action compose re-mounting + // Wait for report action compose re-mounting on mWeb InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus()); } From 66ce5433cbe762b2d013660545acf0d1bad483d5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 20 Dec 2023 16:19:43 +0700 Subject: [PATCH 014/230] fix lint --- src/components/Composer/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 26254292c35f..db64b348b69f 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -15,7 +15,6 @@ import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; import {ComposerProps} from './types'; From 2207db402a87f104de8892bdec402351c9ab334e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 8 May 2024 18:58:03 +0700 Subject: [PATCH 015/230] fix implement recently used currencies --- src/ONYXKEYS.ts | 2 + src/ROUTES.ts | 7 +- .../CurrencySelectionList/index.tsx | 30 ++++++++- src/components/CurrencySelectionList/types.ts | 3 + src/libs/Navigation/types.ts | 1 + src/libs/actions/IOU.ts | 65 ++++++++++++++++++- src/libs/actions/Policy.ts | 19 ++++++ .../iou/request/step/IOURequestStepAmount.tsx | 2 +- .../request/step/IOURequestStepCurrency.tsx | 9 ++- .../step/IOURequestStepTaxAmountPage.tsx | 1 + src/types/onyx/RecentlyUsedCurrencies.ts | 3 + src/types/onyx/index.ts | 2 + 12 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/types/onyx/RecentlyUsedCurrencies.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 804c8dadd553..a7f63306ef23 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -326,6 +326,7 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', POLICY_RECENTLY_USED_TAGS: 'nvp_recentlyUsedTags_', + POLICY_RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies_', // Whether the policy's connection data was attempted to be fetched in // the current user session. As this state only exists client-side, it // should not be included as part of the policy object. The policy @@ -565,6 +566,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; + [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES]: OnyxTypes.RecentlyUsedCurrencies; [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: OnyxTypes.SelectedTabRequest; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index afe806c193aa..5c826a10a0af 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -348,8 +348,11 @@ const ROUTES = { }, MONEY_REQUEST_STEP_CURRENCY: { route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/currency/${transactionID}/${reportID}/${pageIndex}?currency=${currency}`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '', policyID?: string) => + getUrlWithBackToParam( + `${action as string}/${iouType as string}/currency/${transactionID}/${reportID}/${pageIndex}?currency=${currency}${`${policyID ? `&policyID=${policyID}` : ''}`}`, + backTo, + ), }, MONEY_REQUEST_STEP_DATE: { route: ':action/:iouType/date/:transactionID/:reportID/:reportActionID?', diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 361d82140326..654bda46a7d9 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -6,9 +6,10 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; -function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList}: CurrencySelectionListProps) { +function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList, policyRecentlyUsedCurrencies}: CurrencySelectionListProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); @@ -24,10 +25,35 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, }; }); + const policyRecentlyUsedCurrencyOptions: CurrencyListItem[] = + policyRecentlyUsedCurrencies?.map((currencyCode) => { + const currencyInfo = currencyList?.[currencyCode]; + const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode; + return { + currencyName: currencyInfo?.name ?? '', + text: `${currencyCode} - ${CurrencyUtils.getCurrencySymbol(currencyCode)}`, + currencyCode, + keyForList: currencyCode, + isSelected: isSelectedCurrency, + }; + }) ?? []; + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); const filteredCurrencies = currencyOptions.filter((currencyOption) => searchRegex.test(currencyOption.text ?? '') || searchRegex.test(currencyOption.currencyName)); const isEmpty = searchValue.trim() && !filteredCurrencies.length; + const shouldDisplayRecentlyOptions = !isEmptyObject(policyRecentlyUsedCurrencyOptions) && !searchValue; + if (shouldDisplayRecentlyOptions) { + return { + sections: isEmpty + ? [] + : [ + {title: translate('common.recents'), data: policyRecentlyUsedCurrencyOptions, shouldShow: shouldDisplayRecentlyOptions}, + {title: translate('common.all'), data: filteredCurrencies}, + ], + headerMessage: isEmpty ? translate('common.noResultsFound') : '', + }; + } return { sections: isEmpty ? [] @@ -38,7 +64,7 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', }; - }, [currencyList, searchValue, translate, initiallySelectedCurrencyCode]); + }, [currencyList, searchValue, translate, initiallySelectedCurrencyCode, policyRecentlyUsedCurrencies]); return ( void; }; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 13204fe97111..dac43adbf6fb 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -597,6 +597,7 @@ type MoneyRequestNavigatorParamList = { pageIndex?: string; backTo?: Routes; currency?: string; + policyID?: string; }; }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7171a7d6732a..cf9b1476efaf 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -455,6 +455,7 @@ function buildOnyxDataForMoneyRequest( optimisticNextStep?: OnyxTypes.ReportNextStep | null, isOneOnOneSplit = false, existingTransactionThreadReportID?: string, + optimisticPolicyRecentlyUsedCurrencies?: string[], ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const isScanRequest = TransactionUtils.isScanRequest(transaction); const outstandingChildRequest = ReportUtils.getOutstandingChildRequest(iouReport); @@ -578,6 +579,14 @@ function buildOnyxDataForMoneyRequest( }); } + if (optimisticPolicyRecentlyUsedCurrencies?.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${iouReport.policyID}`, + value: optimisticPolicyRecentlyUsedCurrencies, + }); + } + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -819,6 +828,7 @@ function buildOnyxDataForInvoice( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, + optimisticPolicyRecentlyUsedCurrencies?: string[], ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = [ @@ -907,6 +917,14 @@ function buildOnyxDataForInvoice( }); } + if (optimisticPolicyRecentlyUsedCurrencies?.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${iouReport.policyID}`, + value: optimisticPolicyRecentlyUsedCurrencies, + }); + } + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -1656,7 +1674,7 @@ function getSendInvoiceInformation( const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(optimisticInvoiceReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); - + const optimisticPolicyRecentlyUsedCurrencies = Policy.buildOptimisticPolicyRecentlyUsedCurrencies(optimisticInvoiceReport.policyID, currency); // STEP 4: Add optimistic personal details for participant const shouldCreateOptimisticPersonalDetails = isNewChatReport && !allPersonalDetails[receiverAccountID]; if (shouldCreateOptimisticPersonalDetails) { @@ -1713,6 +1731,7 @@ function getSendInvoiceInformation( policy, policyTagList, policyCategories, + optimisticPolicyRecentlyUsedCurrencies, ); return { @@ -1835,7 +1854,8 @@ function getMoneyRequestInformation( const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); - + const optimisticPolicyRecentluUsedCurrencies = Policy.buildOptimisticPolicyRecentlyUsedCurrencies(iouReport.policyID, currency); + // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. @@ -1922,6 +1942,9 @@ function getMoneyRequestInformation( policyTagList, policyCategories, optimisticNextStep, + undefined, + undefined, + optimisticPolicyRecentluUsedCurrencies, ); return { @@ -2472,6 +2495,18 @@ function getUpdateMoneyRequestParams( } } + // Update recently used currencies if the category is changed + if ('currency' in transactionChanges) { + const optimisticPolicyRecentlyUsedCurrencies = Policy.buildOptimisticPolicyRecentlyUsedCurrencies(iouReport?.policyID, transactionChanges.currency); + if (optimisticPolicyRecentlyUsedCurrencies.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${iouReport?.policyID}`, + value: optimisticPolicyRecentlyUsedCurrencies, + }); + } + } + // Update recently used categories if the tag is changed if ('tag' in transactionChanges) { const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport?.policyID, transactionChanges.tag); @@ -3957,6 +3992,9 @@ function createSplitsAndOnyxData( // Add category to optimistic policy recently used categories when a participant is a workspace const optimisticPolicyRecentlyUsedCategories = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category) : []; + // Add category to optimistic policy recently used currencies when a participant is a workspace + const optimisticPolicyRecentlyUsedCurrencies = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedCurrencies(participant.policyID, currency) : []; + // Add tag to optimistic policy recently used tags when a participant is a workspace const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag) : {}; @@ -3981,6 +4019,8 @@ function createSplitsAndOnyxData( null, null, true, + undefined, + optimisticPolicyRecentlyUsedCurrencies, ); const individualSplit = { @@ -4426,6 +4466,7 @@ function startSplitBill({ } const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category); + const optimisticPolicyRecentlyUsedCurrencies = Policy.buildOptimisticPolicyRecentlyUsedCurrencies(participant.policyID, currency); const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag); if (optimisticPolicyRecentlyUsedCategories.length > 0) { @@ -4436,6 +4477,14 @@ function startSplitBill({ }); } + if (optimisticPolicyRecentlyUsedCurrencies.length > 0) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${participant.policyID}`, + value: optimisticPolicyRecentlyUsedCurrencies, + }); + } + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -4853,6 +4902,18 @@ function editRegularMoneyRequest( } } + // Update recently used currencies if the category is changed + if ('currency' in transactionChanges) { + const optimisticPolicyRecentlyUsedCurrencies = Policy.buildOptimisticPolicyRecentlyUsedCurrencies(iouReport?.policyID, transactionChanges.currency); + if (optimisticPolicyRecentlyUsedCurrencies.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${iouReport?.policyID}`, + value: optimisticPolicyRecentlyUsedCurrencies, + }); + } + } + // Update recently used categories if the tag is changed if ('tag' in transactionChanges) { const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport?.policyID, transactionChanges.tag); diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 6dc46383f3fa..1bfe60a1551a 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -86,6 +86,7 @@ import type { PolicyTagList, PolicyTags, RecentlyUsedCategories, + RecentlyUsedCurrencies, RecentlyUsedTags, ReimbursementAccount, Report, @@ -218,6 +219,13 @@ Onyx.connect({ callback: (val) => (allRecentlyUsedCategories = val), }); +let allRecentlyUsedCurrencies: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES, + waitForCollectionCallback: true, + callback: (val) => (allRecentlyUsedCurrencies = val), +}); + let allPolicyTags: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, @@ -2770,6 +2778,16 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID?: string, category return lodashUnion([category], policyRecentlyUsedCategories); } +function buildOptimisticPolicyRecentlyUsedCurrencies(policyID?: string, currency?: string) { + if (!policyID || !currency) { + return []; + } + + const policyRecentlyUsedCurrencies = allRecentlyUsedCurrencies?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${policyID}`] ?? []; + + return lodashUnion([currency], policyRecentlyUsedCurrencies); +} + function buildOptimisticPolicyRecentlyUsedTags(policyID?: string, transactionTags?: string): RecentlyUsedTags { if (!policyID || !transactionTags) { return {}; @@ -5236,6 +5254,7 @@ export { dismissAddedWithPrimaryLoginMessages, openDraftWorkspaceRequest, buildOptimisticPolicyRecentlyUsedCategories, + buildOptimisticPolicyRecentlyUsedCurrencies, buildOptimisticPolicyRecentlyUsedTags, createDraftInitialWorkspace, setWorkspaceInviteMessageDraft, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 6786198b1dc8..c418c8042ceb 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -128,7 +128,7 @@ function IOURequestStepAmount({ const navigateToCurrencySelectionPage = () => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CURRENCY.getRoute(action, iouType, transactionID, reportID, backTo ? 'confirm' : '', currency, Navigation.getActiveRouteWithoutParams()), + ROUTES.MONEY_REQUEST_STEP_CURRENCY.getRoute(action, iouType, transactionID, reportID, backTo ? 'confirm' : '', currency, Navigation.getActiveRouteWithoutParams(), policy?.id), ); }; diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index 8669563f3b9f..4037cb58de6e 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -14,7 +14,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {getUrlWithBackToParam} from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Transaction} from '@src/types/onyx'; +import type {RecentlyUsedCurrencies, Transaction} from '@src/types/onyx'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; @@ -22,6 +22,8 @@ import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNo type IOURequestStepCurrencyOnyxProps = { /** The draft transaction object being modified in Onyx */ draftTransaction: OnyxEntry; + /** List of recently used currencies */ + policyRecentlyUsedCurrencies: OnyxEntry; }; type IOURequestStepCurrencyProps = IOURequestStepCurrencyOnyxProps & WithFullTransactionOrNotFoundProps; @@ -31,6 +33,7 @@ function IOURequestStepCurrency({ params: {backTo, iouType, pageIndex, reportID, transactionID, action, currency: selectedCurrency = ''}, }, draftTransaction, + policyRecentlyUsedCurrencies, }: IOURequestStepCurrencyProps) { const {translate} = useLocalize(); const {currency: originalCurrency = ''} = ReportUtils.getTransactionDetails(draftTransaction) ?? {}; @@ -74,6 +77,7 @@ function IOURequestStepCurrency({ > {({didScreenTransitionEnd}) => ( { if (!didScreenTransitionEnd) { @@ -97,6 +101,9 @@ const IOURequestStepCurrencyWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${route.params.policyID ?? 0}`, + }, })(IOURequestStepCurrency); /* eslint-disable rulesdir/no-negated-variables */ diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index 22a00591b4fc..d90c7e3c155e 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -96,6 +96,7 @@ function IOURequestStepTaxAmountPage({ backTo ? 'confirm' : '', currency, Navigation.getActiveRouteWithoutParams(), + policy?.id, ), ); }; diff --git a/src/types/onyx/RecentlyUsedCurrencies.ts b/src/types/onyx/RecentlyUsedCurrencies.ts new file mode 100644 index 000000000000..59762123c34c --- /dev/null +++ b/src/types/onyx/RecentlyUsedCurrencies.ts @@ -0,0 +1,3 @@ +type RecentlyUsedCurrencies = string[]; + +export default RecentlyUsedCurrencies; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 1695daebace8..532465c550ec 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -47,6 +47,7 @@ import type PriorityMode from './PriorityMode'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type QuickAction from './QuickAction'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; +import type RecentlyUsedCurrencies from './RecentlyUsedCurrencies'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; import type RecentlyUsedTags from './RecentlyUsedTags'; import type RecentWaypoint from './RecentWaypoint'; @@ -132,6 +133,7 @@ export type { RecentWaypoint, RecentlyUsedCategories, RecentlyUsedTags, + RecentlyUsedCurrencies, ReimbursementAccount, Report, ReportAction, From 0309bbfd5b54a6d8303fa687ee977d4845c2f6f5 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 9 May 2024 10:42:38 +0700 Subject: [PATCH 016/230] fix lint --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cf9b1476efaf..784c460e2798 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1855,7 +1855,7 @@ function getMoneyRequestInformation( const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); const optimisticPolicyRecentluUsedCurrencies = Policy.buildOptimisticPolicyRecentlyUsedCurrencies(iouReport.policyID, currency); - + // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. From cfa6401396ebedb234b99c4daab1a37c6852de98 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 12 Jun 2024 16:13:36 +0700 Subject: [PATCH 017/230] fix handle p2p request --- src/libs/actions/Policy/Policy.ts | 12 +++++++++--- src/types/onyx/RecentlyUsedCurrencies.ts | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 15f6d209477e..ff2afc3dcb97 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2071,11 +2071,17 @@ function dismissAddedWithPrimaryLoginMessages(policyID: string) { } function buildOptimisticPolicyRecentlyUsedCurrencies(policyID?: string, currency?: string) { - if (!policyID || !currency) { + if (!currency) { return []; } - - const policyRecentlyUsedCurrencies = allRecentlyUsedCurrencies?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${policyID}`] ?? []; + const personalPolicy = PolicyUtils.getPersonalPolicy(); + const personalPolicyID = personalPolicy?.id; + if (!policyID && !personalPolicyID) { + return []; + } + const policyRecentlyUsedCurrencies = policyID + ? allRecentlyUsedCurrencies?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${policyID}`] + : allRecentlyUsedCurrencies?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${personalPolicyID}`]; return lodashUnion([currency], policyRecentlyUsedCurrencies); } diff --git a/src/types/onyx/RecentlyUsedCurrencies.ts b/src/types/onyx/RecentlyUsedCurrencies.ts index 59762123c34c..9e8ee7f9b6e9 100644 --- a/src/types/onyx/RecentlyUsedCurrencies.ts +++ b/src/types/onyx/RecentlyUsedCurrencies.ts @@ -1,3 +1,4 @@ +/** Policy currencies that have been recently used for each policy */ type RecentlyUsedCurrencies = string[]; export default RecentlyUsedCurrencies; From 4ea13c547fa0f4bec87764097d3b94e6c2fccb06 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 18 Jun 2024 21:31:39 +0700 Subject: [PATCH 018/230] fix type --- src/libs/actions/Policy/Policy.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index baec3de91ad1..85a5ee4d451b 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -51,9 +51,20 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyCategory, ReimbursementAccount, Report, ReportAction, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; +import type { + InvitedEmailsToAccountIDs, + PersonalDetailsList, + Policy, + PolicyCategory, + RecentlyUsedCurrencies, + ReimbursementAccount, + Report, + ReportAction, + TaxRatesWithDefault, + Transaction, +} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; -import type {Attributes, CompanyAddress, CustomUnit, Rate, RecentlyUsedCurrencies, TaxRate, Unit} from '@src/types/onyx/Policy'; +import type {Attributes, CompanyAddress, CustomUnit, Rate, TaxRate, Unit} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; From 2326651eddfc8b3ae4fd78cc63308c73ee88b7f3 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 17 Jul 2024 18:00:10 +0700 Subject: [PATCH 019/230] fix fallback to personal policy --- src/pages/iou/request/step/IOURequestStepAmount.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index bc8fbcc3188b..ba8d73d7dd74 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -10,6 +10,7 @@ import * as TransactionEdit from '@libs/actions/TransactionEdit'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import {getRequestType} from '@libs/TransactionUtils'; @@ -134,7 +135,16 @@ function IOURequestStepAmount({ const navigateToCurrencySelectionPage = () => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CURRENCY.getRoute(action, iouType, transactionID, reportID, backTo ? 'confirm' : '', currency, Navigation.getActiveRouteWithoutParams(), policy?.id), + ROUTES.MONEY_REQUEST_STEP_CURRENCY.getRoute( + action, + iouType, + transactionID, + reportID, + backTo ? 'confirm' : '', + currency, + Navigation.getActiveRouteWithoutParams(), + policy?.id || PolicyUtils.getPersonalPolicy()?.id, + ), ); }; From bd0d2bc223eb8cc02a207e76735489d8f68a3821 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 17 Jul 2024 18:11:49 +0700 Subject: [PATCH 020/230] fix lint --- src/pages/iou/request/step/IOURequestStepAmount.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index ba8d73d7dd74..e7eca6cb7643 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -143,7 +143,7 @@ function IOURequestStepAmount({ backTo ? 'confirm' : '', currency, Navigation.getActiveRouteWithoutParams(), - policy?.id || PolicyUtils.getPersonalPolicy()?.id, + policy?.id ?? PolicyUtils.getPersonalPolicy()?.id, ), ); }; From 4e0eb11726596f3ee672a87c02e135fbceef3560 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 30 Jul 2024 16:57:27 +0700 Subject: [PATCH 021/230] fix: only display 5 recently used options --- src/components/CurrencySelectionList/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 53a64e39b40d..6148d5f084b2 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -5,6 +5,7 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; @@ -44,11 +45,16 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, const shouldDisplayRecentlyOptions = !isEmptyObject(policyRecentlyUsedCurrencyOptions) && !searchValue; if (shouldDisplayRecentlyOptions) { + const cutPolicyRecentlyUsedCurrencyOptions = policyRecentlyUsedCurrencyOptions.slice(0, CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW); return { sections: isEmpty ? [] : [ - {title: translate('common.recents'), data: policyRecentlyUsedCurrencyOptions, shouldShow: shouldDisplayRecentlyOptions}, + { + title: translate('common.recents'), + data: cutPolicyRecentlyUsedCurrencyOptions, + shouldShow: shouldDisplayRecentlyOptions, + }, {title: translate('common.all'), data: filteredCurrencies}, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', From f1b4e20876862b5449f503272bf9481baba7925d Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 1 Aug 2024 23:33:31 +0700 Subject: [PATCH 022/230] reapply changes --- .../EmojiPicker/EmojiPickerButton.tsx | 9 ++++- .../ComposerWithSuggestions.tsx | 33 ++++++++++------ .../report/ReportActionItemMessageEdit.tsx | 39 ++++++++++++++----- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerButton.tsx b/src/components/EmojiPicker/EmojiPickerButton.tsx index 6e0944e5a913..412c6655bdb0 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.tsx +++ b/src/components/EmojiPicker/EmojiPickerButton.tsx @@ -1,5 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import React, {memo, useEffect, useRef} from 'react'; +import type {GestureResponderEvent} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -21,6 +22,9 @@ type EmojiPickerButtonProps = { /** Unique id for emoji picker */ emojiPickerID?: string; + /** A callback function when the button is pressed */ + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; + /** Emoji popup anchor offset shift vertical */ shiftVertical?: number; @@ -29,7 +33,7 @@ type EmojiPickerButtonProps = { onEmojiSelected: EmojiPickerAction.OnEmojiSelected; }; -function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shiftVertical = 0, onModalHide, onEmojiSelected}: EmojiPickerButtonProps) { +function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shiftVertical = 0, onPress, onModalHide, onEmojiSelected}: EmojiPickerButtonProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); @@ -44,7 +48,7 @@ function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shi ref={emojiPopoverAnchor} style={({hovered, pressed}) => [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} disabled={isDisabled} - onPress={() => { + onPress={(e) => { if (!isFocused) { return; } @@ -64,6 +68,7 @@ function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shi } else { EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } + onPress?.(e); }} id={id} accessibilityLabel={translate('reportActionCompose.emoji')} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 19efb2c2968e..804c94d06299 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -555,16 +555,22 @@ function ComposerWithSuggestions( focusComposerWithDelay(textInputRef.current)(shouldDelay); }, []); - const setUpComposeFocusManager = useCallback(() => { - // This callback is used in the contextMenuActions to manage giving focus back to the compose input. - ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => { - if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused) { - return; - } + /** + * Set focus callback + * @param shouldTakeOverFocus - Whether this composer should gain focus priority + */ + const setUpComposeFocusManager = useCallback( + (shouldTakeOverFocus = false) => { + ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => { + if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused) { + return; + } - focus(false); - }, true); - }, [focus, isFocused]); + focus(false); + }, shouldTakeOverFocus); + }, + [focus, isFocused], + ); /** * Check if the composer is visible. Returns true if the composer is not covered up by emoji picker or menu. False otherwise. @@ -627,7 +633,7 @@ function ComposerWithSuggestions( setUpComposeFocusManager(); return () => { - ReportActionComposeFocusManager.clear(true); + ReportActionComposeFocusManager.clear(); KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress); unsubscribeNavigationBlur(); @@ -759,7 +765,11 @@ function ComposerWithSuggestions( textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} maxLines={maxComposerLines} - onFocus={onFocus} + onFocus={() => { + // The last composer that had focus should re-gain focus + setUpComposeFocusManager(true); + onFocus(); + }} onBlur={onBlur} onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={(file) => { @@ -768,7 +778,6 @@ function ComposerWithSuggestions( }} onClear={onClear} isDisabled={isBlockedFromConcierge || disabled} - isReportActionCompose selection={selection} onSelectionChange={onSelectionChange} isFullComposerAvailable={isFullComposerAvailable} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 0ca9cf0b1a3a..54c35f408648 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -28,7 +28,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; -import type {Selection} from '@libs/focusComposerWithDelay/types'; +import type {FocusComposerWithDelay, Selection} from '@libs/focusComposerWithDelay/types'; import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; import onyxSubscribe from '@libs/onyxSubscribe'; import Parser from '@libs/Parser'; @@ -170,6 +170,29 @@ function ReportActionItemMessageEdit( [action.reportActionID], ); + /** + * Focus the composer text input + * @param shouldDelay - Impose delay before focusing the composer + */ + const focus = useCallback((shouldDelay = false, forcedSelectionRange?: Selection) => { + focusComposerWithDelay(textInputRef.current)(shouldDelay, forcedSelectionRange); + }, []); + + // Take over focus priority + const setUpComposeFocusManager = useCallback(() => { + ReportActionComposeFocusManager.onComposerFocus(() => { + focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined); + }, true); + }, [focus]); + + useEffect( + // Remove focus callback on unmount to avoid stale callbacks + () => () => { + ReportActionComposeFocusManager.clear(true); + }, + [], + ); + useEffect( () => { if (isInitialMount.current) { @@ -271,8 +294,9 @@ function ReportActionItemMessageEdit( Report.deleteReportActionDraft(reportID, action); if (isActive()) { - ReportActionComposeFocusManager.clear(); - ReportActionComposeFocusManager.focus(); + ReportActionComposeFocusManager.clear(true); + // Wait for report action compose re-mounting on mWeb + InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus()); } // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report. @@ -421,11 +445,6 @@ function ReportActionItemMessageEdit( [], ); - /** - * Focus the composer text input - */ - const focus = focusComposerWithDelay(textInputRef.current); - useEffect(() => { validateCommentMaxLength(draft, {reportID}); }, [draft, reportID, validateCommentMaxLength]); @@ -500,6 +519,8 @@ function ReportActionItemMessageEdit( }); }); setShouldShowComposeInputKeyboardAware(false); + // The last composer that had focus should re-gain focus + setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(action.reportActionID)) { @@ -543,7 +564,7 @@ function ReportActionItemMessageEdit( { - focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined); + ReportActionComposeFocusManager.focus(); }} onEmojiSelected={addEmojiToTextBox} id={emojiButtonID} From fe16fc02f50104e8970aa0774e2ed5f988c8b924 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 1 Aug 2024 23:34:29 +0700 Subject: [PATCH 023/230] fix lint --- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 54c35f408648..c0e74fc947e5 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -28,7 +28,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; -import type {FocusComposerWithDelay, Selection} from '@libs/focusComposerWithDelay/types'; +import type {Selection} from '@libs/focusComposerWithDelay/types'; import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; import onyxSubscribe from '@libs/onyxSubscribe'; import Parser from '@libs/Parser'; From f6b25c8e48a76b269cca0e20eacdf9ac273c0474 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 2 Aug 2024 11:15:29 +0700 Subject: [PATCH 024/230] focus to edit composer if press its emoji picker --- src/pages/home/report/ReportActionItemMessageEdit.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index c0e74fc947e5..90c932223eb8 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -569,6 +569,7 @@ function ReportActionItemMessageEdit( onEmojiSelected={addEmojiToTextBox} id={emojiButtonID} emojiPickerID={action.reportActionID} + onPress={setUpComposeFocusManager} /> From dd91aa32a96dfb08fcf658ea0c088ba331c530c1 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 5 Aug 2024 17:09:45 +0700 Subject: [PATCH 025/230] feat: move selected option to top --- .../CurrencySelectionList/index.tsx | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 6148d5f084b2..992826212416 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -13,7 +13,7 @@ import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelection function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList, policyRecentlyUsedCurrencies}: CurrencySelectionListProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); - + const getUnselectedOptions = useCallback((options: CurrencyListItem[]) => options.filter((option) => !option.isSelected), []); const {sections, headerMessage} = useMemo(() => { const currencyOptions: CurrencyListItem[] = Object.entries(currencyList ?? {}).map(([currencyCode, currencyInfo]) => { const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode; @@ -43,34 +43,35 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, const filteredCurrencies = currencyOptions.filter((currencyOption) => searchRegex.test(currencyOption.text ?? '') || searchRegex.test(currencyOption.currencyName)); const isEmpty = searchValue.trim() && !filteredCurrencies.length; const shouldDisplayRecentlyOptions = !isEmptyObject(policyRecentlyUsedCurrencyOptions) && !searchValue; - + const selectedOption = currencyOptions.find((option) => option.isSelected); + const shouldDisplaySelectedOptionOnTop = selectedOption && !searchValue; + let sections = []; + if (shouldDisplaySelectedOptionOnTop) { + sections.push({ + title: '', + data: [selectedOption], + shouldShow: true, + }); + } if (shouldDisplayRecentlyOptions) { const cutPolicyRecentlyUsedCurrencyOptions = policyRecentlyUsedCurrencyOptions.slice(0, CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW); - return { - sections: isEmpty - ? [] - : [ - { - title: translate('common.recents'), - data: cutPolicyRecentlyUsedCurrencyOptions, - shouldShow: shouldDisplayRecentlyOptions, - }, - {title: translate('common.all'), data: filteredCurrencies}, - ], - headerMessage: isEmpty ? translate('common.noResultsFound') : '', - }; + if (!isEmpty) { + sections.push( + { + title: translate('common.recents'), + data: shouldDisplaySelectedOptionOnTop ? getUnselectedOptions(cutPolicyRecentlyUsedCurrencyOptions) : cutPolicyRecentlyUsedCurrencyOptions, + shouldShow: shouldDisplayRecentlyOptions, + }, + {title: translate('common.all'), data: shouldDisplayRecentlyOptions ? getUnselectedOptions(filteredCurrencies) : filteredCurrencies}, + ); + } + } else if (!isEmpty) { + sections.push({ + data: shouldDisplaySelectedOptionOnTop ? getUnselectedOptions(filteredCurrencies) : filteredCurrencies, + }); } - return { - sections: isEmpty - ? [] - : [ - { - data: filteredCurrencies, - }, - ], - headerMessage: isEmpty ? translate('common.noResultsFound') : '', - }; - }, [currencyList, searchValue, translate, initiallySelectedCurrencyCode, policyRecentlyUsedCurrencies]); + return {sections, headerMessage: isEmpty ? translate('common.noResultsFound') : ''}; + }, [currencyList, searchValue, translate, initiallySelectedCurrencyCode, policyRecentlyUsedCurrencies, getUnselectedOptions]); return ( Date: Mon, 5 Aug 2024 17:43:57 +0700 Subject: [PATCH 026/230] fix lint --- src/components/CurrencySelectionList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 992826212416..dfe7135bf7f0 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -45,7 +45,7 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, const shouldDisplayRecentlyOptions = !isEmptyObject(policyRecentlyUsedCurrencyOptions) && !searchValue; const selectedOption = currencyOptions.find((option) => option.isSelected); const shouldDisplaySelectedOptionOnTop = selectedOption && !searchValue; - let sections = []; + const sections = []; if (shouldDisplaySelectedOptionOnTop) { sections.push({ title: '', From 7d35e6a8ce7a56b2f553b1069fcf94c176e86cfb Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 12 Aug 2024 17:03:18 +0700 Subject: [PATCH 027/230] fix comment --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f80e791487fb..d5ee58dddb90 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4241,7 +4241,7 @@ function createSplitsAndOnyxData( // Add category to optimistic policy recently used categories when a participant is a workspace const optimisticPolicyRecentlyUsedCategories = isPolicyExpenseChat ? Category.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category) : []; - // Add category to optimistic policy recently used currencies when a participant is a workspace + // Add currency to optimistic policy recently used currencies when a participant is a workspace const optimisticPolicyRecentlyUsedCurrencies = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedCurrencies(participant.policyID, currency) : []; // Add tag to optimistic policy recently used tags when a participant is a workspace From b081a80c7aeb2d751da40836a8d3621c2be720c1 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 12 Aug 2024 17:20:04 +0700 Subject: [PATCH 028/230] fix lint --- src/libs/actions/Policy/Policy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 238cfcad3ba6..f61e6c6e5cb9 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2112,11 +2112,9 @@ function buildOptimisticPolicyRecentlyUsedCurrencies(policyID?: string, currency if (!policyID && !personalPolicyID) { return []; } - const policyRecentlyUsedCurrencies = policyID ? allRecentlyUsedCurrencies?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${policyID}`] : allRecentlyUsedCurrencies?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${personalPolicyID}`]; - console.log("111111111", {policyID, personalPolicyID, name: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CURRENCIES}${personalPolicyID}`}) return lodashUnion([currency], policyRecentlyUsedCurrencies); } From 63584df6606367d24c66b2c026d79bf9ef0662d8 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 12 Aug 2024 18:05:10 +0700 Subject: [PATCH 029/230] fix lint --- src/components/CurrencySelectionList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 9108310bc7c4..3342f92fb19d 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -93,7 +93,7 @@ function CurrencySelectionList({ } return {sections: result, headerMessage: isEmpty ? translate('common.noResultsFound') : ''}; - }, [currencyList, searchValue, canSelectMultiple, translate, initiallySelectedCurrencyCode, selectedCurrencies]); + }, [currencyList, searchValue, canSelectMultiple, translate, initiallySelectedCurrencyCode, selectedCurrencies, getUnselectedOptions, policyRecentlyUsedCurrencies]); return ( Date: Mon, 12 Aug 2024 14:55:48 +0000 Subject: [PATCH 030/230] use medium by default --- src/components/Button/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 126c81961cee..c192cbb0777a 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -177,7 +177,7 @@ function Button( small = false, large = false, - medium = false, + medium = true, isLoading = false, isDisabled = false, From 472dcd97c6c22a4862323d1d4809193cf074c430 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 12 Aug 2024 17:11:58 +0200 Subject: [PATCH 031/230] Implement workspace creation during paying as business --- src/components/SettlementButton.tsx | 36 ++++---- src/libs/API/parameters/PayInvoiceParams.ts | 7 ++ src/libs/actions/IOU.ts | 91 ++++++++++++++++----- 3 files changed, 92 insertions(+), 42 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index efe7aa91de18..f366c99f8c53 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -5,13 +5,11 @@ import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; -import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -157,10 +155,6 @@ function SettlementButton({ }: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - - const primaryPolicy = useMemo(() => PolicyActions.getPrimaryPolicy(activePolicyID), [activePolicyID]); - const session = useSession(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || -1}`); @@ -233,22 +227,20 @@ function SettlementButton({ }); } - if (PolicyUtils.isPolicyAdmin(primaryPolicy) && PolicyUtils.isPaidGroupPolicy(primaryPolicy)) { - buttonOptions.push({ - text: translate('iou.settleBusiness', {formattedAmount}), - icon: Expensicons.Building, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - backButtonText: translate('iou.business'), - subMenuItems: [ - { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.Cash, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), - }, - ], - }); - } + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); } if (shouldShowApproveButton) { diff --git a/src/libs/API/parameters/PayInvoiceParams.ts b/src/libs/API/parameters/PayInvoiceParams.ts index a6b9746d87bc..bd3d75b6da1f 100644 --- a/src/libs/API/parameters/PayInvoiceParams.ts +++ b/src/libs/API/parameters/PayInvoiceParams.ts @@ -5,6 +5,13 @@ type PayInvoiceParams = { reportActionID: string; paymentMethodType: PaymentMethodType; payAsBusiness: boolean; + policyID?: string; + announceChatReportID?: string; + adminsChatReportID?: string; + expenseChatReportID?: string; + announceCreatedReportActionID?: string; + adminsCreatedReportActionID?: string; + expenseCreatedReportActionID?: string; }; export default PayInvoiceParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ef5f6d6d61c0..8f45bd6e4a07 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -135,7 +135,7 @@ type UpdateMoneyRequestData = { }; type PayMoneyRequestData = { - params: PayMoneyRequestParams; + params: PayMoneyRequestParams & Partial; optimisticData: OnyxUpdate[]; successData: OnyxUpdate[]; failureData: OnyxUpdate[]; @@ -6542,7 +6542,37 @@ function getPayMoneyRequestParams( payAsBusiness?: boolean, ): PayMoneyRequestData { const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport); + const primaryPolicy = PolicyUtils.getPolicy(primaryPolicyID); + let payerPolicyID = primaryPolicyID; let chatReport = initialChatReport; + let policyParams = {}; + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && (!primaryPolicy || !PolicyUtils.isPolicyAdmin(primaryPolicy) || !PolicyUtils.isPaidGroupPolicy(primaryPolicy))) { + payerPolicyID = Policy.generatePolicyID(); + const { + optimisticData: policyOptimisticData, + failureData: policyFailureData, + successData: policySuccessData, + params, + } = Policy.buildPolicyData(currentUserEmail, true, undefined, payerPolicyID); + const {announceChatReportID, announceCreatedReportActionID, adminsChatReportID, adminsCreatedReportActionID, expenseChatReportID, expenseCreatedReportActionID} = params; + policyParams = { + policyID: payerPolicyID, + announceChatReportID, + announceCreatedReportActionID, + adminsChatReportID, + adminsCreatedReportActionID, + expenseChatReportID, + expenseCreatedReportActionID, + }; + + optimisticData.push(...policyOptimisticData); + successData.push(...policySuccessData); + failureData.push(...policyFailureData); + } if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', primaryPolicyID); @@ -6591,14 +6621,14 @@ function getPayMoneyRequestParams( lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), }; - if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && payerPolicyID) { optimisticChatReport.invoiceReceiver = { type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, - policyID: primaryPolicyID, + policyID: payerPolicyID, }; } - const optimisticData: OnyxUpdate[] = [ + optimisticData.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, @@ -6640,23 +6670,21 @@ function getPayMoneyRequestParams( key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, value: optimisticNextStep, }, - ]; + ); - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - pendingFields: { - preview: null, - reimbursed: null, - partial: null, - }, + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + pendingFields: { + preview: null, + reimbursed: null, + partial: null, }, }, - ]; + }); - const failureData: OnyxUpdate[] = [ + failureData.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, @@ -6683,7 +6711,7 @@ function getPayMoneyRequestParams( key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, value: currentNextStep, }, - ]; + ); // In case the report preview action is loaded locally, let's update it. if (optimisticReportPreviewAction) { @@ -6729,6 +6757,7 @@ function getPayMoneyRequestParams( optimisticHoldReportID, optimisticHoldActionID, optimisticHoldReportExpenseActionIDs, + ...policyParams, }, optimisticData, successData, @@ -7369,16 +7398,38 @@ function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes. optimisticData, successData, failureData, - params: {reportActionID}, + params: { + reportActionID, + policyID, + announceChatReportID, + announceCreatedReportActionID, + adminsChatReportID, + adminsCreatedReportActionID, + expenseChatReportID, + expenseCreatedReportActionID, + }, } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); - const params: PayInvoiceParams = { + let params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, paymentMethodType, payAsBusiness, }; + if (policyID) { + params = { + ...params, + policyID, + announceChatReportID, + announceCreatedReportActionID, + adminsChatReportID, + adminsCreatedReportActionID, + expenseChatReportID, + expenseCreatedReportActionID, + }; + } + API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData}); } From 6e1450a00f9c5a46bb791a0c5a1be8ff8388426c Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 14 Aug 2024 17:00:10 +0700 Subject: [PATCH 032/230] fix: dropdown menu misalign on search page --- src/components/ButtonWithDropdownMenu/index.tsx | 3 ++- src/components/ButtonWithDropdownMenu/types.ts | 3 +++ src/components/Search/SearchPageHeader.tsx | 1 + src/styles/index.ts | 6 ++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 943d6dbb5c16..7a88ce22d433 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -40,6 +40,7 @@ function ButtonWithDropdownMenu({ enterKeyEventListenerPriority = 0, wrapperStyle, useKeyboardShortcuts = false, + shouldUseStyleUtilityForAnchorPosition = false, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -170,7 +171,7 @@ function ButtonWithDropdownMenu({ }} onModalShow={onOptionsMenuShow} onItemSelected={() => setIsMenuVisible(false)} - anchorPosition={popoverAnchorPosition} + anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverButtonDropdownMenuOffset(windowWidth) : popoverAnchorPosition} anchorRef={nullCheckRef(dropdownAnchor)} withoutOverlay anchorAlignment={anchorAlignment} diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index e4b81da94942..6455a9aa76e5 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -93,6 +93,9 @@ type ButtonWithDropdownMenuProps = { /** Whether to use keyboard shortcuts for confirmation or not */ useKeyboardShortcuts?: boolean; + + /** Determines if a style utility function should be used for calculating the PopoverMenu anchor position. */ + shouldUseStyleUtilityForAnchorPosition?: boolean; }; export type { diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 7e4b69c39596..c1fa1b179334 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -295,6 +295,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa options={headerButtonsOptions} isSplitButton={false} style={styles.ml2} + shouldUseStyleUtilityForAnchorPosition /> )} diff --git a/src/styles/index.ts b/src/styles/index.ts index aaff81f1325a..44036e933a56 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3623,6 +3623,12 @@ const styles = (theme: ThemeColors) => horizontal: windowWidth - 355, } satisfies AnchorPosition), + popoverButtonDropdownMenuOffset: (windowWidth: number) => + ({ + ...getPopOverVerticalOffset(70), + horizontal: windowWidth - 20, + } satisfies AnchorPosition), + iPhoneXSafeArea: { backgroundColor: theme.inverse, flex: 1, From a9ce64aaa58fb95c07d1ddfc5805e1d3fc07abc9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Aug 2024 13:15:59 -0600 Subject: [PATCH 033/230] rm medium prop --- src/components/AnonymousReportFooter.tsx | 1 - src/components/ConfirmContent.tsx | 2 -- src/components/MapView/MapView.tsx | 1 - src/components/MapView/MapView.website.tsx | 1 - src/components/MoneyReportHeader.tsx | 4 ---- src/components/MoneyRequestHeader.tsx | 4 ---- src/components/PromotedActionsBar.tsx | 1 - src/components/ReportActionItem/IssueCardMessage.tsx | 1 - .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 1 - src/components/ReportActionItem/ReportPreview.tsx | 1 - src/components/ReportActionItem/TripRoomPreview.tsx | 1 - src/components/TaskHeaderActionButton.tsx | 1 - src/pages/ErrorPage/GenericErrorPage.tsx | 2 -- src/pages/ReportParticipantDetailsPage.tsx | 1 - src/pages/ReportParticipantsPage.tsx | 1 - src/pages/RoomMembersPage.tsx | 2 -- src/pages/home/HeaderView.tsx | 1 - src/pages/home/sidebar/SignInButton.tsx | 1 - .../iou/request/step/IOURequestStepScan/index.native.tsx | 1 - src/pages/iou/request/step/IOURequestStepScan/index.tsx | 2 -- .../settings/Subscription/SaveWithExpensifyButton/index.tsx | 1 - src/pages/settings/Wallet/ExpensifyCardPage.tsx | 2 -- src/pages/signin/UnlinkLoginForm.tsx | 1 - src/pages/workspace/WorkspaceMembersPage.tsx | 1 - src/pages/workspace/WorkspaceProfilePage.tsx | 2 -- src/pages/workspace/WorkspacesListPage.tsx | 1 - src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx | 1 - src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 -- src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx | 2 -- src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx | 1 - .../expensifyCard/WorkspaceExpensifyCardListPage.tsx | 2 -- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 2 -- .../workspace/reportFields/ReportFieldsListValuesPage.tsx | 1 - .../workspace/reportFields/WorkspaceReportFieldsPage.tsx | 1 - src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 -- src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 2 -- 36 files changed, 54 deletions(-) diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 078b850de5ff..b9f074e887ce 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -48,7 +48,6 @@ function AnonymousReportFooter({isSmallSizeLayout = false, report, policy}: Anon