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