Skip to content

Commit cf24edd

Browse files
committed
Revert "Merge pull request Expensify#76741 from margelo/kureev/new-editing-model"
This reverts commit 073ac6b, reversing changes made to 3a13517.
1 parent 9d65949 commit cf24edd

86 files changed

Lines changed: 1155 additions & 3145 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,18 +1842,6 @@ const CONST = {
18421842
MAX_LINES_FULL: -1,
18431843
// The minimum height needed to enable the full screen composer
18441844
FULL_COMPOSER_MIN_HEIGHT: 60,
1845-
/**
1846-
* TestIDs for the main report composer vs inline message editor (E2E / integration tests only).
1847-
* See tests/ui/ReportActionMessageEditLayoutTest.tsx
1848-
*/
1849-
TEST_ID: {
1850-
REPORT_ACTION_COMPOSE: 'reportActionCompose',
1851-
DRAFT_MESSAGE_ACTION_ROW: 'reportActionCompose_draftMessageActionRow',
1852-
EDITING_MESSAGE_ACTION_ROW: 'reportActionCompose_editingMessageActionRow',
1853-
REPORT_ACTION_ITEM_MESSAGE_EDIT: 'reportActionItemMessageEdit',
1854-
MESSAGE_EDIT_CANCEL_MAIN_COMPOSER: 'messageEditCancel_mainComposer',
1855-
MESSAGE_EDIT_CANCEL_INLINE: 'messageEditCancel_inlineMessageEdit',
1856-
},
18571845
},
18581846
MODAL: {
18591847
MODAL_TYPE: {

src/ONYXKEYS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ const ONYXKEYS = {
423423
/** The policyID of the last workspace whose settings were accessed by the user */
424424
LAST_ACCESSED_WORKSPACE_POLICY_ID: 'lastAccessedWorkspacePolicyID',
425425

426+
/** Whether we should show the compose input or not */
427+
SHOULD_SHOW_COMPOSE_INPUT: 'shouldShowComposeInput',
428+
426429
/** Is app in beta version */
427430
IS_BETA: 'isBeta',
428431

@@ -1477,6 +1480,7 @@ type OnyxValuesMapping = {
14771480
[ONYXKEYS.HAS_LOADED_APP]: boolean;
14781481
[ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer;
14791482
[ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string;
1483+
[ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean;
14801484
[ONYXKEYS.IS_BETA]: boolean;
14811485
[ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM]: boolean;
14821486
[ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS]: Record<string, string>;

src/components/Composer/implementation/index.native.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type {MarkdownStyle, MarkdownTextInput} from '@expensify/react-native-live-markdown';
1+
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
22
import mimeDb from 'mime-db';
33
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
44
import type {NativeSyntheticEvent, TextInputChangeEvent, TextInputPasteEventData} from 'react-native';
55
import {StyleSheet} from 'react-native';
6-
import type {ComposerProps, ComposerRef} from '@components/Composer/types';
6+
import type {ComposerProps} from '@components/Composer/types';
77
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
88
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
99
import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode';
@@ -38,7 +38,7 @@ function Composer({
3838
ref,
3939
...props
4040
}: ComposerProps) {
41-
const textInputRef = useRef<MarkdownTextInput | null>(null);
41+
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
4242
const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? ''))), [value]);
4343
const theme = useTheme();
4444
const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
@@ -47,7 +47,7 @@ function Composer({
4747
const isInLandscapeMode = useIsInLandscapeMode();
4848

4949
useEffect(() => {
50-
if (!textInputRef.current?.setSelection || !selection || isComposerFullSize) {
50+
if (!textInput.current?.setSelection || !selection || isComposerFullSize) {
5151
return;
5252
}
5353

@@ -56,8 +56,8 @@ function Composer({
5656
// (see https://github.com/Expensify/App/pull/50520#discussion_r1861960311 for more context)
5757
const timeoutID = setTimeout(() => {
5858
// We are setting selection twice to trigger a scroll to the cursor on toggling to smaller composer size.
59-
textInputRef.current?.setSelection((selection.start || 1) - 1, selection.start);
60-
textInputRef.current?.setSelection(selection.start, selection.start);
59+
textInput.current?.setSelection((selection.start || 1) - 1, selection.start);
60+
textInput.current?.setSelection(selection.start, selection.start);
6161
}, 0);
6262

6363
return () => clearTimeout(timeoutID);
@@ -71,17 +71,17 @@ function Composer({
7171
*/
7272
const setTextInputRef = useCallback(
7373
(el: AnimatedMarkdownTextInputRef | null) => {
74-
textInputRef.current = isInLandscapeMode ? getLandscapeTextInputRefProxy(el) : el;
74+
textInput.current = isInLandscapeMode ? getLandscapeTextInputRefProxy(el) : el;
7575

76-
if (typeof ref !== 'function' || textInputRef.current === null) {
76+
if (typeof ref !== 'function' || textInput.current === null) {
7777
return;
7878
}
7979

8080
// This callback prop is used by the parent component using the constructor to
8181
// get a ref to the inner textInput element e.g. if we do
8282
// <constructor ref={el => this.textInput = el} /> this will not
8383
// return a ref to the component, but rather the HTML element by default
84-
ref(textInputRef.current as ComposerRef);
84+
ref(textInput.current);
8585
},
8686
// eslint-disable-next-line react-hooks/exhaustive-deps
8787
[isInLandscapeMode],

src/components/Composer/implementation/index.tsx

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import lodashDebounce from 'lodash/debounce';
44
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
55
import type {TextInputKeyPressEvent, TextInputSelectionChangeEvent} from 'react-native';
66
import {DeviceEventEmitter, StyleSheet} from 'react-native';
7-
import type {ComposerProps, ComposerRef} from '@components/Composer/types';
7+
import type {ComposerProps} from '@components/Composer/types';
88
import {useSession} from '@components/OnyxListItemProvider';
99
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
1010
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
@@ -64,7 +64,7 @@ function Composer({
6464
const addAuthTokenToImageURL = useCallback((url: string) => addEncryptedAuthTokenToURL(url, encryptedAuthToken), [encryptedAuthToken]);
6565
const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
6666
const StyleUtils = useStyleUtils();
67-
const textInputRef = useRef<AnimatedMarkdownTextInputRef | null>(null);
67+
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
6868
const [selection, setSelection] = useState<
6969
| {
7070
start: number;
@@ -79,7 +79,7 @@ function Composer({
7979
});
8080
const [isRendered, setIsRendered] = useState(false);
8181

82-
const isScrollBarVisible = useIsScrollBarVisible(textInputRef, value ?? '');
82+
const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
8383
const [prevScroll, setPrevScroll] = useState<number | undefined>();
8484
const [prevHeight, setPrevHeight] = useState<number | undefined>();
8585
const isReportFlatListScrolling = useRef(false);
@@ -95,26 +95,19 @@ function Composer({
9595
/**
9696
* Adds the cursor position to the selection change event.
9797
*/
98-
const addCursorPositionToSelectionChange = useCallback(
99-
(event: TextInputSelectionChangeEvent) => {
100-
const sel = window.getSelection();
101-
const canCalculateCaretPosition = shouldCalculateCaretPosition && isRendered && sel;
102-
if (!canCalculateCaretPosition) {
103-
onSelectionChange(event);
104-
setSelection(event.nativeEvent.selection);
105-
return;
106-
}
107-
98+
const addCursorPositionToSelectionChange = (event: TextInputSelectionChangeEvent) => {
99+
const sel = window.getSelection();
100+
if (shouldCalculateCaretPosition && isRendered && sel) {
108101
const range = sel.getRangeAt(0).cloneRange();
109102
range.collapse(true);
110103
const rect = range.getClientRects()[0] || range.startContainer.parentElement?.getClientRects()[0];
111-
const containerRect = textInputRef.current?.getBoundingClientRect();
104+
const containerRect = textInput.current?.getBoundingClientRect();
112105

113106
let x = 0;
114107
let y = 0;
115108
if (rect && containerRect) {
116109
x = rect.left - containerRect.left;
117-
y = rect.top - containerRect.top + (textInputRef?.current?.scrollTop ?? 0) - rect.height / 2;
110+
y = rect.top - containerRect.top + (textInput?.current?.scrollTop ?? 0) - rect.height / 2;
118111
}
119112

120113
const selectionValue = {
@@ -132,9 +125,11 @@ function Composer({
132125
},
133126
});
134127
setSelection(selectionValue);
135-
},
136-
[isRendered, onSelectionChange, shouldCalculateCaretPosition],
137-
);
128+
} else {
129+
onSelectionChange(event);
130+
setSelection(event.nativeEvent.selection);
131+
}
132+
};
138133

139134
/**
140135
* Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file,
@@ -143,14 +138,14 @@ function Composer({
143138
const handlePaste = useCallback(
144139
(event: ClipboardEvent) => {
145140
const isVisible = checkComposerVisibility();
146-
const isFocused = textInputRef.current?.isFocused();
141+
const isFocused = textInput.current?.isFocused();
147142
const isContenteditableDivFocused = document.activeElement?.nodeName === 'DIV' && document.activeElement?.hasAttribute('contenteditable');
148143

149144
if (!(isVisible || isFocused)) {
150145
return true;
151146
}
152147

153-
if (textInputRef.current !== event.target && !(isContenteditableDivFocused && !event.clipboardData?.files.length)) {
148+
if (textInput.current !== event.target && !(isContenteditableDivFocused && !event.clipboardData?.files.length)) {
154149
const eventTarget = event.target as HTMLInputElement | HTMLTextAreaElement | null;
155150
// To make sure the composer does not capture paste events from other inputs, we check where the event originated
156151
// If it did originate in another input, we return early to prevent the composer from handling the paste
@@ -159,7 +154,7 @@ function Composer({
159154
return true;
160155
}
161156

162-
textInputRef.current?.focus();
157+
textInput.current?.focus();
163158
}
164159

165160
event.preventDefault();
@@ -214,23 +209,19 @@ function Composer({
214209
);
215210

216211
useEffect(() => {
217-
if (!textInputRef.current) {
212+
if (!textInput.current) {
218213
return;
219214
}
220-
221-
const inputRef = textInputRef.current;
222-
223215
const debouncedSetPrevScroll = lodashDebounce(() => {
224-
if (!inputRef) {
216+
if (!textInput.current) {
225217
return;
226218
}
227-
setPrevScroll(inputRef.scrollTop);
219+
setPrevScroll(textInput.current.scrollTop);
228220
}, 100);
229221

230-
inputRef.addEventListener('scroll', debouncedSetPrevScroll);
231-
222+
textInput.current.addEventListener('scroll', debouncedSetPrevScroll);
232223
return () => {
233-
inputRef?.removeEventListener('scroll', debouncedSetPrevScroll);
224+
textInput.current?.removeEventListener('scroll', debouncedSetPrevScroll);
234225
};
235226
}, []);
236227

@@ -243,8 +234,6 @@ function Composer({
243234
}, []);
244235

245236
useEffect(() => {
246-
const inputRef = textInputRef.current;
247-
248237
const handleWheel = (e: MouseEvent) => {
249238
if (isReportFlatListScrolling.current) {
250239
e.preventDefault();
@@ -253,40 +242,40 @@ function Composer({
253242

254243
// When the composer has no scrollable content, the stopPropagation will prevent the inverted wheel event handler on the Chat body
255244
// which defaults to the browser wheel behavior. This causes the chat body to scroll in the opposite direction creating jerky behavior.
256-
if (inputRef && inputRef.scrollHeight <= inputRef.clientHeight) {
245+
if (textInput.current && textInput.current.scrollHeight <= textInput.current.clientHeight) {
257246
return;
258247
}
259248
e.stopPropagation();
260249
};
261-
inputRef?.addEventListener('wheel', handleWheel, {passive: false});
250+
textInput.current?.addEventListener('wheel', handleWheel, {passive: false});
262251

263252
return () => {
264-
inputRef?.removeEventListener('wheel', handleWheel);
253+
textInput.current?.removeEventListener('wheel', handleWheel);
265254
};
266255
}, []);
267256

268257
useEffect(() => {
269-
if (!textInputRef.current || prevScroll === undefined || prevHeight === undefined) {
258+
if (!textInput.current || prevScroll === undefined || prevHeight === undefined) {
270259
return;
271260
}
272-
textInputRef.current.scrollTop = prevScroll + prevHeight - textInputRef.current.clientHeight;
261+
textInput.current.scrollTop = prevScroll + prevHeight - textInput.current.clientHeight;
273262
// eslint-disable-next-line react-hooks/exhaustive-deps
274263
}, [isComposerFullSize]);
275264

276265
const isActive = useIsFocused();
277-
useHtmlPaste(textInputRef, handlePaste, isActive);
266+
useHtmlPaste(textInput, handlePaste, isActive);
278267

279268
useEffect(() => {
280269
setIsRendered(true);
281270
}, []);
282271

283272
const clear = useCallback(() => {
284-
if (!textInputRef.current) {
273+
if (!textInput.current) {
285274
return;
286275
}
287276

288-
const currentText = textInputRef.current.value;
289-
textInputRef.current.clear();
277+
const currentText = textInput.current.value;
278+
textInput.current.clear();
290279

291280
// We need to reset the selection to 0,0 manually after clearing the text input on web
292281
const selectionEvent = {
@@ -304,22 +293,22 @@ function Composer({
304293
}, [onClear, onSelectionChange]);
305294

306295
useImperativeHandle(ref, () => {
307-
const textInput = textInputRef.current;
308-
if (!textInput) {
309-
throw new Error('textInput is not available. This should never happen and indicates a developer error.');
296+
const textInputRef = textInput.current;
297+
if (!textInputRef) {
298+
throw new Error('textInputRef is not available. This should never happen and indicates a developer error.');
310299
}
311300

312301
return {
313-
...textInput,
302+
...textInputRef,
314303
// Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works
315304
clear,
316305
// We have to redefine these methods as they are inherited by prototype chain and are not accessible directly
317-
blur: () => textInput.blur(),
318-
focus: () => textInput.focus(),
306+
blur: () => textInputRef.blur(),
307+
focus: () => textInputRef.focus(),
319308
get scrollTop() {
320-
return textInput.scrollTop;
309+
return textInputRef.scrollTop;
321310
},
322-
} as ComposerRef;
311+
};
323312
}, [clear]);
324313

325314
const handleKeyPress = useCallback(
@@ -361,7 +350,9 @@ function Composer({
361350
autoComplete="off"
362351
autoCorrect={!isMobileSafari()}
363352
placeholderTextColor={theme.placeholderText}
364-
ref={textInputRef}
353+
ref={(el) => {
354+
textInput.current = el;
355+
}}
365356
selection={selection}
366357
style={[inputStyleMemo]}
367358
markdownStyle={markdownStyle}

src/components/Composer/types.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type {MarkdownTextInput} from '@expensify/react-native-live-markdown';
21
import type {Ref} from 'react';
3-
import type {StyleProp, TextInputProps, TextInputSelectionChangeEvent, TextStyle} from 'react-native';
2+
import type {StyleProp, TextInput, TextInputProps, TextInputSelectionChangeEvent, TextStyle} from 'react-native';
3+
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
44
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
55
import type {FileObject} from '@src/types/utils/Attachment';
66

@@ -15,8 +15,6 @@ type CustomSelectionChangeEvent = TextInputSelectionChangeEvent & {
1515
positionY?: number;
1616
};
1717

18-
type ComposerRef = MarkdownTextInput & HTMLInputElement & HTMLTextAreaElement;
19-
2018
type ComposerProps = Omit<TextInputProps, 'onClear'> &
2119
ForwardedFSClassProps & {
2220
/** Indicate whether input is multiline */
@@ -76,7 +74,7 @@ type ComposerProps = Omit<TextInputProps, 'onClear'> &
7674
isGroupPolicyReport?: boolean;
7775

7876
/** Ref exposing imperative methods on the underlying text input */
79-
ref?: Ref<ComposerRef>;
77+
ref?: Ref<TextInput | HTMLInputElement | HTMLTextAreaElement | AnimatedMarkdownTextInputRef>;
8078
};
8179

82-
export type {TextSelection, ComposerProps, CustomSelectionChangeEvent, ComposerRef};
80+
export type {TextSelection, ComposerProps, CustomSelectionChangeEvent};

src/components/ExceededCommentLength.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type ExceededCommentLengthProps = {
99
isTaskTitle?: boolean;
1010
};
1111

12-
function ExceededCommentLength({maxCommentLength = CONST.MAX_COMMENT_LENGTH, isTaskTitle = false}: ExceededCommentLengthProps) {
12+
function ExceededCommentLength({maxCommentLength = CONST.MAX_COMMENT_LENGTH, isTaskTitle}: ExceededCommentLengthProps) {
1313
const styles = useThemeStyles();
1414
const {numberFormat, translate} = useLocalize();
1515

src/components/ImageSVG/index.android.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import getImageRecyclingKey from '@libs/getImageRecyclingKey';
55
import type ImageSVGProps from './types';
66

77
function ImageSVG({src, width = '100%', height = '100%', fill, contentFit = 'cover', style, onLoadEnd}: ImageSVGProps) {
8+
const tintColorProp = fill ? {tintColor: fill} : {};
89
const isReactComponent = typeof src === 'function';
910

1011
// Clear memory cache when unmounting images to avoid memory overload
@@ -60,10 +61,8 @@ function ImageSVG({src, width = '100%', height = '100%', fill, contentFit = 'cov
6061
source={src}
6162
recyclingKey={getImageRecyclingKey(src)}
6263
style={[{width, height}, style as ExpoImageProps['style']]}
63-
tintColor={fill}
64-
// On android, there's an issue where the fill color of the icon does not change,
65-
// unless the component is remounted. (https://github.com/Expensify/App/pull/76741#issuecomment-4245274687)
66-
key={fill}
64+
// eslint-disable-next-line react/jsx-props-no-spreading
65+
{...tintColorProp}
6766
/>
6867
);
6968
}

0 commit comments

Comments
 (0)