diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -90,6 +90,7 @@ import type { LayoutEvent } from '../types/react-native'; import { type AnimatedViewStyle, AnimatedView } from '../types/styles'; import { runTiming } from '../utils/animation-utils'; +import { waitForAnimationFrameFlush } from '../utils/timers'; import { ChatContext } from './chat-context'; import type { ChatNavigationProp } from './chat.react'; @@ -722,20 +723,54 @@ }); }, 400); - focusAndUpdateText = (text: string) => { - const { textInput } = this; - if (!textInput) { - return; - } + focusAndUpdateTextAndSelection = (text: string, selection: Selection) => { + // On iOS, the update of text below will trigger a selection change which + // will overwrite our update of selection. To address this, we schedule a + // second update of selection after waiting for the animation frame to + // flush. We also make sure that the overwriting selection change is not + // saved to our selection state by ignoring selection updates until our + // second update. + this.setState( + { + text, + textEdited: true, + selection, + controlSelection: true, + }, + async () => { + if (Platform.OS !== 'ios') { + return; + } + await waitForAnimationFrameFlush(); + this.setState({ + selection, + controlSelection: true, + }); + }, + ); + this.saveDraft(text); + + this.focusAndUpdateButtonsVisibility(); + }; + focusAndUpdateText = (text: string) => { const currentText = this.state.text; if (!currentText.startsWith(text)) { const prependedText = text.concat(currentText); this.updateText(prependedText); - this.immediatelyShowSendButton(); - this.immediatelyHideButtons(); } + this.focusAndUpdateButtonsVisibility(); + }; + + focusAndUpdateButtonsVisibility = () => { + const { textInput } = this; + if (!textInput) { + return; + } + + this.immediatelyShowSendButton(); + this.immediatelyHideButtons(); textInput.focus(); };