Add loading state to message composer and input (#10595)

This commit is contained in:
DS Boyce
2026-05-22 13:57:19 -07:00
committed by GitHub
parent 0d14513939
commit 6159cd7777
2 changed files with 43 additions and 15 deletions
@@ -32,6 +32,7 @@ import * as EmojiPicker from '#/components/EmojiPicker'
import {GlassView} from '#/components/GlassView'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
@@ -43,18 +44,20 @@ export function MessageComposer({
hasEmbed,
setEmbed,
children,
loading = false,
}: {
textInputId?: string
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
loading?: boolean
}) {
const t = useTheme()
const {t: l} = useLingui()
const playHaptic = useHaptics()
const {needsEmailVerification} = useEmail()
const editable = !needsEmailVerification
const editable = !needsEmailVerification && !loading
const {getDraft, clearDraft} = useMessageDraft()
const composerInternalApiRef = useComposerInternalApiRef()
@@ -209,7 +212,11 @@ export function MessageComposer({
<Composer
nativeID={textInputId}
label={l`Message input field`}
placeholder={l`Message`}
placeholder={
loading
? l({message: 'Loading chat…', context: 'placeholder'})
: l({message: 'Message', context: 'action'})
}
autocompletePlacement="top-start"
internalApiRef={composerInternalApiRef}
defaultValue={text}
@@ -238,7 +245,11 @@ export function MessageComposer({
/>
</View>
</GlassView>
<SubmitButton onPress={handleSubmit} disabled={submitDisabled} />
<SubmitButton
onPress={handleSubmit}
disabled={submitDisabled}
loading={loading}
/>
</GlassContainer>
</View>
</ComposerContainer>
@@ -248,9 +259,11 @@ export function MessageComposer({
function SubmitButton({
onPress,
disabled,
loading,
}: {
onPress: () => void
disabled: boolean
loading: boolean
}) {
const {t: l} = useLingui()
const t = useTheme()
@@ -279,7 +292,11 @@ function SubmitButton({
]}
onPress={onPress}
disabled={disabled}>
<PaperPlaneIcon size="md" fill={t.palette.white} style={[a.mb_2xs]} />
{loading ? (
<Loader size="md" fill={t.palette.white} style={[a.mb_2xs]} />
) : (
<PaperPlaneIcon size="md" fill={t.palette.white} style={[a.mb_2xs]} />
)}
</Pressable>
</GlassView>
)
@@ -28,6 +28,7 @@ import {
import {atoms as a, platform, tokens, useTheme} from '#/alf'
import {GlassView} from '#/components/GlassView'
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
import {ComposerContainer} from './MessageComposer'
@@ -43,12 +44,14 @@ export function MessageInput({
hasEmbed,
setEmbed,
children,
loading = false,
}: {
textInputId?: string
onSendMessage: (message: string) => Promise<void> | void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
loading?: boolean
}) {
const {t: l} = useLingui()
const t = useTheme()
@@ -67,12 +70,13 @@ export function MessageInput({
const [shouldEnforceClear, setShouldEnforceClear] = useState(false)
const {needsEmailVerification} = useEmail()
const editable = !needsEmailVerification && !loading
useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)
const onSubmit = useCallback(() => {
if (needsEmailVerification) {
if (!editable) {
return
}
if (!hasEmbed && message.trim() === '') {
@@ -103,7 +107,7 @@ export function MessageInput({
void onSendMessage(message)
})
}, [
needsEmailVerification,
editable,
hasEmbed,
message,
clearDraft,
@@ -139,8 +143,7 @@ export function MessageInput({
scrollEnabled: isInputScrollable.get(),
}))
const submitDisabled =
needsEmailVerification || (!hasEmbed && message.trim().length === 0)
const submitDisabled = !editable || (!hasEmbed && message.trim().length === 0)
const blur = useCallback(() => {
inputRef.current?.blur()
@@ -209,7 +212,7 @@ export function MessageInput({
ref={inputRef}
hitSlop={HITSLOP_10}
animatedProps={animatedProps}
editable={!needsEmailVerification}
editable={editable}
/>
</GlassView>
<GlassView
@@ -226,7 +229,11 @@ export function MessageInput({
}}>
<Pressable
accessibilityRole="button"
accessibilityLabel={l`Send message`}
accessibilityLabel={
loading
? l({message: 'Loading chat…', context: 'placeholder'})
: l({message: 'Message', context: 'action'})
}
accessibilityHint=""
hitSlop={HITSLOP_10}
style={[
@@ -240,11 +247,15 @@ export function MessageInput({
]}
onPress={onSubmit}
disabled={submitDisabled}>
<PaperPlaneIcon
size="md"
fill={t.palette.white}
style={[a.mb_2xs]}
/>
{loading ? (
<Loader size="md" fill={t.palette.white} style={[a.mb_2xs]} />
) : (
<PaperPlaneIcon
size="md"
fill={t.palette.white}
style={[a.mb_2xs]}
/>
)}
</Pressable>
</GlassView>
</GlassContainer>