Skip to content

Commit 8cbb4a6

Browse files
authored
Merge pull request Expensify#90513 from margelo/@chrispader/revert/composer-editing
revert: New editing mechanism for small screens
2 parents 9d65949 + ea7c23b commit 8cbb4a6

87 files changed

Lines changed: 1162 additions & 3151 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.

config/eslint/eslint.seatbelt.tsv

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@
496496
"../../src/pages/domain/Groups/PreferredWorkspaceToggle.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
497497
"../../src/pages/domain/Saml/SamlLoginSectionContent.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
498498
"../../src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
499-
"../../src/pages/inbox/ReportFetchHandler.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2
499+
"../../src/pages/inbox/ReportFetchHandler.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 3
500500
"../../src/pages/inbox/ReportNavigateAwayHandler.tsx" "react-hooks/exhaustive-deps" 1
501501
"../../src/pages/inbox/hooks/useReportWasDeleted.ts" "react-hooks/set-state-in-effect" 1
502502
"../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
@@ -505,20 +505,21 @@
505505
"../../src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx" "@typescript-eslint/no-deprecated/getReportNameDeprecated" 1
506506
"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
507507
"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
508-
"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "react-hooks/refs" 29
508+
"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "react-hooks/refs" 30
509509
"../../src/pages/inbox/report/ListBoundaryLoader.tsx" "react-hooks/set-state-in-effect" 1
510510
"../../src/pages/inbox/report/PureReportActionItem.tsx" "react-hooks/refs" 2
511511
"../../src/pages/inbox/report/PureReportActionItem.tsx" "react-hooks/set-state-in-effect" 1
512512
"../../src/pages/inbox/report/ReactionList/HeaderReactionList.tsx" "no-restricted-syntax" 1
513-
"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
514-
"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx" "react-hooks/refs" 7
513+
"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2
514+
"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx" "react-hooks/preserve-manual-memoization" 2
515+
"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx" "react-hooks/refs" 8
515516
"../../src/pages/inbox/report/ReportActionCompose/SuggestionEmoji.tsx" "react-hooks/refs" 1
516517
"../../src/pages/inbox/report/ReportActionCompose/SuggestionMention.tsx" "react-hooks/refs" 3
517518
"../../src/pages/inbox/report/ReportActionItemMessage.tsx" "@typescript-eslint/no-deprecated/getReportName" 1
518-
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
519+
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2
519520
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "no-restricted-syntax" 1
520521
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/preserve-manual-memoization" 1
521-
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/refs" 5
522+
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/refs" 6
522523
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/set-state-in-effect" 1
523524
"../../src/pages/inbox/report/ReportActionsList.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 4
524525
"../../src/pages/inbox/report/ReportActionsList.tsx" "react-hooks/refs" 5

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}

0 commit comments

Comments
 (0)