Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Composer context menu not opening #55647

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/components/Composer/implementation/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import {containsOnlyEmojis} from '@libs/EmojiUtils';
import {splitExtensionFromFileName} from '@libs/fileDownload/FileUtils';
import getPlatform from '@libs/getPlatform';
import CONST from '@src/CONST';

Expand Down Expand Up @@ -45,7 +45,7 @@ function Composer(
) {
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
const {isFocused, shouldResetFocusRef} = useResetComposerFocus(textInput);
const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const styles = useThemeStyles();
Expand Down Expand Up @@ -129,7 +129,7 @@ function Composer(
const mimeType = clipboardContent?.type ?? '';
const fileURI = clipboardContent?.data;
const baseFileName = fileURI?.split('/').pop() ?? 'file';
const {fileName: stem, fileExtension: originalFileExtension} = FileUtils.splitExtensionFromFileName(baseFileName);
const {fileName: stem, fileExtension: originalFileExtension} = splitExtensionFromFileName(baseFileName);
const fileExtension = originalFileExtension || (mimeDb[mimeType].extensions?.[0] ?? 'bin');
const fileName = `${stem}.${fileExtension}`;
const file: FileObject = {uri: fileURI, name: fileName, type: mimeType};
Expand Down Expand Up @@ -172,7 +172,8 @@ function Composer(
}}
onClear={onClear}
showSoftInputOnFocus={showSoftInputOnFocus}
contextMenuHidden={contextMenuHidden}
// Prevent the context menu from showing when tapping the composer to focus it on iOS.
contextMenuHidden={getPlatform() === CONST.PLATFORM.IOS ? contextMenuHidden : false}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,31 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
import {isMobileSafari, isMobileWebKit} from '@libs/Browser';
import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
import {forceClearInput} from '@libs/ComponentUtils';
import * as ComposerUtils from '@libs/ComposerUtils';
import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSpaceAtIndex} from '@libs/ComposerUtils';
import convertToLTRForComposer from '@libs/convertToLTRForComposer';
import {getDraftComment} from '@libs/DraftCommentUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getPreferredSkinToneIndex, replaceAndExtractEmojis} from '@libs/EmojiUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import getPlatform from '@libs/getPlatform';
import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener';
import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener';
import Parser from '@libs/Parser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportUtils from '@libs/ReportUtils';
import {isValidReportIDFromPath, shouldAutoFocusOnKeyPress} from '@libs/ReportUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
import getCursorPosition from '@pages/home/report/ReportActionCompose/getCursorPosition';
import getScrollPosition from '@pages/home/report/ReportActionCompose/getScrollPosition';
import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose';
import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater';
import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions';
import * as EmojiPickerActions from '@userActions/EmojiPickerAction';
import * as InputFocus from '@userActions/InputFocus';
import * as Modal from '@userActions/Modal';
import * as Report from '@userActions/Report';
import {isEmojiPickerVisible} from '@userActions/EmojiPickerAction';
import type {OnEmojiSelected} from '@userActions/EmojiPickerAction';
import {inputFocusChange} from '@userActions/InputFocus';
import {areAllModalsHidden} from '@userActions/Modal';
import {broadcastUserIsTyping, saveReportActionDraft, saveReportDraftComment} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
Expand Down Expand Up @@ -155,7 +156,7 @@ type SwitchToCurrentReportProps = {
type ComposerRef = {
blur: () => void;
focus: (shouldDelay?: boolean) => void;
replaceSelectionWithText: EmojiPickerActions.OnEmojiSelected;
replaceSelectionWithText: OnEmojiSelected;
getCurrentText: () => string;
isFocused: () => boolean;
/**
Expand All @@ -174,7 +175,7 @@ const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
*/
const debouncedBroadcastUserIsTyping = lodashDebounce(
(reportID: string) => {
Report.broadcastUserIsTyping(reportID);
broadcastUserIsTyping(reportID);
},
1000,
{
Expand Down Expand Up @@ -247,15 +248,15 @@ function ComposerWithSuggestions(
const draftComment = getDraftComment(reportID) ?? '';
const [value, setValue] = useState(() => {
if (draftComment) {
emojisPresentBefore.current = EmojiUtils.extractEmojis(draftComment);
emojisPresentBefore.current = extractEmojis(draftComment);
}
return draftComment;
});

const commentRef = useRef(value);

const [modal] = useOnyx(ONYXKEYS.MODAL);
const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: EmojiUtils.getPreferredSkinToneIndex});
const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: getPreferredSkinToneIndex});
const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED);

const lastTextRef = useRef(value);
Expand All @@ -266,7 +267,7 @@ function ComposerWithSuggestions(
const {shouldUseNarrowLayout} = useResponsiveLayout();
const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;

const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput && Modal.areAllModalsHidden() && isFocused && !didHideComposerInput;
const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput;

const valueRef = useRef(value);
valueRef.current = value;
Expand Down Expand Up @@ -307,7 +308,7 @@ function ComposerWithSuggestions(
const debouncedSaveReportComment = useMemo(
() =>
lodashDebounce((selectedReportID: string, newComment: string | null) => {
Report.saveReportDraftComment(selectedReportID, newComment);
saveReportDraftComment(selectedReportID, newComment);
isCommentPendingSaved.current = false;
}, 1000),
[],
Expand All @@ -319,7 +320,7 @@ function ComposerWithSuggestions(
callback();
return;
}
Report.saveReportDraftComment(preexistingReportID, commentRef.current, callback);
saveReportDraftComment(preexistingReportID, commentRef.current, callback);
});

return () => {
Expand Down Expand Up @@ -350,7 +351,7 @@ function ComposerWithSuggestions(

if (currentIndex < newText.length) {
startIndex = currentIndex;
const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection?.end ?? 0);
const commonSuffixLength = findCommonSuffixLength(prevText, newText, selection?.end ?? 0);
// if text is getting pasted over find length of common suffix and subtract it from new text length
if (commonSuffixLength > 0 || (selection?.end ?? 0) - selection.start > 0) {
endIndex = newText.length - commonSuffixLength;
Expand All @@ -374,11 +375,11 @@ function ComposerWithSuggestions(
(commentValue: string, shouldDebounceSaveComment?: boolean) => {
raiseIsScrollLikelyLayoutTriggered();
const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue);
const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff);
const commentWithSpaceInserted = isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue;
const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentWithSpaceInserted, preferredSkinTone, preferredLocale);
const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && containsOnlyEmojis(diff);
const commentWithSpaceInserted = isEmojiInserted ? insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue;
const {text: newComment, emojis, cursorPosition} = replaceAndExtractEmojis(commentWithSpaceInserted, preferredSkinTone, preferredLocale);
if (emojis.length) {
const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current);
const newEmojis = getAddedEmojis(emojis, emojisPresentBefore.current);
if (newEmojis.length) {
// Ensure emoji suggestions are hidden after inserting emoji even when the selection is not changed
if (suggestionsRef.current) {
Expand Down Expand Up @@ -417,7 +418,7 @@ function ComposerWithSuggestions(
isCommentPendingSaved.current = true;
debouncedSaveReportComment(reportID, newCommentConverted);
} else {
Report.saveReportDraftComment(reportID, newCommentConverted);
saveReportDraftComment(reportID, newCommentConverted);
}
if (newCommentConverted) {
debouncedBroadcastUserIsTyping(reportID);
Expand All @@ -433,15 +434,15 @@ function ComposerWithSuggestions(
(text: string) => {
// selection replacement should be debounced to avoid conflicts with text typing
// (f.e. when emoji is being picked and 1 second still did not pass after user finished typing)
updateComment(ComposerUtils.insertText(commentRef.current, selection, text), true);
updateComment(insertText(commentRef.current, selection, text), true);
},
[selection, updateComment],
);

const handleKeyPress = useCallback(
(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
const webEvent = event as unknown as KeyboardEvent;
if (!webEvent || ComposerUtils.canSkipTriggerHotkeys(shouldUseNarrowLayout, isKeyboardShown)) {
if (!webEvent || canSkipTriggerHotkeys(shouldUseNarrowLayout, isKeyboardShown)) {
return;
}

Expand All @@ -461,7 +462,7 @@ function ComposerWithSuggestions(
webEvent.preventDefault();
if (lastReportAction) {
const message = Array.isArray(lastReportAction?.message) ? lastReportAction?.message?.at(-1) ?? null : lastReportAction?.message ?? null;
Report.saveReportActionDraft(reportID, lastReportAction, Parser.htmlToMarkdown(message?.html ?? ''));
saveReportActionDraft(reportID, lastReportAction, Parser.htmlToMarkdown(message?.html ?? ''));
}
}
// Flag emojis like "Wales" have several code points. Default backspace key action does not remove such flag emojis completely.
Expand Down Expand Up @@ -544,7 +545,7 @@ function ComposerWithSuggestions(
if (!suggestionsRef.current) {
return false;
}
InputFocus.inputFocusChange(false);
inputFocusChange(false);
return suggestionsRef.current.setShouldBlockSuggestionCalc(false);
}, [suggestionsRef]);

Expand Down Expand Up @@ -579,7 +580,7 @@ function ComposerWithSuggestions(
*/
const checkComposerVisibility = useCallback(() => {
// Checking whether the screen is focused or not, helps avoid `modal.isVisible` false when popups are closed, even if the modal is opened.
const isComposerCoveredUp = !isFocused || EmojiPickerActions.isEmojiPickerVisible() || isMenuVisible || !!modal?.isVisible || modal?.willAlertModalBecomeVisible;
const isComposerCoveredUp = !isFocused || isEmojiPickerVisible() || isMenuVisible || !!modal?.isVisible || modal?.willAlertModalBecomeVisible;
return !isComposerCoveredUp;
}, [isMenuVisible, modal, isFocused]);

Expand All @@ -590,7 +591,7 @@ function ComposerWithSuggestions(
return;
}

if (!ReportUtils.shouldAutoFocusOnKeyPress(e)) {
if (!shouldAutoFocusOnKeyPress(e)) {
return;
}

Expand Down Expand Up @@ -622,21 +623,21 @@ function ComposerWithSuggestions(
}, []);

useEffect(() => {
const unsubscribeNavigationBlur = navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress));
const unsubscribeNavigationBlur = navigation.addListener('blur', () => removeKeyDownPressListener(focusComposerOnKeyPress));
const unsubscribeNavigationFocus = navigation.addListener('focus', () => {
KeyDownListener.addKeyDownPressListener(focusComposerOnKeyPress);
addKeyDownPressListener(focusComposerOnKeyPress);
// The report isn't unmounted and can be focused again after going back from another report so we should update the composerRef again
ReportActionComposeFocusManager.composerRef.current = textInputRef.current;
setUpComposeFocusManager();
});
KeyDownListener.addKeyDownPressListener(focusComposerOnKeyPress);
addKeyDownPressListener(focusComposerOnKeyPress);

setUpComposeFocusManager();

return () => {
ReportActionComposeFocusManager.clear();

KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress);
removeKeyDownPressListener(focusComposerOnKeyPress);
unsubscribeNavigationBlur();
unsubscribeNavigationFocus();
};
Expand Down Expand Up @@ -672,7 +673,7 @@ function ComposerWithSuggestions(
}

if (editFocused) {
InputFocus.inputFocusChange(false);
inputFocusChange(false);
return;
}
focus(true);
Expand Down Expand Up @@ -783,9 +784,16 @@ function ComposerWithSuggestions(
[measureParentContainer, cursorPositionValue, selection],
);

const isTouchEndedRef = useRef(false);

return (
<>
<View style={[StyleUtils.getContainerComposeStyles(), styles.textInputComposeBorder]}>
<View
style={[StyleUtils.getContainerComposeStyles(), styles.textInputComposeBorder]}
onTouchEndCapture={() => {
isTouchEndedRef.current = true;
}}
>
<Composer
checkComposerVisibility={checkComposerVisibility}
autoFocus={!!shouldAutoFocus}
Expand Down Expand Up @@ -819,15 +827,21 @@ function ComposerWithSuggestions(
shouldCalculateCaretPosition
onLayout={onLayout}
onScroll={hideSuggestionMenu}
shouldContainScroll={Browser.isMobileSafari()}
shouldContainScroll={isMobileSafari()}
isGroupPolicyReport={isGroupPolicyReport}
showSoftInputOnFocus={showSoftInputOnFocus}
onTouchStart={() => {
if (showSoftInputOnFocus) {
return;
}
if (Browser.isMobileSafari()) {
if (isMobileWebKit()) {
isTouchEndedRef.current = false;
// In iOS browsers, open the keyboard after a timeout, or it will close briefly.
setTimeout(() => {
if (!isTouchEndedRef.current) {
// Don't open the keyboard on long press so the callout menu can show.
return;
}
setShowSoftInputOnFocus(true);
}, CONST.ANIMATED_TRANSITION);
return;
Expand All @@ -851,7 +865,7 @@ function ComposerWithSuggestions(
resetKeyboardInput={resetKeyboardInput}
/>

{ReportUtils.isValidReportIDFromPath(reportID) && (
{isValidReportIDFromPath(reportID) && (
<SilentCommentUpdater
reportID={reportID}
value={value}
Expand Down