From 3ecb3ef3587f347c41e4a7651854bab14cc3701d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 28 Sep 2023 13:54:25 +0100 Subject: [PATCH 01/50] update index.js --- .../HTMLRenderers/PreRenderer/index.js | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index efc9e432cba8..e4aec1446135 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -1,70 +1,66 @@ -import React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import _ from 'underscore'; -import withLocalize from '../../../withLocalize'; + +import ControlSelection from '../../../../libs/ControlSelection'; +import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; -import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; -import ControlSelection from '../../../../libs/ControlSelection'; - -class PreRenderer extends React.Component { - constructor(props) { - super(props); - this.scrollNode = this.scrollNode.bind(this); - this.debouncedIsScrollingVertically = _.debounce(this.isScrollingVertically.bind(this), 100, true); - } +const isScrollingVertically = (event) => + // Mark as vertical scrolling only when absolute value of deltaY is more than the double of absolute + // value of deltaX, so user can use trackpad scroll on the code block horizontally at a wide angle. + Math.abs(event.deltaY) > Math.abs(event.deltaX) * 2; - componentDidMount() { - if (!this.ref) { - return; - } - this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode); - } +const debouncedIsScrollingVertically = (event) => _.debounce(isScrollingVertically(event), 100, true); - componentWillUnmount() { - this.ref.getScrollableNode().removeEventListener('wheel', this.scrollNode); - } +function PreRenderer(props) { + const scrollViewRef = useRef(); /** - * Check if user is scrolling vertically based on deltaX and deltaY. We debounce this - * method in the constructor to make sure it's called only for the first event. + * Checks if user is scrolling vertically based on deltaX and deltaY. We debounce this + * method in order to make sure it's called only for the first event. * @param {WheelEvent} event Wheel event * @returns {Boolean} true if user is scrolling vertically */ - isScrollingVertically(event) { - // Mark as vertical scrolling only when absolute value of deltaY is more than the double of absolute - // value of deltaX, so user can use trackpad scroll on the code block horizontally at a wide angle. - return Math.abs(event.deltaY) > Math.abs(event.deltaX) * 2; - } /** * Manually scrolls the code block if code block horizontal scrollable, then prevents the event from being passed up to the parent. * @param {Object} event native event */ - scrollNode(event) { - const node = this.ref.getScrollableNode(); + const scrollNode = useCallback((event) => { + const node = scrollViewRef.current.getScrollableNode(); const horizontalOverflow = node.scrollWidth > node.offsetWidth; - const isScrollingVertically = this.debouncedIsScrollingVertically(event); - if (event.currentTarget === node && horizontalOverflow && !isScrollingVertically) { + if (event.currentTarget === node && horizontalOverflow && !debouncedIsScrollingVertically(event)) { node.scrollLeft += event.deltaX; event.preventDefault(); event.stopPropagation(); } - } + }, []); + + useEffect(() => { + const eventListenerRefValue = scrollViewRef.current; + if (!eventListenerRefValue) { + return; + } + eventListenerRefValue.getScrollableNode().addEventListener('wheel', scrollNode); + + return () => { + eventListenerRefValue.getScrollableNode().removeEventListener('wheel', scrollNode); + }; + }, [scrollNode]); - render() { - return ( - (this.ref = el)} - onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - /> - ); - } + return ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={ControlSelection.unblock} + /> + ); } PreRenderer.propTypes = htmlRendererPropTypes; +PreRenderer.displayName = 'PreRenderer'; -export default withLocalize(PreRenderer); +export default PreRenderer; \ No newline at end of file From eb52c3e5d55cc62c2a50d4ce067ff3caf3d92a62 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 07:28:32 +0100 Subject: [PATCH 02/50] migrate clipboard to TS --- src/libs/Clipboard/index.native.js | 18 ----- src/libs/Clipboard/index.native.ts | 19 ++++++ src/libs/Clipboard/{index.js => index.ts} | 83 +++++++++++++++-------- src/libs/Clipboard/types.ts | 9 +++ src/types/modules/react-native-web.d.ts | 7 ++ 5 files changed, 88 insertions(+), 48 deletions(-) delete mode 100644 src/libs/Clipboard/index.native.js create mode 100644 src/libs/Clipboard/index.native.ts rename src/libs/Clipboard/{index.js => index.ts} (53%) create mode 100644 src/libs/Clipboard/types.ts create mode 100644 src/types/modules/react-native-web.d.ts diff --git a/src/libs/Clipboard/index.native.js b/src/libs/Clipboard/index.native.js deleted file mode 100644 index d6345ac94a36..000000000000 --- a/src/libs/Clipboard/index.native.js +++ /dev/null @@ -1,18 +0,0 @@ -import Clipboard from '@react-native-community/clipboard'; - -/** - * Sets a string on the Clipboard object via @react-native-community/clipboard - * - * @param {String} text - */ -const setString = (text) => { - Clipboard.setString(text); -}; - -export default { - setString, - - // We don't want to set HTML on native platforms so noop them. - canSetHtml: () => false, - setHtml: () => {}, -}; diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts new file mode 100644 index 000000000000..bc7da7ad9d3f --- /dev/null +++ b/src/libs/Clipboard/index.native.ts @@ -0,0 +1,19 @@ +import RNCClipboard from '@react-native-community/clipboard'; +import {SetString, Clipboard} from './types'; + +/** + * Sets a string on the Clipboard object via @react-native-community/clipboard + */ +const setString: SetString = (text) => { + RNCClipboard.setString(text); +}; + +const clipboard: Clipboard = { + setString, + + // We don't want to set HTML on native platforms so noop them. + canSetHtml: () => false, + setHtml: () => {}, +}; + +export default clipboard; diff --git a/src/libs/Clipboard/index.js b/src/libs/Clipboard/index.ts similarity index 53% rename from src/libs/Clipboard/index.js rename to src/libs/Clipboard/index.ts index b770b2f2c787..fe0515edc585 100644 --- a/src/libs/Clipboard/index.js +++ b/src/libs/Clipboard/index.ts @@ -1,17 +1,37 @@ // on Web/desktop this import will be replaced with `react-native-web` -import {Clipboard} from 'react-native-web'; -import lodashGet from 'lodash/get'; +import {Clipboard as RNWClipboard} from 'react-native-web'; import CONST from '../../CONST'; import * as Browser from '../Browser'; +import {SetString, Clipboard} from './types'; -const canSetHtml = () => lodashGet(navigator, 'clipboard.write'); +type ComposerSelection = { + start: number; + end: number; + direction: 'forward' | 'backward' | 'none'; +}; + +type AnchorSelection = { + anchorOffset: number; + focusOffset: number; + anchorNode: Node; + focusNode: Node; +}; + +type Nullable = {[K in keyof T]: T[K] | null}; +type OriginalSelection = ComposerSelection | Partial>; + +/* +* @param {this: void} object The object to query. +*/ + +const canSetHtml = () => (...args: ClipboardItems) => navigator?.clipboard?.write([...args]); /** * Deprecated method to write the content as HTML to clipboard. - * @param {String} html HTML representation - * @param {String} text Plain text representation + * @param HTML representation + * @param Plain text representation */ -function setHTMLSync(html, text) { +function setHTMLSync(html: string, text: string) { const node = document.createElement('span'); node.textContent = html; node.style.all = 'unset'; @@ -22,16 +42,16 @@ function setHTMLSync(html, text) { node.addEventListener('copy', (e) => { e.stopPropagation(); e.preventDefault(); - e.clipboardData.clearData(); - e.clipboardData.setData('text/html', html); - e.clipboardData.setData('text/plain', text); + e.clipboardData?.clearData(); + e.clipboardData?.setData('text/html', html); + e.clipboardData?.setData('text/plain', text); }); document.body.appendChild(node); - const selection = window.getSelection(); - const firstAnchorChild = selection.anchorNode && selection.anchorNode.firstChild; + const selection = window?.getSelection(); + const firstAnchorChild = selection?.anchorNode?.firstChild; const isComposer = firstAnchorChild instanceof HTMLTextAreaElement; - let originalSelection = null; + let originalSelection: OriginalSelection | null = null; if (isComposer) { originalSelection = { start: firstAnchorChild.selectionStart, @@ -40,17 +60,17 @@ function setHTMLSync(html, text) { }; } else { originalSelection = { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, + anchorNode: selection?.anchorNode, + anchorOffset: selection?.anchorOffset, + focusNode: selection?.focusNode, + focusOffset: selection?.focusOffset, }; } - selection.removeAllRanges(); + selection?.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(node); - selection.addRange(range); + selection?.addRange(range); try { document.execCommand('copy'); @@ -59,12 +79,14 @@ function setHTMLSync(html, text) { // See https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#the-copy-command for more details. } - selection.removeAllRanges(); + selection?.removeAllRanges(); if (isComposer) { - firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction); + const composerSelection = originalSelection as ComposerSelection; + firstAnchorChild.setSelectionRange(composerSelection.start, composerSelection.end, composerSelection.direction); } else { - selection.setBaseAndExtent(originalSelection.anchorNode, originalSelection.anchorOffset, originalSelection.focusNode, originalSelection.focusOffset); + const anchorSelection = originalSelection as AnchorSelection; + selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); } document.body.removeChild(node); @@ -72,10 +94,10 @@ function setHTMLSync(html, text) { /** * Writes the content as HTML if the web client supports it. - * @param {String} html HTML representation - * @param {String} text Plain text representation + * @param HTML representation + * @param Plain text representation */ -const setHtml = (html, text) => { +const setHtml = (html: string, text: string) => { if (!html || !text) { return; } @@ -92,9 +114,10 @@ const setHtml = (html, text) => { setHTMLSync(html, text); } else { navigator.clipboard.write([ - // eslint-disable-next-line no-undef new ClipboardItem({ + // eslint-disable-next-line @typescript-eslint/naming-convention 'text/html': new Blob([html], {type: 'text/html'}), + // eslint-disable-next-line @typescript-eslint/naming-convention 'text/plain': new Blob([text], {type: 'text/plain'}), }), ]); @@ -103,15 +126,15 @@ const setHtml = (html, text) => { /** * Sets a string on the Clipboard object via react-native-web - * - * @param {String} text */ -const setString = (text) => { - Clipboard.setString(text); +const setString: SetString = (text) => { + RNWClipboard.setString(text); }; -export default { +const clipboard: Clipboard = { setString, canSetHtml, setHtml, }; + +export default clipboard; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts new file mode 100644 index 000000000000..e4b7cb1b5332 --- /dev/null +++ b/src/libs/Clipboard/types.ts @@ -0,0 +1,9 @@ +type SetString = (text: string) => void; + +type Clipboard = { + setString: SetString; + canSetHtml: () => void; + setHtml: (html: string, text: string) => void; +} + +export type {SetString, Clipboard}; \ No newline at end of file diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts new file mode 100644 index 000000000000..067e2f95e07a --- /dev/null +++ b/src/types/modules/react-native-web.d.ts @@ -0,0 +1,7 @@ +declare module 'react-native-web' { + type SetString = (text: string) => void; + + const Clipboard: { + setString: SetString; + } +} \ No newline at end of file From 82abacaa4985587dd730465d75652aa37a12b00d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 07:56:04 +0100 Subject: [PATCH 03/50] fix lint --- src/libs/Clipboard/index.ts | 9 ++++++--- src/libs/Clipboard/types.ts | 4 ++-- src/types/modules/react-native-web.d.ts | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index fe0515edc585..8c2d1be1afd5 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -21,10 +21,13 @@ type Nullable = {[K in keyof T]: T[K] | null}; type OriginalSelection = ComposerSelection | Partial>; /* -* @param {this: void} object The object to query. -*/ + * @param {this: void} object The object to query. + */ -const canSetHtml = () => (...args: ClipboardItems) => navigator?.clipboard?.write([...args]); +const canSetHtml = + () => + (...args: ClipboardItems) => + navigator?.clipboard?.write([...args]); /** * Deprecated method to write the content as HTML to clipboard. diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index e4b7cb1b5332..92a90df85e02 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -4,6 +4,6 @@ type Clipboard = { setString: SetString; canSetHtml: () => void; setHtml: (html: string, text: string) => void; -} +}; -export type {SetString, Clipboard}; \ No newline at end of file +export type {SetString, Clipboard}; diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts index 067e2f95e07a..da723e9a811d 100644 --- a/src/types/modules/react-native-web.d.ts +++ b/src/types/modules/react-native-web.d.ts @@ -1,7 +1,7 @@ declare module 'react-native-web' { type SetString = (text: string) => void; - + const Clipboard: { setString: SetString; - } -} \ No newline at end of file + }; +} From 40226f38242c79e2cf02d535af3713ce9d402d53 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 09:16:21 +0100 Subject: [PATCH 04/50] remove redundant JSDOC and Move Nullable to utils --- src/libs/Clipboard/index.ts | 15 +++------------ src/types/utils/Nullable.ts | 3 +++ 2 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 src/types/utils/Nullable.ts diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 8c2d1be1afd5..3166cab1bed9 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -3,6 +3,7 @@ import {Clipboard as RNWClipboard} from 'react-native-web'; import CONST from '../../CONST'; import * as Browser from '../Browser'; import {SetString, Clipboard} from './types'; +import Nullable from '../../types/utils/Nullable'; type ComposerSelection = { start: number; @@ -17,13 +18,8 @@ type AnchorSelection = { focusNode: Node; }; -type Nullable = {[K in keyof T]: T[K] | null}; type OriginalSelection = ComposerSelection | Partial>; -/* - * @param {this: void} object The object to query. - */ - const canSetHtml = () => (...args: ClipboardItems) => @@ -31,8 +27,6 @@ const canSetHtml = /** * Deprecated method to write the content as HTML to clipboard. - * @param HTML representation - * @param Plain text representation */ function setHTMLSync(html: string, text: string) { const node = document.createElement('span'); @@ -84,9 +78,8 @@ function setHTMLSync(html: string, text: string) { selection?.removeAllRanges(); - if (isComposer) { - const composerSelection = originalSelection as ComposerSelection; - firstAnchorChild.setSelectionRange(composerSelection.start, composerSelection.end, composerSelection.direction); + if (isComposer && 'start' in originalSelection) { + firstAnchorChild.setSelectionRange(originalSelection.start, originalSelection.end, originalSelection.direction); } else { const anchorSelection = originalSelection as AnchorSelection; selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); @@ -97,8 +90,6 @@ function setHTMLSync(html: string, text: string) { /** * Writes the content as HTML if the web client supports it. - * @param HTML representation - * @param Plain text representation */ const setHtml = (html: string, text: string) => { if (!html || !text) { diff --git a/src/types/utils/Nullable.ts b/src/types/utils/Nullable.ts new file mode 100644 index 000000000000..caba46ef5b58 --- /dev/null +++ b/src/types/utils/Nullable.ts @@ -0,0 +1,3 @@ +type Nullable = {[K in keyof T]: T[K] | null}; + +export default Nullable; From dfc7be9bd67acee2ba2253ab4afa41170e065bd9 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 09:25:30 +0100 Subject: [PATCH 05/50] Update src/types/modules/react-native-web.d.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: FΓ‘bio Henriques --- src/types/modules/react-native-web.d.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/types/modules/react-native-web.d.ts b/src/types/modules/react-native-web.d.ts index da723e9a811d..f7db951eadad 100644 --- a/src/types/modules/react-native-web.d.ts +++ b/src/types/modules/react-native-web.d.ts @@ -1,7 +1,11 @@ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ declare module 'react-native-web' { - type SetString = (text: string) => void; + class Clipboard { + static isAvailable(): boolean; + static getString(): Promise; + static setString(text: string): boolean; + } - const Clipboard: { - setString: SetString; - }; + export {Clipboard}; } From 4bf68670f7af143b6c576cc8bf83076d6cd607f4 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 4 Oct 2023 10:07:42 +0100 Subject: [PATCH 06/50] fix reviews --- src/libs/Clipboard/index.native.ts | 20 ++++++++++---------- src/libs/Clipboard/index.ts | 19 +++++++++---------- src/libs/Clipboard/types.ts | 10 +++------- src/types/utils/Nullable.ts | 3 --- 4 files changed, 22 insertions(+), 30 deletions(-) delete mode 100644 src/types/utils/Nullable.ts diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts index bc7da7ad9d3f..4a805b466d5b 100644 --- a/src/libs/Clipboard/index.native.ts +++ b/src/libs/Clipboard/index.native.ts @@ -1,19 +1,19 @@ -import RNCClipboard from '@react-native-community/clipboard'; -import {SetString, Clipboard} from './types'; +import Clipboard from '@react-native-community/clipboard'; +import {SetString, CanSetHtml, SetHtml} from './types'; /** * Sets a string on the Clipboard object via @react-native-community/clipboard */ const setString: SetString = (text) => { - RNCClipboard.setString(text); + Clipboard.setString(text); }; -const clipboard: Clipboard = { - setString, +// We don't want to set HTML on native platforms so noop them. +const canSetHtml: CanSetHtml = () => false; +const setHtml: SetHtml = () => {}; - // We don't want to set HTML on native platforms so noop them. - canSetHtml: () => false, - setHtml: () => {}, +export default { + setString, + canSetHtml, + setHtml, }; - -export default clipboard; diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 3166cab1bed9..975780f561a3 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,9 +1,8 @@ // on Web/desktop this import will be replaced with `react-native-web` -import {Clipboard as RNWClipboard} from 'react-native-web'; +import {Clipboard} from 'react-native-web'; import CONST from '../../CONST'; import * as Browser from '../Browser'; -import {SetString, Clipboard} from './types'; -import Nullable from '../../types/utils/Nullable'; +import {SetString, SetHtml, CanSetHtml} from './types'; type ComposerSelection = { start: number; @@ -18,9 +17,11 @@ type AnchorSelection = { focusNode: Node; }; -type OriginalSelection = ComposerSelection | Partial>; +type NullableObject = {[K in keyof T]: T[K] | null}; -const canSetHtml = +type OriginalSelection = ComposerSelection | Partial>; + +const canSetHtml: CanSetHtml = () => (...args: ClipboardItems) => navigator?.clipboard?.write([...args]); @@ -91,7 +92,7 @@ function setHTMLSync(html: string, text: string) { /** * Writes the content as HTML if the web client supports it. */ -const setHtml = (html: string, text: string) => { +const setHtml: SetHtml = (html: string, text: string) => { if (!html || !text) { return; } @@ -122,13 +123,11 @@ const setHtml = (html: string, text: string) => { * Sets a string on the Clipboard object via react-native-web */ const setString: SetString = (text) => { - RNWClipboard.setString(text); + Clipboard.setString(text); }; -const clipboard: Clipboard = { +export default { setString, canSetHtml, setHtml, }; - -export default clipboard; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index 92a90df85e02..1d899144a2ba 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,9 +1,5 @@ type SetString = (text: string) => void; +type SetHtml = (html: string, text: string) => void; +type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean); -type Clipboard = { - setString: SetString; - canSetHtml: () => void; - setHtml: (html: string, text: string) => void; -}; - -export type {SetString, Clipboard}; +export type {SetString, CanSetHtml, SetHtml}; diff --git a/src/types/utils/Nullable.ts b/src/types/utils/Nullable.ts deleted file mode 100644 index caba46ef5b58..000000000000 --- a/src/types/utils/Nullable.ts +++ /dev/null @@ -1,3 +0,0 @@ -type Nullable = {[K in keyof T]: T[K] | null}; - -export default Nullable; From 4ef562b24f78f214e7d7e3b830cbed899dc4e694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 13 Oct 2023 12:54:59 +0200 Subject: [PATCH 07/50] memoize useDebounce return value --- src/hooks/useDebounce.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.js index 8995a0443b85..3435a2699e93 100644 --- a/src/hooks/useDebounce.js +++ b/src/hooks/useDebounce.js @@ -1,4 +1,4 @@ -import {useEffect, useRef} from 'react'; +import {useCallback, useEffect, useRef} from 'react'; import lodashDebounce from 'lodash/debounce'; /** @@ -27,11 +27,13 @@ export default function useDebounce(func, wait, options) { return debouncedFn.cancel; }, [func, wait, leading, maxWait, trailing]); - return (...args) => { + const debounceCallback = useCallback((...args) => { const debouncedFn = debouncedFnRef.current; if (debouncedFn) { debouncedFn(...args); } - }; + }, []); + + return debounceCallback; } From 547ab90addaf9bb3db18b5527ff9a2d8c8d445c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 13 Oct 2023 15:11:18 +0200 Subject: [PATCH 08/50] reuse personalDetails prop by introducing context instead of using withOnyx --- .../PersonalDetailsContext.js | 7 + .../ReportActionCompose.js | 223 +++++++++--------- .../ReportActionCompose/SuggestionMention.js | 45 +--- 3 files changed, 131 insertions(+), 144 deletions(-) create mode 100644 src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js diff --git a/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js b/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js new file mode 100644 index 000000000000..83ce73404e2d --- /dev/null +++ b/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js @@ -0,0 +1,7 @@ +import {createContext} from 'react'; + +const PersonalDetailsContext = createContext({}); + +PersonalDetailsContext.displayName = 'PersonalDetailsContext'; + +export default PersonalDetailsContext; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index dd4d51653546..831d03d2871c 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -38,6 +38,7 @@ import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; import getDraftComment from '../../../../libs/ComposerUtils/getDraftComment'; import updatePropsPaperWorklet from '../../../../libs/updatePropsPaperWorklet'; +import PersonalDetailsContext from './PersonalDetailsContext'; const propTypes = { /** A method to call when the form is submitted */ @@ -342,121 +343,123 @@ function ReportActionCompose({ }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); return ( - - - + - {shouldShowReportRecipientLocalTime && hasReportRecipient && } - + - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} + {shouldShowReportRecipientLocalTime && hasReportRecipient && } + - {({displayFileInModal}) => ( - <> - - - { - if (isAttachmentPreviewActive) { - return; - } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); - displayFileInModal(data); - }} - /> - + setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} + > + {({displayFileInModal}) => ( + <> + + + { + if (isAttachmentPreviewActive) { + return; + } + const data = lodashGet(e, ['dataTransfer', 'items', 0]); + displayFileInModal(data); + }} + /> + + )} + + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + composerRef.current.replaceSelectionWithText(...args)} + emojiPickerID={report.reportID} + /> )} - - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} + - )} - - - - {!isSmallScreenWidth && } - - - - - + + + {!isSmallScreenWidth && } + + + + + + ); } diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 84bee9c80c7f..e8dfab4eb9ac 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -1,7 +1,6 @@ -import React, {useState, useCallback, useRef, useImperativeHandle, useEffect} from 'react'; +import React, {useState, useCallback, useRef, useImperativeHandle, useEffect, useContext} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import {withOnyx} from 'react-native-onyx'; import CONST from '../../../../CONST'; import useArrowKeyFocusManager from '../../../../hooks/useArrowKeyFocusManager'; import MentionSuggestions from '../../../../components/MentionSuggestions'; @@ -9,9 +8,8 @@ import * as UserUtils from '../../../../libs/UserUtils'; import * as Expensicons from '../../../../components/Icon/Expensicons'; import * as SuggestionsUtils from '../../../../libs/SuggestionUtils'; import useLocalize from '../../../../hooks/useLocalize'; -import ONYXKEYS from '../../../../ONYXKEYS'; -import personalDetailsPropType from '../../../personalDetailsPropType'; import * as SuggestionProps from './suggestionProps'; +import PersonalDetailsContext from './PersonalDetailsContext'; /** * Check if this piece of string looks like a mention @@ -28,9 +26,6 @@ const defaultSuggestionsValues = { }; const propTypes = { - /** Personal details of all users */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** A ref to this component */ forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), @@ -38,23 +33,11 @@ const propTypes = { }; const defaultProps = { - personalDetails: {}, forwardedRef: null, }; -function SuggestionMention({ - value, - setValue, - selection, - setSelection, - isComposerFullSize, - personalDetails, - updateComment, - composerHeight, - forwardedRef, - isAutoSuggestionPickerLarge, - measureParentContainer, -}) { +function SuggestionMention({value, setValue, selection, setSelection, isComposerFullSize, updateComment, composerHeight, forwardedRef, isAutoSuggestionPickerLarge, measureParentContainer}) { + const personalDetails = useContext(PersonalDetailsContext); const {translate} = useLocalize(); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); @@ -296,16 +279,10 @@ SuggestionMention.propTypes = propTypes; SuggestionMention.defaultProps = defaultProps; SuggestionMention.displayName = 'SuggestionMention'; -export default withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, -})( - React.forwardRef((props, ref) => ( - - )), -); +export default React.forwardRef((props, ref) => ( + +)); From 956d7175a6700caf91bbe7fd86a77ea835e8bf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 13 Oct 2023 18:50:14 +0200 Subject: [PATCH 09/50] Remove draft comment dependency from OptionRowLHNData component --- .../LHNOptionsList/OptionRowLHNData.js | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 3386dbe8c8cd..b2dcffa7b9bd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -2,14 +2,12 @@ import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import PropTypes from 'prop-types'; -import React, {useEffect, useRef, useMemo} from 'react'; +import React, {useRef, useMemo} from 'react'; import {deepEqual} from 'fast-equals'; -import {withReportCommentDrafts} from '../OnyxProvider'; import SidebarUtils from '../../libs/SidebarUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import OptionRowLHN, {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './OptionRowLHN'; -import * as Report from '../../libs/actions/Report'; import * as UserUtils from '../../libs/UserUtils'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; import * as TransactionUtils from '../../libs/TransactionUtils'; @@ -70,19 +68,7 @@ const defaultProps = { * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({ - isFocused, - fullReport, - reportActions, - personalDetails, - preferredLocale, - comment, - policy, - receiptTransactions, - parentReportActions, - transaction, - ...propsToForward -}) { +function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails, preferredLocale, policy, receiptTransactions, parentReportActions, transaction, ...propsToForward}) { const reportID = propsToForward.reportID; const parentReportAction = parentReportActions[fullReport.parentReportActionID]; @@ -109,14 +95,6 @@ function OptionRowLHNData({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); - useEffect(() => { - if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { - return; - } - Report.setReportWithDraft(reportID, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( */ export default React.memo( compose( - withReportCommentDrafts({ - propName: 'comment', - transformValue: (drafts, props) => { - const draftKey = `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${props.reportID}`; - return lodashGet(drafts, draftKey, ''); - }, - }), withOnyx({ fullReport: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, From b0fc9a48e4871b72de0522194c80cc98bca0b165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 16 Oct 2023 16:53:48 +0200 Subject: [PATCH 10/50] remove unused variable --- src/components/LHNOptionsList/OptionRowLHNData.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index b2dcffa7b9bd..b00fb65e33c7 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -69,8 +69,6 @@ const defaultProps = { * re-render if the data really changed. */ function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails, preferredLocale, policy, receiptTransactions, parentReportActions, transaction, ...propsToForward}) { - const reportID = propsToForward.reportID; - const parentReportAction = parentReportActions[fullReport.parentReportActionID]; const optionItemRef = useRef(); From d36a76282ef0aab476dc4538f8d523102844c963 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 17 Oct 2023 21:20:02 +0700 Subject: [PATCH 11/50] remove error when all workspace is deleted --- src/libs/actions/Policy.js | 87 +++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 53753e193fb1..bd234d22a826 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -66,6 +66,12 @@ Onyx.connect({ callback: (val) => (allPersonalDetails = val), }); +let reimbursementAccount; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + callback: (val) => (reimbursementAccount = val), +}); + let allRecentlyUsedCategories = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES, @@ -81,6 +87,36 @@ function updateLastAccessedWorkspace(policyID) { Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); } +/** + * Check if the user has any active free policies (aka workspaces) + * + * @param {Array} policies + * @returns {Boolean} + */ +function hasActiveFreePolicy(policies) { + const adminFreePolicies = _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); + + if (adminFreePolicies.length === 0) { + return false; + } + + if (_.some(adminFreePolicies, (policy) => !policy.pendingAction)) { + return true; + } + + if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) { + return true; + } + + if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { + return false; + } + + // If there are no add or delete pending actions the only option left is an update + // pendingAction, in which case we should return true. + return true; +} + /** * Delete the workspace * @@ -89,6 +125,10 @@ function updateLastAccessedWorkspace(policyID) { * @param {String} policyName */ function deleteWorkspace(policyID, reports, policyName) { + const filteredPolicies = _.filter(allPolicies, (policy) => policy.id !== policyID); + const hasActivePolicy = hasActiveFreePolicy(filteredPolicies); + const oldReimbursementAccount = reimbursementAccount; + const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -125,6 +165,18 @@ function deleteWorkspace(policyID, reports, policyName) { value: optimisticReportActions, }; }), + + ...(!hasActivePolicy + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + errors: null, + }, + }, + ] + : []), ]; // Restore the old report stateNum and statusNum @@ -139,6 +191,11 @@ function deleteWorkspace(policyID, reports, policyName) { oldPolicyName, }, })), + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: oldReimbursementAccount, + }, ]; // We don't need success data since the push notification will update @@ -162,36 +219,6 @@ function isAdminOfFreePolicy(policies) { return _.some(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); } -/** - * Check if the user has any active free policies (aka workspaces) - * - * @param {Array} policies - * @returns {Boolean} - */ -function hasActiveFreePolicy(policies) { - const adminFreePolicies = _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); - - if (adminFreePolicies.length === 0) { - return false; - } - - if (_.some(adminFreePolicies, (policy) => !policy.pendingAction)) { - return true; - } - - if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) { - return true; - } - - if (_.some(adminFreePolicies, (policy) => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { - return false; - } - - // If there are no add or delete pending actions the only option left is an update - // pendingAction, in which case we should return true. - return true; -} - /** * Remove the passed members from the policy employeeList * From 2cdea51fd549f0d1dc7ea3b4a94747bb3fc8b897 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 18 Oct 2023 15:01:00 +0200 Subject: [PATCH 12/50] ref: moved InlineErrorText to TS --- src/components/InlineErrorText.js | 31 ------------------------------ src/components/InlineErrorText.tsx | 19 ++++++++++++++++++ 2 files changed, 19 insertions(+), 31 deletions(-) delete mode 100644 src/components/InlineErrorText.js create mode 100644 src/components/InlineErrorText.tsx diff --git a/src/components/InlineErrorText.js b/src/components/InlineErrorText.js deleted file mode 100644 index ea701a3f6e8e..000000000000 --- a/src/components/InlineErrorText.js +++ /dev/null @@ -1,31 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from '../styles/styles'; -import Text from './Text'; - -const propTypes = { - /** Text to display */ - children: PropTypes.string.isRequired, - - /** Styling for inline error text */ - // eslint-disable-next-line react/forbid-prop-types - styles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - styles: [], -}; - -function InlineErrorText(props) { - if (_.isEmpty(props.children)) { - return null; - } - - return {props.children}; -} - -InlineErrorText.propTypes = propTypes; -InlineErrorText.defaultProps = defaultProps; -InlineErrorText.displayName = 'InlineErrorText'; -export default InlineErrorText; diff --git a/src/components/InlineErrorText.tsx b/src/components/InlineErrorText.tsx new file mode 100644 index 000000000000..109acfc1b893 --- /dev/null +++ b/src/components/InlineErrorText.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {TextStyle} from 'react-native'; +import styles from '../styles/styles'; +import Text from './Text'; + +type InlineErrorTextProps = { + children: React.ReactNode; + styles: TextStyle[]; +}; +function InlineErrorText(props: InlineErrorTextProps) { + if (!props.children) { + return null; + } + + return {props.children}; +} + +InlineErrorText.displayName = 'InlineErrorText'; +export default InlineErrorText; From 427f99168a34b73c6a795beb499cbab9b3941b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 19 Oct 2023 10:54:33 +0200 Subject: [PATCH 13/50] wrap ComposerWithSuggestions with context instead of entire ReportActionCompose --- .../ReportActionCompose.js | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 831d03d2871c..2ca3103bad93 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -343,54 +343,54 @@ function ReportActionCompose({ }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); return ( - - + + - - } + - {shouldShowReportRecipientLocalTime && hasReportRecipient && } - setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} > - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} - > - {({displayFileInModal}) => ( - <> - + {({displayFileInModal}) => ( + <> + + - { - if (isAttachmentPreviewActive) { - return; - } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); - displayFileInModal(data); - }} - /> - - )} - - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} - /> + + { + if (isAttachmentPreviewActive) { + return; + } + const data = lodashGet(e, ['dataTransfer', 'items', 0]); + displayFileInModal(data); + }} + /> + )} - + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + composerRef.current.replaceSelectionWithText(...args)} + emojiPickerID={report.reportID} /> - - - {!isSmallScreenWidth && } - - - - - - + )} + + + + {!isSmallScreenWidth && } + + + + + ); } From 1dff6dd3fd3d1582d7eae15f3e9aa668c1c3fe2e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 20 Oct 2023 12:20:53 +0700 Subject: [PATCH 14/50] using lodash instead of underscore --- src/libs/actions/Policy.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index bd234d22a826..4417d5f00853 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import filter from 'lodash/filter'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; import lodashUnion from 'lodash/union'; @@ -125,8 +126,7 @@ function hasActiveFreePolicy(policies) { * @param {String} policyName */ function deleteWorkspace(policyID, reports, policyName) { - const filteredPolicies = _.filter(allPolicies, (policy) => policy.id !== policyID); - const hasActivePolicy = hasActiveFreePolicy(filteredPolicies); + const filteredPolicies = filter(allPolicies, (policy) => policy.id !== policyID); const oldReimbursementAccount = reimbursementAccount; const optimisticData = [ @@ -166,7 +166,7 @@ function deleteWorkspace(policyID, reports, policyName) { }; }), - ...(!hasActivePolicy + ...(!hasActiveFreePolicy(filteredPolicies) ? [ { onyxMethod: Onyx.METHOD.MERGE, @@ -194,7 +194,9 @@ function deleteWorkspace(policyID, reports, policyName) { { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: oldReimbursementAccount, + value: { + errors: lodashGet(oldReimbursementAccount, 'errors', null), + }, }, ]; From 4a67723154ac01bc139a5a06e5c0ea229a6dae07 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sun, 22 Oct 2023 05:53:01 +0530 Subject: [PATCH 15/50] fix: append whitespace after emoji --- src/libs/ComposerUtils/index.ts | 5 +- .../ComposerWithSuggestions.js | 59 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 5e2a42fc65dd..987615f17695 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -32,7 +32,10 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo */ function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + if(str1.length===0||str2.length===0){ + return 0; + } + while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index e194d0870885..a1950ad2a96e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -116,6 +116,7 @@ function ComposerWithSuggestions({ return draft; }); const commentRef = useRef(value); + const lastTextRef = useRef(value); const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; @@ -194,6 +195,31 @@ function ComposerWithSuggestions({ RNTextInputReset.resetKeyboardInput(findNodeHandle(textInputRef.current)); }, [textInputRef]); + const findNewlyAddedChars = useCallback( + (prevText, newText) => { + const isTextReplace = selection.end - selection.start > 0; + const commonSuffixLength =ComposerUtils.getCommonSuffixLength(prevText, newText); + let startIndex = -1; + let endIndex = -1; + let i = 0; + + while (i < newText.length && prevText.charAt(i) === newText.charAt(i) && selection.start > i) { + i++; + } + + if (i < newText.length) { + startIndex = i; + // if text is getting pasted over find length of common suffix and subtract it from new text length + endIndex = isTextReplace ? newText.length-commonSuffixLength : i + (newText.length - prevText.length); + } + + return {startIndex, endIndex, diff: newText.substring(startIndex, endIndex)}; + }, + [selection.end, selection.start], + ); + + const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`; + const debouncedSaveReportComment = useMemo( () => _.debounce((selectedReportID, newComment) => { @@ -211,7 +237,13 @@ function ComposerWithSuggestions({ const updateComment = useCallback( (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); + const isEmojiInserted = diff.length && endIndex > startIndex && EmojiUtils.containsOnlyEmojis(diff); + const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( + isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, + preferredSkinTone, + preferredLocale, + ); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); @@ -255,16 +287,7 @@ function ComposerWithSuggestions({ debouncedBroadcastUserIsTyping(reportID); } }, - [ - debouncedUpdateFrequentlyUsedEmojis, - preferredLocale, - preferredSkinTone, - reportID, - setIsCommentEmpty, - suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, - debouncedSaveReportComment, - ], + [raiseIsScrollLikelyLayoutTriggered, findNewlyAddedChars, preferredSkinTone, preferredLocale, setIsCommentEmpty, debouncedUpdateFrequentlyUsedEmojis, suggestionsRef, reportID, debouncedSaveReportComment], ); /** @@ -313,14 +336,8 @@ function ComposerWithSuggestions({ * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { - const updatedText = shouldAddTrailSpace ? `${text} ` : text; - const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; - updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); - setSelection((prevSelection) => ({ - start: prevSelection.start + text.length + selectionSpaceLength, - end: prevSelection.start + text.length + selectionSpaceLength, - })); + (text) => { + updateComment(ComposerUtils.insertText(commentRef.current, selection, text)); }, [selection, updateComment], ); @@ -508,6 +525,10 @@ function ComposerWithSuggestions({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + lastTextRef.current = value; + }, [value]); + useImperativeHandle( forwardedRef, () => ({ From 6db20146c0d0d11ff350255c0ba8e6e02c5020b3 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sun, 22 Oct 2023 06:18:40 +0530 Subject: [PATCH 16/50] fix: prevent infinite loop --- src/libs/ComposerUtils/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 987615f17695..bf22fcb04a49 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -35,7 +35,8 @@ function getCommonSuffixLength(str1: string, str2: string): number { if(str1.length===0||str2.length===0){ return 0; } - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + const minLen = Math.min(str1.length, str2.length); + while (i>minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; From a8bd00576b2efad25380615217eb9904203dcda4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sun, 22 Oct 2023 06:24:05 +0530 Subject: [PATCH 17/50] fix: clean lint --- src/libs/ComposerUtils/index.ts | 4 ++-- .../ComposerWithSuggestions.js | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index bf22fcb04a49..3167ce851e60 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -32,11 +32,11 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo */ function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; - if(str1.length===0||str2.length===0){ + if (str1.length === 0 || str2.length === 0) { return 0; } const minLen = Math.min(str1.length, str2.length); - while (i>minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + while (i < minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index a1950ad2a96e..d8b3bc8f820a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -198,7 +198,7 @@ function ComposerWithSuggestions({ const findNewlyAddedChars = useCallback( (prevText, newText) => { const isTextReplace = selection.end - selection.start > 0; - const commonSuffixLength =ComposerUtils.getCommonSuffixLength(prevText, newText); + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); let startIndex = -1; let endIndex = -1; let i = 0; @@ -210,7 +210,7 @@ function ComposerWithSuggestions({ if (i < newText.length) { startIndex = i; // if text is getting pasted over find length of common suffix and subtract it from new text length - endIndex = isTextReplace ? newText.length-commonSuffixLength : i + (newText.length - prevText.length); + endIndex = isTextReplace ? newText.length - commonSuffixLength : i + (newText.length - prevText.length); } return {startIndex, endIndex, diff: newText.substring(startIndex, endIndex)}; @@ -287,7 +287,17 @@ function ComposerWithSuggestions({ debouncedBroadcastUserIsTyping(reportID); } }, - [raiseIsScrollLikelyLayoutTriggered, findNewlyAddedChars, preferredSkinTone, preferredLocale, setIsCommentEmpty, debouncedUpdateFrequentlyUsedEmojis, suggestionsRef, reportID, debouncedSaveReportComment], + [ + raiseIsScrollLikelyLayoutTriggered, + findNewlyAddedChars, + preferredSkinTone, + preferredLocale, + setIsCommentEmpty, + debouncedUpdateFrequentlyUsedEmojis, + suggestionsRef, + reportID, + debouncedSaveReportComment, + ], ); /** From 228aa5a847f54713ac2b928894ac7417605e32f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 23 Oct 2023 16:09:14 +0200 Subject: [PATCH 18/50] remove obsolete solution with context --- .../PersonalDetailsContext.js | 7 --- .../ReportActionCompose.js | 55 +++++++++---------- .../ReportActionCompose/SuggestionMention.js | 4 +- 3 files changed, 27 insertions(+), 39 deletions(-) delete mode 100644 src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js diff --git a/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js b/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js deleted file mode 100644 index 83ce73404e2d..000000000000 --- a/src/pages/home/report/ReportActionCompose/PersonalDetailsContext.js +++ /dev/null @@ -1,7 +0,0 @@ -import {createContext} from 'react'; - -const PersonalDetailsContext = createContext({}); - -PersonalDetailsContext.displayName = 'PersonalDetailsContext'; - -export default PersonalDetailsContext; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 2ca3103bad93..dd4d51653546 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -38,7 +38,6 @@ import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; import getDraftComment from '../../../../libs/ComposerUtils/getDraftComment'; import updatePropsPaperWorklet from '../../../../libs/updatePropsPaperWorklet'; -import PersonalDetailsContext from './PersonalDetailsContext'; const propTypes = { /** A method to call when the form is submitted */ @@ -390,34 +389,32 @@ function ReportActionCompose({ onItemSelected={onItemSelected} actionButtonRef={actionButtonRef} /> - - - + { if (isAttachmentPreviewActive) { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index e8dfab4eb9ac..d3f97fd501c9 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -1,4 +1,4 @@ -import React, {useState, useCallback, useRef, useImperativeHandle, useEffect, useContext} from 'react'; +import React, {useState, useCallback, useRef, useImperativeHandle, useEffect} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import CONST from '../../../../CONST'; @@ -9,7 +9,6 @@ import * as Expensicons from '../../../../components/Icon/Expensicons'; import * as SuggestionsUtils from '../../../../libs/SuggestionUtils'; import useLocalize from '../../../../hooks/useLocalize'; import * as SuggestionProps from './suggestionProps'; -import PersonalDetailsContext from './PersonalDetailsContext'; /** * Check if this piece of string looks like a mention @@ -37,7 +36,6 @@ const defaultProps = { }; function SuggestionMention({value, setValue, selection, setSelection, isComposerFullSize, updateComment, composerHeight, forwardedRef, isAutoSuggestionPickerLarge, measureParentContainer}) { - const personalDetails = useContext(PersonalDetailsContext); const {translate} = useLocalize(); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); From ff1592d05f309ab56c07a72f23ba0aac767dc6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 23 Oct 2023 16:19:23 +0200 Subject: [PATCH 19/50] use solution from https://github.com/Expensify/App/pull/30063 --- src/components/OnyxProvider.tsx | 3 ++- src/components/createOnyxContext.tsx | 19 ++++++++++++++++--- .../ReportActionCompose/SuggestionMention.js | 3 ++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index 3bd4ca52c3be..8682e832debc 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -6,7 +6,7 @@ import ComposeProviders from './ComposeProviders'; // Set up any providers for individual keys. This should only be used in cases where many components will subscribe to // the same key (e.g. FlatList renderItem components) const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK); -const [withPersonalDetails, PersonalDetailsProvider] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST); +const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST); const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE); const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); @@ -45,6 +45,7 @@ export default OnyxProvider; export { withNetwork, withPersonalDetails, + usePersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, diff --git a/src/components/createOnyxContext.tsx b/src/components/createOnyxContext.tsx index d142e551012f..a0ac5942b098 100644 --- a/src/components/createOnyxContext.tsx +++ b/src/components/createOnyxContext.tsx @@ -1,4 +1,4 @@ -import React, {ComponentType, ForwardRefExoticComponent, ForwardedRef, PropsWithoutRef, ReactNode, RefAttributes, createContext, forwardRef} from 'react'; +import React, {ComponentType, ForwardRefExoticComponent, ForwardedRef, PropsWithoutRef, ReactNode, RefAttributes, createContext, forwardRef, useContext} from 'react'; import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import getComponentDisplayName from '../libs/getComponentDisplayName'; @@ -29,7 +29,12 @@ type WithOnyxKey = WrapComponentWithConsumer; // createOnyxContext return type -type CreateOnyxContext = [WithOnyxKey, ComponentType, TOnyxKey>>, React.Context>]; +type CreateOnyxContext = [ + WithOnyxKey, + ComponentType, TOnyxKey>>, + React.Context>, + () => OnyxValues[TOnyxKey], +]; export default (onyxKeyName: TOnyxKey): CreateOnyxContext => { const Context = createContext>(null); @@ -77,5 +82,13 @@ export default (onyxKeyName: TOnyxKey): CreateOnyxCon }; } - return [withOnyxKey, ProviderWithOnyx, Context]; + const useOnyxContext = () => { + const value = useContext(Context); + if (value === null) { + throw new Error(`useOnyxContext must be used within a OnyxProvider [key: ${onyxKeyName}]`); + } + return value; + }; + + return [withOnyxKey, ProviderWithOnyx, Context, useOnyxContext]; }; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index b3847eb59ff2..59db74cd9b11 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -9,6 +9,7 @@ import * as Expensicons from '../../../../components/Icon/Expensicons'; import * as SuggestionsUtils from '../../../../libs/SuggestionUtils'; import useLocalize from '../../../../hooks/useLocalize'; import * as SuggestionProps from './suggestionProps'; +import {usePersonalDetails} from '../../../../components/OnyxProvider'; /** * Check if this piece of string looks like a mention @@ -41,7 +42,6 @@ function SuggestionMention({ selection, setSelection, isComposerFullSize, - personalDetails, updateComment, composerHeight, forwardedRef, @@ -49,6 +49,7 @@ function SuggestionMention({ measureParentContainer, isComposerFocused, }) { + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const {translate} = useLocalize(); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); From 9f5b80e493594a2b2bd7cacf90783e775e848704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 23 Oct 2023 16:31:23 +0200 Subject: [PATCH 20/50] ?? -> || --- src/pages/home/report/ReportActionCompose/SuggestionMention.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 9a7c5fc65e04..9be3b67d584e 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -50,7 +50,7 @@ function SuggestionMention({ measureParentContainer, isComposerFocused, }) { - const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const {translate} = useLocalize(); const previousValue = usePrevious(value); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); From f06f66f6693a7b8a2015d202b8d6bb7f10181a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 23 Oct 2023 17:10:54 +0200 Subject: [PATCH 21/50] Revert "Remove draft comment dependency from OptionRowLHNData component" This reverts commit 956d7175a6700caf91bbe7fd86a77ea835e8bf18. --- .../LHNOptionsList/OptionRowLHNData.js | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index b00fb65e33c7..3386dbe8c8cd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -2,12 +2,14 @@ import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import PropTypes from 'prop-types'; -import React, {useRef, useMemo} from 'react'; +import React, {useEffect, useRef, useMemo} from 'react'; import {deepEqual} from 'fast-equals'; +import {withReportCommentDrafts} from '../OnyxProvider'; import SidebarUtils from '../../libs/SidebarUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import OptionRowLHN, {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './OptionRowLHN'; +import * as Report from '../../libs/actions/Report'; import * as UserUtils from '../../libs/UserUtils'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; import * as TransactionUtils from '../../libs/TransactionUtils'; @@ -68,7 +70,21 @@ const defaultProps = { * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails, preferredLocale, policy, receiptTransactions, parentReportActions, transaction, ...propsToForward}) { +function OptionRowLHNData({ + isFocused, + fullReport, + reportActions, + personalDetails, + preferredLocale, + comment, + policy, + receiptTransactions, + parentReportActions, + transaction, + ...propsToForward +}) { + const reportID = propsToForward.reportID; + const parentReportAction = parentReportActions[fullReport.parentReportActionID]; const optionItemRef = useRef(); @@ -93,6 +109,14 @@ function OptionRowLHNData({isFocused, fullReport, reportActions, personalDetails // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); + useEffect(() => { + if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { + return; + } + Report.setReportWithDraft(reportID, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( */ export default React.memo( compose( + withReportCommentDrafts({ + propName: 'comment', + transformValue: (drafts, props) => { + const draftKey = `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${props.reportID}`; + return lodashGet(drafts, draftKey, ''); + }, + }), withOnyx({ fullReport: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, From b8a97da49ea9ecbfcfcdf61959ec300be462d825 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 24 Oct 2023 12:27:52 +0700 Subject: [PATCH 22/50] remove un-use variable --- src/libs/actions/Policy.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 7f4d3b72e81f..1878ab6e71b3 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -134,8 +134,6 @@ function hasActiveFreePolicy(policies) { */ function deleteWorkspace(policyID, reports, policyName) { const filteredPolicies = filter(allPolicies, (policy) => policy.id !== policyID); - const oldReimbursementAccount = reimbursementAccount; - const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -202,7 +200,7 @@ function deleteWorkspace(policyID, reports, policyName) { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: { - errors: lodashGet(oldReimbursementAccount, 'errors', null), + errors: lodashGet(reimbursementAccount, 'errors', null), }, }, ]; From ec3284d9e078d05e72f23eee3ab0578be9643d56 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 26 Oct 2023 07:27:32 +0530 Subject: [PATCH 23/50] fix: remove extra white space getting added --- src/libs/EmojiUtils.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js index 05ad1bd3c2ce..9f3931e59530 100644 --- a/src/libs/EmojiUtils.js +++ b/src/libs/EmojiUtils.js @@ -360,19 +360,13 @@ function replaceEmojis(text, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, } } if (checkEmoji && checkEmoji.metaData.code) { - let emojiReplacement = getEmojiCodeWithSkinColor(checkEmoji.metaData, preferredSkinTone); + const emojiReplacement = getEmojiCodeWithSkinColor(checkEmoji.metaData, preferredSkinTone); emojis.push({ name, code: checkEmoji.metaData.code, types: checkEmoji.metaData.types, }); - // If this is the last emoji in the message and it's the end of the message so far, - // add a space after it so the user can keep typing easily. - if (i === emojiData.length - 1) { - emojiReplacement += ' '; - } - newText = newText.replace(emojiData[i], emojiReplacement); } } From 51406fcd2abd28cf9de862c072a8d7c1b5c7efad Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 26 Oct 2023 07:27:43 +0530 Subject: [PATCH 24/50] refactor and add comments --- .../ComposerWithSuggestions.js | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 183152088f8d..bcfeb3d74cf9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -196,25 +196,37 @@ function ComposerWithSuggestions({ RNTextInputReset.resetKeyboardInput(findNodeHandle(textInputRef.current)); }, [textInputRef]); + /** + * Find diff between text changes in composer + */ const findNewlyAddedChars = useCallback( (prevText, newText) => { - const isTextReplace = selection.end - selection.start > 0; - const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); let startIndex = -1; let endIndex = -1; - let i = 0; + let currentIndex = 0; - while (i < newText.length && prevText.charAt(i) === newText.charAt(i) && selection.start > i) { - i++; + // Find the first character mismatch with newText + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { + currentIndex++; } - if (i < newText.length) { - startIndex = i; + if (currentIndex < newText.length) { + startIndex = currentIndex; + // if text is getting pasted over find length of common suffix and subtract it from new text length - endIndex = isTextReplace ? newText.length - commonSuffixLength : i + (newText.length - prevText.length); + if (selection.end - selection.start > 0) { + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); + endIndex = newText.length - commonSuffixLength; + } else { + endIndex = currentIndex + (newText.length - prevText.length); + } } - return {startIndex, endIndex, diff: newText.substring(startIndex, endIndex)}; + return { + startIndex, + endIndex, + diff: newText.substring(startIndex, endIndex), + }; }, [selection.end, selection.start], ); From 12273a3bae0be54175ccad1c8978592a16e73a0d Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 26 Oct 2023 09:57:14 +0530 Subject: [PATCH 25/50] fix: remove redundant test cases --- tests/unit/EmojiTest.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index d10da618480e..d1f69b8c3384 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -101,16 +101,6 @@ describe('EmojiTest', () => { expect(EmojiUtils.containsOnlyEmojis('πŸ…ƒπŸ„΄πŸ…‚πŸ…ƒ')).toBe(false); }); - it('replaces an emoji code with an emoji and a space', () => { - const text = 'Hi :smile:'; - expect(lodashGet(EmojiUtils.replaceEmojis(text), 'text')).toBe('Hi πŸ˜„ '); - }); - - it('will add a space after the last emoji if there is text after it', () => { - const text = 'Hi :smile::wave:space after last emoji'; - expect(lodashGet(EmojiUtils.replaceEmojis(text), 'text')).toBe('Hi πŸ˜„πŸ‘‹ space after last emoji'); - }); - it('suggests emojis when typing emojis prefix after colon', () => { const text = 'Hi :coffin'; expect(EmojiUtils.suggestEmojis(text, 'en')).toEqual([{code: '⚰️', name: 'coffin'}]); From bb6bd37d2ba450d56068bd3b608d8e74bd6a2476 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 26 Oct 2023 10:00:00 +0530 Subject: [PATCH 26/50] remove unused import --- tests/unit/EmojiTest.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index d1f69b8c3384..2be35467a163 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import {getUnixTime} from 'date-fns'; import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import Emoji from '../../assets/emojis'; import * as EmojiUtils from '../../src/libs/EmojiUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; From 6fb578b18db90da362ae195f7bfe32c22c538037 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 26 Oct 2023 08:23:50 +0100 Subject: [PATCH 27/50] use react native clipboard --- src/libs/Clipboard/index.native.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts index 4a805b466d5b..f7eb43ba8033 100644 --- a/src/libs/Clipboard/index.native.ts +++ b/src/libs/Clipboard/index.native.ts @@ -1,8 +1,8 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import {SetString, CanSetHtml, SetHtml} from './types'; /** - * Sets a string on the Clipboard object via @react-native-community/clipboard + * Sets a string on the Clipboard object via @react-native-clipboard/clipboard */ const setString: SetString = (text) => { Clipboard.setString(text); From f00a383dcdcb9b017712cafa9fd2571a9d678e39 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Fri, 27 Oct 2023 20:06:35 +0530 Subject: [PATCH 28/50] fix: clean lint --- .../home/report/ReportActionCompose/ComposerWithSuggestions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index f9a50fdb8c73..3b7690482a62 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -255,7 +255,8 @@ function ComposerWithSuggestions({ const isEmojiInserted = diff.length && endIndex > startIndex && EmojiUtils.containsOnlyEmojis(diff); const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, - preferredSkinTone, preferredLocale); + preferredSkinTone, + preferredLocale, ); if (!_.isEmpty(emojis)) { From b1213cd23cc3f389abf1c4dcc57f8cf638280a49 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Fri, 27 Oct 2023 20:13:47 +0530 Subject: [PATCH 29/50] fix: prettify code --- .imgbotconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.imgbotconfig b/.imgbotconfig index 9996ba809af1..ff5c3345cc4d 100644 --- a/.imgbotconfig +++ b/.imgbotconfig @@ -1,6 +1,6 @@ { "ignoredFiles": [ - "assets/images/empty-state_background-fade.png", // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499 + "assets/images/empty-state_background-fade.png" // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499 ], "aggressiveCompression": "false" } From 9f3653ce3963edec6d275b395632a321ea3751d7 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Sat, 28 Oct 2023 14:25:50 +0530 Subject: [PATCH 30/50] fix: revert change to EmojiUtils --- src/libs/EmojiUtils.js | 8 +++++++- tests/unit/EmojiTest.js | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js index 9f3931e59530..05ad1bd3c2ce 100644 --- a/src/libs/EmojiUtils.js +++ b/src/libs/EmojiUtils.js @@ -360,13 +360,19 @@ function replaceEmojis(text, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, } } if (checkEmoji && checkEmoji.metaData.code) { - const emojiReplacement = getEmojiCodeWithSkinColor(checkEmoji.metaData, preferredSkinTone); + let emojiReplacement = getEmojiCodeWithSkinColor(checkEmoji.metaData, preferredSkinTone); emojis.push({ name, code: checkEmoji.metaData.code, types: checkEmoji.metaData.types, }); + // If this is the last emoji in the message and it's the end of the message so far, + // add a space after it so the user can keep typing easily. + if (i === emojiData.length - 1) { + emojiReplacement += ' '; + } + newText = newText.replace(emojiData[i], emojiReplacement); } } diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index 2be35467a163..d10da618480e 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import {getUnixTime} from 'date-fns'; import Onyx from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import Emoji from '../../assets/emojis'; import * as EmojiUtils from '../../src/libs/EmojiUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; @@ -100,6 +101,16 @@ describe('EmojiTest', () => { expect(EmojiUtils.containsOnlyEmojis('πŸ…ƒπŸ„΄πŸ…‚πŸ…ƒ')).toBe(false); }); + it('replaces an emoji code with an emoji and a space', () => { + const text = 'Hi :smile:'; + expect(lodashGet(EmojiUtils.replaceEmojis(text), 'text')).toBe('Hi πŸ˜„ '); + }); + + it('will add a space after the last emoji if there is text after it', () => { + const text = 'Hi :smile::wave:space after last emoji'; + expect(lodashGet(EmojiUtils.replaceEmojis(text), 'text')).toBe('Hi πŸ˜„πŸ‘‹ space after last emoji'); + }); + it('suggests emojis when typing emojis prefix after colon', () => { const text = 'Hi :coffin'; expect(EmojiUtils.suggestEmojis(text, 'en')).toEqual([{code: '⚰️', name: 'coffin'}]); From bbdcb42cceb2d1c5b455c4e18cc966fdfe3e6c5f Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 30 Oct 2023 11:22:45 +0100 Subject: [PATCH 31/50] disable lint for function --- src/libs/Clipboard/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 070d1b40e27f..f5f2eeb5db34 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -109,9 +109,8 @@ const setHtml: SetHtml = (html: string, text: string) => { } else { navigator.clipboard.write([ new ClipboardItem({ - // eslint-disable-next-line @typescript-eslint/naming-convention + /* eslint-disable @typescript-eslint/naming-convention */ 'text/html': new Blob([html], {type: 'text/html'}), - // eslint-disable-next-line @typescript-eslint/naming-convention 'text/plain': new Blob([text], {type: 'text/plain'}), }), ]); From cf823bbd51099477d1862e80bc7797d3239a5fbd Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 30 Oct 2023 14:31:07 +0100 Subject: [PATCH 32/50] fix lint --- src/libs/Clipboard/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 8bf8f23d066d..23e899c995e3 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,7 +1,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import {SetString, SetHtml, CanSetHtml} from './types'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; +import {SetString, SetHtml, CanSetHtml} from './types'; type ComposerSelection = { start: number; From 0febe75a6e6f2b6d47085c9a57351b42b95ba199 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 30 Oct 2023 14:36:40 +0100 Subject: [PATCH 33/50] fix prettier --- src/libs/Clipboard/index.native.ts | 2 +- src/libs/Clipboard/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.native.ts index f7eb43ba8033..f78c5e4ab230 100644 --- a/src/libs/Clipboard/index.native.ts +++ b/src/libs/Clipboard/index.native.ts @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import {SetString, CanSetHtml, SetHtml} from './types'; +import {CanSetHtml, SetHtml, SetString} from './types'; /** * Sets a string on the Clipboard object via @react-native-clipboard/clipboard diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 23e899c995e3..e2fc82a133a0 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,7 +1,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; -import {SetString, SetHtml, CanSetHtml} from './types'; +import {CanSetHtml, SetHtml, SetString} from './types'; type ComposerSelection = { start: number; From 42e7dbe1919d83ecd5836deafa1f4984acdb3e09 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 30 Oct 2023 15:41:46 +0100 Subject: [PATCH 34/50] fix: removed component since its not used anymore --- src/components/InlineErrorText.tsx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/components/InlineErrorText.tsx diff --git a/src/components/InlineErrorText.tsx b/src/components/InlineErrorText.tsx deleted file mode 100644 index 53c219ff8407..000000000000 --- a/src/components/InlineErrorText.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import {TextStyle} from 'react-native'; -import styles from '@styles/styles'; -import Text from './Text'; - -type InlineErrorTextProps = { - children: React.ReactNode; - styles: TextStyle[]; -}; -function InlineErrorText(props: InlineErrorTextProps) { - if (!props.children) { - return null; - } - - return {props.children}; -} - -InlineErrorText.displayName = 'InlineErrorText'; -export default InlineErrorText; From 40a6f855bc79f3b7fbba2c60c1e122922743d526 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 31 Oct 2023 11:00:08 +0530 Subject: [PATCH 35/50] fix: reset cursor to last position on focus --- .../report/ReportActionCompose/ComposerWithSuggestions.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index b55927e56edd..ee287bda07c5 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -486,7 +486,12 @@ function ComposerWithSuggestions({ } focus(); - replaceSelectionWithText(e.key, false); + // Reset cursor to last known location + setSelection((prevSelection) => ({ + start: prevSelection.start+1 , + end: prevSelection.end+1, + })); + replaceSelectionWithText(e.key); }, [checkComposerVisibility, focus, replaceSelectionWithText], ); From c66bc8cb181aac505c4dd222d273587ddfbb5b86 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 31 Oct 2023 11:02:59 +0530 Subject: [PATCH 36/50] prettify code --- .../report/ReportActionCompose/ComposerWithSuggestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index ee287bda07c5..efc0b88fe5c2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -488,8 +488,8 @@ function ComposerWithSuggestions({ focus(); // Reset cursor to last known location setSelection((prevSelection) => ({ - start: prevSelection.start+1 , - end: prevSelection.end+1, + start: prevSelection.start + 1, + end: prevSelection.end + 1, })); replaceSelectionWithText(e.key); }, From 79fbe70c33e77f47ea29b46a237c35606e24c17d Mon Sep 17 00:00:00 2001 From: someone-here Date: Tue, 31 Oct 2023 19:43:09 +0530 Subject: [PATCH 37/50] Workspace settings page form error fix --- src/pages/workspace/WorkspaceSettingsPage.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 9d1000179291..d913ae26c170 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -101,8 +101,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) {
- + Date: Wed, 1 Nov 2023 20:39:47 +0530 Subject: [PATCH 38/50] Add params for jsDoc --- .../ReportActionCompose/ComposerWithSuggestions.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index efc0b88fe5c2..b306676d476a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -198,7 +198,14 @@ function ComposerWithSuggestions({ }, [textInputRef]); /** - * Find diff between text changes in composer + * Find the newly added characters between the previous text and the new text based on the selection. + * + * @param {string} prevText - The previous text. + * @param {string} newText - The new text. + * @returns {object} An object containing information about the newly added characters. + * @property {number} startIndex - The start index of the newly added characters in the new text. + * @property {number} endIndex - The end index of the newly added characters in the new text. + * @property {string} diff - The newly added characters. */ const findNewlyAddedChars = useCallback( (prevText, newText) => { From 4acd3b4ee7daee5f294c1fde7406059ad42da042 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Wed, 1 Nov 2023 21:13:44 +0100 Subject: [PATCH 39/50] Fix XOF currency not searchable by its second term --- src/pages/iou/IOUCurrencySelection.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index c7b5885865df..95b04d50ed18 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -126,8 +126,11 @@ function IOUCurrencySelection(props) { }; }); - const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); - const filteredCurrencies = _.filter(currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text) || searchRegex.test(currencyOption.currencyName)); + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.replace(/\s/g, ' ')), 'i'); + const filteredCurrencies = _.filter( + currencyOptions, + (currencyOption) => searchRegex.test(currencyOption.text.replace(/\s/g, ' ')) || searchRegex.test(currencyOption.currencyName.replace(/\s/g, ' ')), + ); const isEmpty = searchValue.trim() && !filteredCurrencies.length; return { From fc2d0ea31e0d4575216f9e36898b499feddffc7e Mon Sep 17 00:00:00 2001 From: Yauheni Date: Wed, 1 Nov 2023 21:33:17 +0100 Subject: [PATCH 40/50] Add trim for searchRegex --- src/pages/iou/IOUCurrencySelection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 95b04d50ed18..2b342ae962c0 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -126,7 +126,7 @@ function IOUCurrencySelection(props) { }; }); - const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.replace(/\s/g, ' ')), 'i'); + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim().replace(/\s/g, ' ')), 'i'); const filteredCurrencies = _.filter( currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text.replace(/\s/g, ' ')) || searchRegex.test(currencyOption.currencyName.replace(/\s/g, ' ')), From 3f9f30fe4d0ba6e9558ab31ae997edb3c2bee49f Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 2 Nov 2023 05:51:51 +0100 Subject: [PATCH 41/50] return func if selection is null --- src/libs/Clipboard/index.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 653219ec5e9f..b703b0b4d7f5 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -18,7 +18,7 @@ type AnchorSelection = { type NullableObject = {[K in keyof T]: T[K] | null}; -type OriginalSelection = ComposerSelection | Partial>; +type OriginalSelection = ComposerSelection | NullableObject; const canSetHtml: CanSetHtml = () => @@ -46,7 +46,12 @@ function setHTMLSync(html: string, text: string) { document.body.appendChild(node); const selection = window?.getSelection(); - const firstAnchorChild = selection?.anchorNode?.firstChild; + + if (selection === null) { + return; + } + + const firstAnchorChild = selection.anchorNode?.firstChild; const isComposer = firstAnchorChild instanceof HTMLTextAreaElement; let originalSelection: OriginalSelection | null = null; if (isComposer) { @@ -57,17 +62,17 @@ function setHTMLSync(html: string, text: string) { }; } else { originalSelection = { - anchorNode: selection?.anchorNode, - anchorOffset: selection?.anchorOffset, - focusNode: selection?.focusNode, - focusOffset: selection?.focusOffset, + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, }; } - selection?.removeAllRanges(); + selection.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(node); - selection?.addRange(range); + selection.addRange(range); try { document.execCommand('copy'); @@ -76,7 +81,7 @@ function setHTMLSync(html: string, text: string) { // See https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#the-copy-command for more details. } - selection?.removeAllRanges(); + selection.removeAllRanges(); const anchorSelection = originalSelection as AnchorSelection; @@ -85,7 +90,7 @@ function setHTMLSync(html: string, text: string) { } else if (anchorSelection.anchorNode && anchorSelection.focusNode) { // When copying to the clipboard here, the original values of anchorNode and focusNode will be null since there will be no user selection. // We are adding a check to prevent null values from being passed to setBaseAndExtent, in accordance with the standards of the Selection API as outlined here: https://w3c.github.io/selection-api/#dom-selection-setbaseandextent. - selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); + selection.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); } document.body.removeChild(node); From f5838d2606d989c6780677a325faeb0b2819732b Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 2 Nov 2023 06:03:14 +0100 Subject: [PATCH 42/50] undo return func if selection is null --- src/libs/Clipboard/index.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index b703b0b4d7f5..d6427cfdc919 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -18,7 +18,7 @@ type AnchorSelection = { type NullableObject = {[K in keyof T]: T[K] | null}; -type OriginalSelection = ComposerSelection | NullableObject; +type OriginalSelection = ComposerSelection | Partial>; const canSetHtml: CanSetHtml = () => @@ -47,11 +47,7 @@ function setHTMLSync(html: string, text: string) { const selection = window?.getSelection(); - if (selection === null) { - return; - } - - const firstAnchorChild = selection.anchorNode?.firstChild; + const firstAnchorChild = selection?.anchorNode?.firstChild; const isComposer = firstAnchorChild instanceof HTMLTextAreaElement; let originalSelection: OriginalSelection | null = null; if (isComposer) { @@ -62,17 +58,17 @@ function setHTMLSync(html: string, text: string) { }; } else { originalSelection = { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, + anchorNode: selection?.anchorNode, + anchorOffset: selection?.anchorOffset, + focusNode: selection?.focusNode, + focusOffset: selection?.focusOffset, }; } - selection.removeAllRanges(); + selection?.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(node); - selection.addRange(range); + selection?.addRange(range); try { document.execCommand('copy'); @@ -81,7 +77,7 @@ function setHTMLSync(html: string, text: string) { // See https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#the-copy-command for more details. } - selection.removeAllRanges(); + selection?.removeAllRanges(); const anchorSelection = originalSelection as AnchorSelection; @@ -90,7 +86,7 @@ function setHTMLSync(html: string, text: string) { } else if (anchorSelection.anchorNode && anchorSelection.focusNode) { // When copying to the clipboard here, the original values of anchorNode and focusNode will be null since there will be no user selection. // We are adding a check to prevent null values from being passed to setBaseAndExtent, in accordance with the standards of the Selection API as outlined here: https://w3c.github.io/selection-api/#dom-selection-setbaseandextent. - selection.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); + selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); } document.body.removeChild(node); From 9618154df909b298363bfb545abb44a42f9abe5f Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 2 Nov 2023 06:13:50 +0100 Subject: [PATCH 43/50] return func if selection is null --- src/libs/Clipboard/index.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index d6427cfdc919..b703b0b4d7f5 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -18,7 +18,7 @@ type AnchorSelection = { type NullableObject = {[K in keyof T]: T[K] | null}; -type OriginalSelection = ComposerSelection | Partial>; +type OriginalSelection = ComposerSelection | NullableObject; const canSetHtml: CanSetHtml = () => @@ -47,7 +47,11 @@ function setHTMLSync(html: string, text: string) { const selection = window?.getSelection(); - const firstAnchorChild = selection?.anchorNode?.firstChild; + if (selection === null) { + return; + } + + const firstAnchorChild = selection.anchorNode?.firstChild; const isComposer = firstAnchorChild instanceof HTMLTextAreaElement; let originalSelection: OriginalSelection | null = null; if (isComposer) { @@ -58,17 +62,17 @@ function setHTMLSync(html: string, text: string) { }; } else { originalSelection = { - anchorNode: selection?.anchorNode, - anchorOffset: selection?.anchorOffset, - focusNode: selection?.focusNode, - focusOffset: selection?.focusOffset, + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, }; } - selection?.removeAllRanges(); + selection.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(node); - selection?.addRange(range); + selection.addRange(range); try { document.execCommand('copy'); @@ -77,7 +81,7 @@ function setHTMLSync(html: string, text: string) { // See https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#the-copy-command for more details. } - selection?.removeAllRanges(); + selection.removeAllRanges(); const anchorSelection = originalSelection as AnchorSelection; @@ -86,7 +90,7 @@ function setHTMLSync(html: string, text: string) { } else if (anchorSelection.anchorNode && anchorSelection.focusNode) { // When copying to the clipboard here, the original values of anchorNode and focusNode will be null since there will be no user selection. // We are adding a check to prevent null values from being passed to setBaseAndExtent, in accordance with the standards of the Selection API as outlined here: https://w3c.github.io/selection-api/#dom-selection-setbaseandextent. - selection?.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); + selection.setBaseAndExtent(anchorSelection.anchorNode, anchorSelection.anchorOffset, anchorSelection.focusNode, anchorSelection.focusOffset); } document.body.removeChild(node); From 9b24175227286b36baed7adb9100122f059eba04 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 2 Nov 2023 17:21:25 +0700 Subject: [PATCH 44/50] fix lint --- src/libs/actions/Policy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index db264bed5b47..9b33ff9b086e 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1,6 +1,7 @@ import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; import Str from 'expensify-common/lib/str'; import {escapeRegExp} from 'lodash'; +import filter from 'lodash/filter'; import lodashGet from 'lodash/get'; import lodashUnion from 'lodash/union'; import Onyx from 'react-native-onyx'; From ae4c7cb09982e49a4145586140a651bcb59d701f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 2 Nov 2023 17:33:16 +0100 Subject: [PATCH 45/50] Add const for space --- src/CONST.ts | 1 + src/pages/iou/IOUCurrencySelection.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index db8a9cc49dc0..de9eef0f6dc6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1311,6 +1311,7 @@ const CONST = { TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, + ANY_SPACE: /\s/g, // Extract attachment's source from the data's html string ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g, diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 2b342ae962c0..f6e2f7f125ff 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -126,10 +126,10 @@ function IOUCurrencySelection(props) { }; }); - const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim().replace(/\s/g, ' ')), 'i'); + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim().replace(CONST.REGEX.ANY_SPACE, ' ')), 'i'); const filteredCurrencies = _.filter( currencyOptions, - (currencyOption) => searchRegex.test(currencyOption.text.replace(/\s/g, ' ')) || searchRegex.test(currencyOption.currencyName.replace(/\s/g, ' ')), + (currencyOption) => searchRegex.test(currencyOption.text.replace(CONST.REGEX.ANY_SPACE, ' ')) || searchRegex.test(currencyOption.currencyName.replace(CONST.REGEX.ANY_SPACE, ' ')), ); const isEmpty = searchValue.trim() && !filteredCurrencies.length; From 27b5503b6b7ab8f9e0d69044c990d571c54178b8 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 2 Nov 2023 19:16:25 +0100 Subject: [PATCH 46/50] Push changes after prettier --- src/pages/iou/IOUCurrencySelection.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index f6e2f7f125ff..20344a08a2c8 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -129,7 +129,8 @@ function IOUCurrencySelection(props) { const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim().replace(CONST.REGEX.ANY_SPACE, ' ')), 'i'); const filteredCurrencies = _.filter( currencyOptions, - (currencyOption) => searchRegex.test(currencyOption.text.replace(CONST.REGEX.ANY_SPACE, ' ')) || searchRegex.test(currencyOption.currencyName.replace(CONST.REGEX.ANY_SPACE, ' ')), + (currencyOption) => + searchRegex.test(currencyOption.text.replace(CONST.REGEX.ANY_SPACE, ' ')) || searchRegex.test(currencyOption.currencyName.replace(CONST.REGEX.ANY_SPACE, ' ')), ); const isEmpty = searchValue.trim() && !filteredCurrencies.length; From c975ed9378c147daa104ad7e4ee3fc28ac7ad109 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 3 Nov 2023 08:03:03 +0000 Subject: [PATCH 47/50] Fix total calculation --- src/libs/ReportUtils.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index deac1b498e3f..f146b4666907 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1487,15 +1487,18 @@ function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { } if (moneyRequestReport) { let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0); - let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0); + let totalSpend = lodashGet(moneyRequestReport, 'total', 0); - if (nonReimbursableSpend + reimbursableSpend !== 0) { + if (nonReimbursableSpend + totalSpend !== 0) { // There is a possibility that if the Expense report has a negative total. // This is because there are instances where you can get a credit back on your card, // or you enter a negative expense to β€œoffset” future expenses nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend); - reimbursableSpend = isExpenseReport(moneyRequestReport) ? reimbursableSpend * -1 : Math.abs(reimbursableSpend); - const totalDisplaySpend = nonReimbursableSpend + reimbursableSpend; + totalSpend = isExpenseReport(moneyRequestReport) ? totalSpend * -1 : Math.abs(totalSpend); + + const totalDisplaySpend = totalSpend; + const reimbursableSpend = totalDisplaySpend - nonReimbursableSpend; + return { nonReimbursableSpend, reimbursableSpend, From 1680a113fb51165e69fb66a67f17f573b46dd92a Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 3 Nov 2023 18:28:08 +0500 Subject: [PATCH 48/50] fix: use parseISO instead od Date --- src/components/DatePicker/index.android.js | 4 ++-- src/components/DatePicker/index.ios.js | 4 ++-- src/components/DatePicker/index.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js index d92869162d49..561fc700b6a5 100644 --- a/src/components/DatePicker/index.android.js +++ b/src/components/DatePicker/index.android.js @@ -1,5 +1,5 @@ import RNDatePicker from '@react-native-community/datetimepicker'; -import {format} from 'date-fns'; +import {format, parseISO} from 'date-fns'; import React, {forwardRef, useCallback, useImperativeHandle, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; import TextInput from '@components/TextInput'; @@ -39,7 +39,7 @@ function DatePicker({value, defaultValue, label, placeholder, errorText, contain ); const date = value || defaultValue; - const dateAsText = date ? format(new Date(date), CONST.DATE.FNS_FORMAT_STRING) : ''; + const dateAsText = date ? format(parseISO(date), CONST.DATE.FNS_FORMAT_STRING) : ''; return ( <> diff --git a/src/components/DatePicker/index.ios.js b/src/components/DatePicker/index.ios.js index 0f741e8db1ea..60307f70e954 100644 --- a/src/components/DatePicker/index.ios.js +++ b/src/components/DatePicker/index.ios.js @@ -1,5 +1,5 @@ import RNDatePicker from '@react-native-community/datetimepicker'; -import {format} from 'date-fns'; +import {format, parseISO} from 'date-fns'; import isFunction from 'lodash/isFunction'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Button, Keyboard, View} from 'react-native'; @@ -77,7 +77,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca setSelectedDate(date); }; - const dateAsText = dateValue ? format(new Date(dateValue), CONST.DATE.FNS_FORMAT_STRING) : ''; + const dateAsText = dateValue ? format(parseISO(dateValue), CONST.DATE.FNS_FORMAT_STRING) : ''; return ( <> diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index 3bed9ca55321..33266242c5db 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -1,4 +1,4 @@ -import {format, isValid} from 'date-fns'; +import {format, isValid, parseISO} from 'date-fns'; import React, {useEffect, useRef} from 'react'; import _ from 'underscore'; import TextInput from '@components/TextInput'; @@ -29,7 +29,7 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl return; } - const date = new Date(text); + const date = parseISO(text); if (isValid(date)) { onInputChange(format(date, CONST.DATE.FNS_FORMAT_STRING)); } From 82d0f7cc371601fe878e60eb020195d3e606f024 Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:44:19 +0000 Subject: [PATCH 49/50] Revert "perf: refactor heavy operations when user starts to type" --- .../LHNOptionsList/OptionRowLHNData.js | 7 +---- src/libs/OptionsListUtils.js | 1 - src/libs/ReportActionsUtils.ts | 7 ++--- src/libs/ReportUtils.js | 26 +++--------------- src/libs/SidebarUtils.ts | 22 ++++++++------- src/libs/UnreadIndicatorUpdater/index.js | 27 ++----------------- src/libs/actions/Report.js | 1 - .../ComposerWithSuggestions.js | 10 +++---- src/pages/home/sidebar/SidebarLinksData.js | 5 ---- 9 files changed, 24 insertions(+), 82 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index cb64a135b264..ebba2ffe0587 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -168,17 +168,14 @@ export default React.memo( }, fullReport: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - initialValue: {}, }, reportActions: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, canEvict: false, - initialValue: {}, }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, selector: personalDetailsSelector, - initialValue: {}, }, preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, @@ -189,17 +186,15 @@ export default React.memo( parentReportActions: { key: ({fullReport}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fullReport.parentReportID}`, canEvict: false, - initialValue: {}, }, policy: { key: ({fullReport}) => `${ONYXKEYS.COLLECTION.POLICY}${fullReport.policyID}`, - initialValue: {}, }, // Ideally, we aim to access only the last transaction for the current report by listening to changes in reportActions. // In some scenarios, a transaction might be created after reportActions have been modified. // This can lead to situations where `lastTransaction` doesn't update and retains the previous value. // However, performance overhead of this is minimized by using memos inside the component. - receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION, initialValue: {}}, + receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION}, }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 99853975f86a..54d09b75eff2 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -87,7 +87,6 @@ Onyx.connect({ const policyExpenseReports = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, callback: (report, key) => { if (!ReportUtils.isPolicyExpenseChat(report)) { return; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 45bdfb18b451..11e11f549682 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -406,12 +406,9 @@ function getLastVisibleMessage(reportID: string, actionsToMerge: ReportActions = }; } - let messageText = message?.text ?? ''; - if (messageText) { - messageText = String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); - } + const messageText = message?.text ?? ''; return { - lastMessageText: messageText, + lastMessageText: String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(), }; } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5bb8fd4ad4fc..06408dd938f6 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -818,15 +818,8 @@ function isOneOnOneChat(report) { * @returns {Object} */ function getReport(reportID) { - /** - * Using typical string concatenation here due to performance issues - * with template literals. - */ - if (!allReports) { - return {}; - } - - return allReports[ONYXKEYS.COLLECTION.REPORT + reportID] || {}; + // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check + return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; } /** @@ -1530,25 +1523,14 @@ function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { * @returns {String} */ function getPolicyExpenseChatName(report, policy = undefined) { - const ownerAccountID = report.ownerAccountID; - const personalDetails = allPersonalDetails[ownerAccountID]; - const login = personalDetails ? personalDetails.login : null; - const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || login || report.reportName; + const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || lodashGet(allPersonalDetails, [report.ownerAccountID, 'login']) || report.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. if (report.isOwnPolicyExpenseChat) { return getPolicyName(report, false, policy); } - let policyExpenseChatRole = 'user'; - /** - * Using typical string concatenation here due to performance issues - * with template literals. - */ - const policyItem = allPolicies[ONYXKEYS.COLLECTION.POLICY + report.policyID]; - if (policyItem) { - policyExpenseChatRole = policyItem.role || 'user'; - } + const policyExpenseChatRole = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'role']) || 'user'; // If this user is not admin and this policy expense chat has been archived because of account merging, this must be an old workspace chat // of the account which was merged into the current user's account. Use the name of the policy as the name of the report. diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4951432bcd03..4aa708d5882d 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -156,6 +156,18 @@ function getOrderedReportIDs( } } + // There are a few properties that need to be calculated for the report which are used when sorting reports. + reportsToDisplay.forEach((report) => { + // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. + // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add + // the reportDisplayName property to the report object directly. + // eslint-disable-next-line no-param-reassign + report.displayName = ReportUtils.getReportName(report); + + // eslint-disable-next-line no-param-reassign + report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReports); + }); + // The LHN is split into four distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: // 1. Pinned/GBR - Always sorted by reportDisplayName // 2. Drafts - Always sorted by reportDisplayName @@ -169,17 +181,7 @@ function getOrderedReportIDs( const draftReports: Report[] = []; const nonArchivedReports: Report[] = []; const archivedReports: Report[] = []; - reportsToDisplay.forEach((report) => { - // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. - // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add - // the reportDisplayName property to the report object directly. - // eslint-disable-next-line no-param-reassign - report.displayName = ReportUtils.getReportName(report); - - // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReports); - const isPinned = report.isPinned ?? false; if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report)) { pinnedAndGBRReports.push(report); diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index bfa0cd911177..9af74f8313c3 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -1,4 +1,3 @@ -import {InteractionManager} from 'react-native'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; import * as ReportUtils from '@libs/ReportUtils'; @@ -6,33 +5,11 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import updateUnread from './updateUnread/index'; -let previousUnreadCount = 0; - Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (reportsFromOnyx) => { - if (!reportsFromOnyx) { - return; - } - - /** - * We need to wait until after interactions have finished to update the unread count because otherwise - * the unread count will be updated while the interactions/animations are in progress and we don't want - * to put more work on the main thread. - * - * For eg. On web we are manipulating DOM and it makes it a better candidate to wait until after interactions - * have finished. - * - * More info: https://reactnative.dev/docs/interactionmanager - */ - InteractionManager.runAfterInteractions(() => { - const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - const unreadReportsCount = _.size(unreadReports); - if (previousUnreadCount !== unreadReportsCount) { - previousUnreadCount = unreadReportsCount; - updateUnread(unreadReportsCount); - } - }); + const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + updateUnread(_.size(unreadReports)); }, }); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 4646e0e33da1..1de15c1184cb 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -65,7 +65,6 @@ Onyx.connect({ const currentReportData = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, callback: (data, key) => { if (!key || !data) { return; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index aebd70a4175f..6b375fb5ffa5 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -278,14 +278,8 @@ function ComposerWithSuggestions({ } } const newCommentConverted = convertToLTRForComposer(newComment); - const isNewCommentEmpty = !!newCommentConverted.match(/^(\s)*$/); - const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/); - - /** Only update isCommentEmpty state if it's different from previous one */ - if (isNewCommentEmpty !== isPrevCommentEmpty) { - setIsCommentEmpty(isNewCommentEmpty); - } emojisPresentBefore.current = emojis; + setIsCommentEmpty(!!newCommentConverted.match(/^(\s)*$/)); setValue(newCommentConverted); if (commentValue !== newComment) { const remainder = ComposerUtils.getCommonSuffixLength(commentValue, newComment); @@ -562,7 +556,9 @@ function ComposerWithSuggestions({ if (value.length === 0) { return; } + Report.setReportWithDraft(reportID, true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 1e5e11fd9fcb..293dc3f5cd9d 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -198,28 +198,23 @@ export default compose( chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, - initialValue: {}, }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, - initialValue: CONST.PRIORITY_MODE.DEFAULT, }, betas: { key: ONYXKEYS.BETAS, - initialValue: [], }, allReportActions: { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, selector: reportActionsSelector, - initialValue: {}, }, policies: { key: ONYXKEYS.COLLECTION.POLICY, selector: policySelector, - initialValue: {}, }, }), )(SidebarLinksData); From bc9cb8f8ca149c1f90dd09de9f19ae97bcaf07f0 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 3 Nov 2023 14:56:12 +0000 Subject: [PATCH 50/50] Update version to 1.3.95-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e43909433367..b07c66308609 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001039504 - versionName "1.3.95-4" + versionCode 1001039505 + versionName "1.3.95-5" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4d019ccacaa1..1966f3862d59 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.95.4 + 1.3.95.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 64aaf1899c16..387687a2beaa 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.95.4 + 1.3.95.5 diff --git a/package-lock.json b/package-lock.json index 7c4ba8f2aad7..a80022853a24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.95-4", + "version": "1.3.95-5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.95-4", + "version": "1.3.95-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c18fa7d9da00..f3462a2b63bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.95-4", + "version": "1.3.95-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",