From 98f25046afa17a72f4b0450f38735c1398e5fdc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 22 Feb 2023 18:03:21 +0100 Subject: [PATCH] Fix double tilde by waiting for text values to propagate to the native side (#7132) Co-authored-by: Daniel Espino --- .../post_draft/post_input/post_input.tsx | 22 +++++++++++----- app/hooks/input.ts | 25 ++++++++++++++++++ .../channel_info_form.tsx | 19 ++++++++++++-- app/screens/edit_post/edit_post.tsx | 26 ++++++++++++++----- 4 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 app/hooks/input.ts diff --git a/app/components/post_draft/post_input/post_input.tsx b/app/components/post_draft/post_input/post_input.tsx index f0aff5092..fd09fe6ff 100644 --- a/app/components/post_draft/post_input/post_input.tsx +++ b/app/components/post_draft/post_input/post_input.tsx @@ -17,6 +17,7 @@ import {Events, Screens} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {useInputPropagation} from '@hooks/input'; import {t} from '@i18n'; import NavigationStore from '@store/navigation_store'; import {extractFileInfo} from '@utils/file'; @@ -120,6 +121,7 @@ export default function PostInput({ const style = getStyleSheet(theme); const serverUrl = useServerUrl(); const managedConfig = useManagedConfig(); + const [propagateValue, shouldProcessEvent] = useInputPropagation(); const lastTypingEventSent = useRef(0); @@ -180,6 +182,9 @@ export default function PostInput({ }, [updateCursorPosition, cursorPosition]); const handleTextChange = useCallback((newValue: string) => { + if (!shouldProcessEvent(newValue)) { + return; + } updateValue(newValue); lastNativeValue.current = newValue; @@ -224,10 +229,16 @@ export default function PostInput({ case 'enter': sendMessage(); break; - case 'shift-enter': - updateValue((v) => v.substring(0, cursorPosition) + '\n' + v.substring(cursorPosition)); + case 'shift-enter': { + let newValue: string; + updateValue((v) => { + newValue = v.substring(0, cursorPosition) + '\n' + v.substring(cursorPosition); + return newValue; + }); updateCursorPosition((pos) => pos + 1); + propagateValue(newValue!); break; + } } } }, [sendMessage, updateValue, cursorPosition, isTablet]); @@ -266,6 +277,7 @@ export default function PostInput({ const draft = value ? `${value} ${text} ` : `${text} `; updateValue(draft); updateCursorPosition(draft.length); + propagateValue(draft); inputRef.current?.focus(); } }); @@ -274,10 +286,7 @@ export default function PostInput({ useEffect(() => { if (value !== lastNativeValue.current) { - // May change when we implement Fabric - inputRef.current?.setNativeProps({ - text: value, - }); + propagateValue(value); lastNativeValue.current = value; } }, [value]); @@ -310,6 +319,7 @@ export default function PostInput({ testID={testID} underlineColorAndroid='transparent' textContentType='none' + value={value} /> ); } diff --git a/app/hooks/input.ts b/app/hooks/input.ts new file mode 100644 index 000000000..cf2dc8baf --- /dev/null +++ b/app/hooks/input.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {useCallback, useRef} from 'react'; +import {Platform} from 'react-native'; + +export function useInputPropagation(): [(v: string) => void, (v: string) => boolean] { + const waitForValue = useRef(); + const waitToPropagate = useCallback((value: string) => { + waitForValue.current = value; + }, []); + const shouldProcessEvent = useCallback((newValue: string) => { + if (Platform.OS === 'android') { + return true; + } + if (waitForValue.current === undefined) { + return true; + } + if (newValue === waitForValue.current) { + waitForValue.current = undefined; + } + return false; + }, []); + + return [waitToPropagate, shouldProcessEvent]; +} diff --git a/app/screens/create_or_edit_channel/channel_info_form.tsx b/app/screens/create_or_edit_channel/channel_info_form.tsx index ca984e1fb..64954583d 100644 --- a/app/screens/create_or_edit_channel/channel_info_form.tsx +++ b/app/screens/create_or_edit_channel/channel_info_form.tsx @@ -17,6 +17,7 @@ import { import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; +import {useInputPropagation} from '@app/hooks/input'; import Autocomplete from '@components/autocomplete'; import ErrorText from '@components/error_text'; import FloatingTextInput from '@components/floating_text_input_label'; @@ -126,6 +127,8 @@ export default function ChannelInfoForm({ const dimensions = useWindowDimensions(); const isTablet = useIsTablet(); + const [propagateValue, shouldProcessEvent] = useInputPropagation(); + const keyboardHeight = useKeyboardHeight(); const [keyboardVisible, setKeyBoardVisible] = useState(false); const [scrollPosition, setScrollPosition] = useState(0); @@ -193,6 +196,18 @@ export default function ChannelInfoForm({ } }, [keyboardHeight]); + const onHeaderAutocompleteChange = useCallback((value: string) => { + onHeaderChange(value); + propagateValue(value); + }, [onHeaderChange]); + + const onHeaderInputChange = useCallback((value: string) => { + if (!shouldProcessEvent(value)) { + return; + } + onHeaderChange(value); + }, [onHeaderChange]); + const onLayoutError = useCallback((e: LayoutChangeEvent) => { setErrorHeight(e.nativeEvent.layout.height); }, []); @@ -371,7 +386,7 @@ export default function ChannelInfoForm({ enablesReturnKeyAutomatically={true} label={labelHeader} placeholder={placeholderHeader} - onChangeText={onHeaderChange} + onChangeText={onHeaderInputChange} multiline={true} keyboardAppearance={getKeyboardAppearanceFromTheme(theme)} returnKeyType='next' @@ -397,7 +412,7 @@ export default function ChannelInfoForm({ (); const [isUpdating, setIsUpdating] = useState(false); const [containerHeight, setContainerHeight] = useState(0); + const [propagateValue, shouldProcessEvent] = useInputPropagation(); const mainView = useRef(null); const modalPosition = useModalPosition(mainView); @@ -123,10 +125,8 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach }); }, [componentId, intl, theme]); - const onChangeText = useCallback((message: string) => { - setPostMessage(message); + const onChangeTextCommon = useCallback((message: string) => { const tooLong = message.trim().length > maxPostSize; - if (tooLong) { const line = intl.formatMessage({id: 'mobile.message_length.message_split_left', defaultMessage: 'Message exceeds the character limit'}); const extra = `${message.trim().length} / ${maxPostSize}`; @@ -134,7 +134,21 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach setErrorExtra(extra); } toggleSaveButton(post.message !== message); - }, [intl, maxPostSize, toggleSaveButton]); + }, [intl, maxPostSize, post.message, toggleSaveButton]); + + const onAutocompleteChangeText = useCallback((message: string) => { + setPostMessage(message); + propagateValue(message); + onChangeTextCommon(message); + }, [onChangeTextCommon]); + + const onInputChangeText = useCallback((message: string) => { + if (!shouldProcessEvent(message)) { + return; + } + setPostMessage(message); + onChangeTextCommon(message); + }, [onChangeTextCommon]); const handleUIUpdates = useCallback((res: {error?: unknown}) => { if (res.error) { @@ -233,7 +247,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach @@ -244,7 +258,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach hasFilesAttached={hasFilesAttached} nestedScrollEnabled={true} rootId={post.rootId} - updateValue={onChangeText} + updateValue={onAutocompleteChangeText} value={postMessage} cursorPosition={cursorPosition} position={animatedAutocompletePosition}