Fix DMs input automatically accepting autocomplete suggestion instead of clearing (#7796)

* fix input not clearing when autocomplete suggestion active

* fix up storybook

* restore web focus hack
This commit is contained in:
Samuel Newman
2025-02-24 13:03:01 -08:00
committed by GitHub
parent 9c739912a3
commit 7c14b54ab2
3 changed files with 59 additions and 40 deletions
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useState} from 'react'
import {Pressable, TextInput, useWindowDimensions, View} from 'react-native' import {Pressable, TextInput, useWindowDimensions, View} from 'react-native'
import { import {
useFocusedInputHandler, useFocusedInputHandler,
@@ -19,7 +19,7 @@ import Graphemer from 'graphemer'
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useEmail} from '#/lib/hooks/useEmail' import {useEmail} from '#/lib/hooks/useEmail'
import {isIOS} from '#/platform/detection' import {isIOS, isWeb} from '#/platform/detection'
import { import {
useMessageDraft, useMessageDraft,
useSaveMessageDraft, useSaveMessageDraft,
@@ -58,16 +58,17 @@ export function MessageInput({
const isInputScrollable = useSharedValue(false) const isInputScrollable = useSharedValue(false)
const inputStyles = useSharedInputStyles() const inputStyles = useSharedInputStyles()
const [isFocused, setIsFocused] = React.useState(false) const [isFocused, setIsFocused] = useState(false)
const [message, setMessage] = React.useState(getDraft) const [message, setMessage] = useState(getDraft)
const inputRef = useAnimatedRef<TextInput>() const inputRef = useAnimatedRef<TextInput>()
const [shouldEnforceClear, setShouldEnforceClear] = useState(false)
const {needsEmailVerification} = useEmail() const {needsEmailVerification} = useEmail()
useSaveMessageDraft(message) useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed) useExtractEmbedFromFacets(message, setEmbed)
const onSubmit = React.useCallback(() => { const onSubmit = useCallback(() => {
if (needsEmailVerification) { if (needsEmailVerification) {
return return
} }
@@ -81,14 +82,18 @@ export function MessageInput({
clearDraft() clearDraft()
onSendMessage(message) onSendMessage(message)
playHaptic() playHaptic()
setMessage('')
setEmbed(undefined) setEmbed(undefined)
setMessage('')
// Pressing the send button causes the text input to lose focus, so we need to if (isIOS) {
// re-focus it after sending setShouldEnforceClear(true)
setTimeout(() => { }
inputRef.current?.focus() if (isWeb) {
}, 100) // Pressing the send button causes the text input to lose focus, so we need to
// re-focus it after sending
setTimeout(() => {
inputRef.current?.focus()
}, 100)
}
}, [ }, [
needsEmailVerification, needsEmailVerification,
hasEmbed, hasEmbed,
@@ -97,8 +102,8 @@ export function MessageInput({
onSendMessage, onSendMessage,
playHaptic, playHaptic,
setEmbed, setEmbed,
_,
inputRef, inputRef,
_,
]) ])
useFocusedInputHandler( useFocusedInputHandler(
@@ -149,8 +154,21 @@ export function MessageInput({
placeholder={_(msg`Write a message`)} placeholder={_(msg`Write a message`)}
placeholderTextColor={t.palette.contrast_500} placeholderTextColor={t.palette.contrast_500}
value={message} value={message}
onChange={evt => {
// bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen
// including the button we just pressed - and this overrides clearing the input! so we watch for the
// next change and double make sure the input is cleared. It should *always* send an onChange event after
// clearing via setMessage('') that happens in onSubmit()
// -sfn
if (isIOS && shouldEnforceClear) {
setShouldEnforceClear(false)
setMessage('')
return
}
const text = evt.nativeEvent.text
setMessage(text)
}}
multiline={true} multiline={true}
onChangeText={setMessage}
style={[ style={[
a.flex_1, a.flex_1,
a.text_md, a.text_md,
@@ -160,7 +178,7 @@ export function MessageInput({
animatedStyle, animatedStyle,
]} ]}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
blurOnSubmit={false} submitBehavior="submit"
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
ref={inputRef} ref={inputRef}
+11 -8
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {TextInput, View} from 'react-native'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -19,6 +19,8 @@ export function Forms() {
const [value, setValue] = React.useState('') const [value, setValue] = React.useState('')
const [date, setDate] = React.useState('2001-01-01') const [date, setDate] = React.useState('2001-01-01')
const inputRef = React.useRef<TextInput>(null)
return ( return (
<View style={[a.gap_4xl, a.align_start]}> <View style={[a.gap_4xl, a.align_start]}>
<H1>Forms</H1> <H1>Forms</H1>
@@ -33,22 +35,23 @@ export function Forms() {
/> />
<View style={[a.flex_row, a.align_start, a.gap_sm]}> <View style={[a.flex_row, a.align_start, a.gap_sm]}>
<View <View style={[a.flex_1]}>
style={[
{
width: '50%',
},
]}>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Globe} /> <TextField.Icon icon={Globe} />
<TextField.Input <TextField.Input
inputRef={inputRef}
value={value} value={value}
onChangeText={setValue} onChangeText={setValue}
label="Text field" label="Text field"
/> />
</TextField.Root> </TextField.Root>
</View> </View>
<Button label="Submit" size="large" variant="solid" color="primary"> <Button
label="Submit"
size="large"
variant="solid"
color="primary"
onPress={() => inputRef.current?.clear()}>
<ButtonText>Submit</ButtonText> <ButtonText>Submit</ButtonText>
</Button> </Button>
</View> </View>
+15 -17
View File
@@ -1,13 +1,11 @@
import React from 'react' import React from 'react'
import {ScrollView, View} from 'react-native' import {View} from 'react-native'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection'
import {useSetThemePrefs} from '#/state/shell' import {useSetThemePrefs} from '#/state/shell'
import {CenteredView} from '#/view/com/util/Views'
import {ListContained} from '#/view/screens/Storybook/ListContained' import {ListContained} from '#/view/screens/Storybook/ListContained'
import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {atoms as a, ThemeProvider} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Admonitions} from './Admonitions' import {Admonitions} from './Admonitions'
@@ -27,25 +25,27 @@ import {Typography} from './Typography'
export function Storybook() { export function Storybook() {
return ( return (
<Layout.Screen> <Layout.Screen>
{isWeb ? ( <Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>Storybook</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content keyboardShouldPersistTaps="handled">
<StorybookInner /> <StorybookInner />
) : ( </Layout.Content>
<ScrollView>
<StorybookInner />
</ScrollView>
)}
</Layout.Screen> </Layout.Screen>
) )
} }
function StorybookInner() { function StorybookInner() {
const t = useTheme()
const {setColorMode, setDarkTheme} = useSetThemePrefs() const {setColorMode, setDarkTheme} = useSetThemePrefs()
const [showContainedList, setShowContainedList] = React.useState(false) const [showContainedList, setShowContainedList] = React.useState(false)
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
return ( return (
<CenteredView style={[t.atoms.bg]}> <>
<View style={[a.p_xl, a.gap_5xl, {paddingBottom: 100}]}> <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 100}]}>
{!showContainedList ? ( {!showContainedList ? (
<> <>
@@ -100,10 +100,6 @@ function StorybookInner() {
<ButtonText>Open Shared Prefs Tester</ButtonText> <ButtonText>Open Shared Prefs Tester</ButtonText>
</Button> </Button>
<Admonitions />
<Settings />
<ThemeProvider theme="light"> <ThemeProvider theme="light">
<Theming /> <Theming />
</ThemeProvider> </ThemeProvider>
@@ -126,6 +122,8 @@ function StorybookInner() {
<Menus /> <Menus />
<Breakpoints /> <Breakpoints />
<Dialogs /> <Dialogs />
<Admonitions />
<Settings />
<Button <Button
variant="solid" variant="solid"
@@ -150,6 +148,6 @@ function StorybookInner() {
</> </>
)} )}
</View> </View>
</CenteredView> </>
) )
} }