Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Chat] Workaround for autocomplete suggestion bug on iOS (#10321)

authored by

Samuel Newman and committed by
GitHub
bfdaab0a 3153ea43

+51 -11
+51 -11
src/screens/Messages/components/MessageComposer.tsx
··· 1 - import {useState} from 'react' 1 + import {useRef, useState} from 'react' 2 2 import {Pressable, View} from 'react-native' 3 3 import { 4 4 useKeyboardHandler, ··· 19 19 20 20 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 21 21 import {useHaptics} from '#/lib/haptics' 22 + import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 22 23 import {isBskyPostUrl} from '#/lib/strings/url-helpers' 23 24 import {useEmail} from '#/state/email-verification' 24 25 import { ··· 32 33 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji' 33 34 import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane' 34 35 import * as Toast from '#/components/Toast' 35 - import {IS_ANDROID, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' 36 + import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' 36 37 37 38 const MIN_HEIGHT = 40 38 39 ··· 61 62 useSaveMessageDraft(text) 62 63 63 64 // Android interactive dismiss sometimes doesn't blur the input 64 - const blur = () => { 65 + const blur = useNonReactiveCallback(() => { 65 66 composerInternalApiRef.current?.input?.blur() 66 - } 67 + }) 67 68 68 69 useKeyboardHandler({ 69 70 onEnd: evt => { ··· 76 77 77 78 const submitDisabled = !editable || (!hasEmbed && text.trim().length === 0) 78 79 79 - const onSubmit = () => { 80 + const onSubmit = (message: string) => { 80 81 if (!editable) return 81 - if (!hasEmbed && text.trim() === '') return 82 - const graphemeCount = countGraphemes(text) 82 + if (!hasEmbed && message.trim() === '') return 83 + const graphemeCount = countGraphemes(message) 83 84 if (graphemeCount > MAX_DM_GRAPHEME_LENGTH) { 84 85 Toast.show( 85 86 l`Message is too long (${graphemeCount}/${MAX_DM_GRAPHEME_LENGTH})`, ··· 89 90 } 90 91 91 92 clearDraft() 92 - onSendMessage(text) 93 + onSendMessage(message) 93 94 playHaptic() 94 95 setEmbed(undefined) 95 96 composerInternalApiRef.current?.clear() ··· 99 100 } 100 101 } 101 102 103 + const isFlushingAutocorrectSuggestion = useRef(false) 104 + const handleSubmit = () => { 105 + if (IS_IOS) { 106 + // HACKFIX: If there's a pending autocomplete suggestion, iOS will prioritize 107 + // accepting the suggestion over any imperative `.clear()` action on the textinput. 108 + // This means we'll send the message with the typo while the corrected text remains 109 + // in the composer's textinput. 110 + // 111 + // In MessageInput, the previous iteration, we simply sent it, and if another text change 112 + // event came in, we'd clear it again. However, it's nicer UX to actually accept the suggestion. 113 + // 114 + // Thus the solution: 115 + // 1. Set a ref indicating we're flushing the autocorrect suggestion 116 + // 2. Watch for incoming onChange events. If something comes in, it's almost certainly the corrected text, 117 + // so send that 118 + // 3. Meanwhile, race that against a simple timeout. If the timeout fires first, send the original text. 119 + // 120 + // Hopefully, it's delaying the send by no more than a couple frames -sfn 121 + isFlushingAutocorrectSuggestion.current = true 122 + setTimeout(() => { 123 + if (isFlushingAutocorrectSuggestion.current) { 124 + isFlushingAutocorrectSuggestion.current = false 125 + onSubmit(text) 126 + } 127 + }, 20) 128 + } else { 129 + onSubmit(text) 130 + } 131 + } 132 + 133 + const handleChange = (nextText: string) => { 134 + if (IS_IOS && isFlushingAutocorrectSuggestion.current) { 135 + isFlushingAutocorrectSuggestion.current = false 136 + onSubmit(nextText) 137 + } else { 138 + setText(nextText) 139 + } 140 + } 141 + 102 142 return ( 103 143 <ComposerContainer> 104 144 {children} ··· 180 220 paddingBottom: 10, 181 221 paddingRight: 16 + platform({web: 20, default: 0}), 182 222 }} 183 - onChange={setText} 223 + onChange={handleChange} 184 224 onFacetCommitted={facet => { 185 225 if (facet.type === 'url' && isBskyPostUrl(facet.value)) { 186 226 setEmbed(facet.value) ··· 189 229 onRequestSubmit={req => { 190 230 if (req.platform === 'web' && req.shiftKey) return 191 231 req.nativeEvent.preventDefault() 192 - onSubmit() 232 + handleSubmit() 193 233 }} 194 234 /> 195 235 </GlassView> 196 - <SubmitButton onPress={onSubmit} disabled={submitDisabled} /> 236 + <SubmitButton onPress={handleSubmit} disabled={submitDisabled} /> 197 237 </GlassContainer> 198 238 </View> 199 239 </ComposerContainer>