Update loading state for chats (#10596)

This commit is contained in:
DS Boyce
2026-05-25 01:58:46 -07:00
committed by GitHub
parent bf83f165ee
commit 537a1f7120
4 changed files with 163 additions and 151 deletions
@@ -165,7 +165,7 @@ export function MessageComposer({
fallbackStyle={[t.atoms.bg_contrast_50]}>
{children}
<View style={[a.flex_1]}>
{IS_WEB && (
{IS_WEB && !loading ? (
<EmojiPicker.Root
onEmojiSelect={emoji =>
composerInternalApiRef.current?.insert(emoji.native)
@@ -207,7 +207,7 @@ export function MessageComposer({
</EmojiPicker.Trigger>
<EmojiPicker.Picker />
</EmojiPicker.Root>
)}
) : null}
<Composer
nativeID={textInputId}
@@ -26,11 +26,13 @@ export function MessageInput({
hasEmbed,
setEmbed,
children,
loading = false,
}: {
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
loading?: boolean
}) {
const {isMobile} = useWebMediaQueries()
const {t: l} = useLingui()
@@ -139,47 +141,50 @@ export function MessageInput({
// @ts-expect-error web only
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}>
<EmojiPicker.Root
onEmojiSelect={onEmojiInserted}
nextFocusRef={textAreaRef}>
<EmojiPicker.Trigger label={l`Open emoji picker`}>
{({props, state}) => (
<Button
style={[
a.rounded_full,
a.overflow_hidden,
a.align_center,
a.justify_center,
{
marginTop: 5,
height: 30,
width: 30,
},
]}
label={props.accessibilityLabel}
{...props}>
<View
{loading ? null : (
<EmojiPicker.Root
onEmojiSelect={onEmojiInserted}
nextFocusRef={textAreaRef}>
<EmojiPicker.Trigger label={l`Open emoji picker`}>
{({props, state}) => (
<Button
style={[
a.absolute,
a.inset_0,
a.rounded_full,
a.overflow_hidden,
a.align_center,
a.justify_center,
{
backgroundColor:
state.hovered || state.focused || state.pressed
? t.atoms.bg.backgroundColor
: undefined,
marginTop: 5,
height: 30,
width: 30,
},
]}>
<EmojiSmile size="lg" />
</View>
</Button>
)}
</EmojiPicker.Trigger>
<EmojiPicker.Picker />
</EmojiPicker.Root>
]}
label={props.accessibilityLabel}
{...props}>
<View
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
backgroundColor:
state.hovered || state.focused || state.pressed
? t.atoms.bg.backgroundColor
: undefined,
},
]}>
<EmojiSmile size="lg" />
</View>
</Button>
)}
</EmojiPicker.Trigger>
<EmojiPicker.Picker />
</EmojiPicker.Root>
)}
<TextareaAutosize
ref={textAreaRef}
disabled={loading}
style={flatten([
a.flex_1,
a.px_sm,
@@ -192,7 +197,11 @@ export function MessageInput({
},
])}
maxRows={12}
placeholder={l`Message`}
placeholder={
loading
? l({message: 'Loading chat…', context: 'placeholder'})
: l({message: 'Message', context: 'action'})
}
defaultValue=""
value={message}
dirName="ltr"
@@ -215,6 +224,7 @@ export function MessageInput({
accessibilityRole="button"
accessibilityLabel={l`Send message`}
accessibilityHint=""
disabled={loading}
style={[
a.rounded_full,
a.align_center,
+115 -82
View File
@@ -12,13 +12,15 @@ import {
type KeyboardChatScrollViewProps,
KeyboardGestureArea,
} from 'react-native-keyboard-controller'
import {
import Animated, {
runOnJS,
type ScrollEvent,
type SharedValue,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
@@ -157,6 +159,16 @@ export function MessagesList({
startContentOffset: 0,
})
const listOpacity = useSharedValue(0)
useEffect(() => {
if (hasScrolled) {
listOpacity.set(withTiming(1, {duration: 200}))
} else {
listOpacity.set(0)
}
}, [hasScrolled, listOpacity])
const inputHeightUI = useSharedValue(0)
const [inputHeightJS, setInputHeightJS] = useState(0)
@@ -476,6 +488,10 @@ export function MessagesList({
[inputHeightUI],
)
const animatedListStyle = useAnimatedStyle(() => ({
opacity: listOpacity.get(),
}))
return (
<InviteLinkDialogProvider convo={convoState.convo}>
<KeyboardGestureArea
@@ -486,64 +502,69 @@ export function MessagesList({
// textInputNativeID={textInputId}
style={[a.flex_1]}>
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
<ScrollProvider onScroll={onScroll}>
<List
ref={flatListRef}
data={renderItems}
renderItem={renderItem}
keyExtractor={keyExtractor}
disableFullWindowScroll={true}
disableVirtualization={true}
// The extra two items account for the header and the footer components
initialNumToRender={IS_NATIVE ? 32 : 62}
maxToRenderPerBatch={IS_WEB ? 32 : 62}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{minIndexForVisible: 0}}
removeClippedSubviews={false}
sideBorders={false}
onContentSizeChange={onContentSizeChange}
onStartReached={onStartReached}
onScrollToIndexFailed={onScrollToIndexFailed}
showsVerticalScrollIndicator={!IS_ANDROID}
scrollEventThrottle={100}
ListHeaderComponent={
<>
<MaybeLoader isLoading={convoState.isFetchingHistory} />
{convoState.hasAllHistory ? (
convoState.convo?.kind === 'group' ? (
<MessagesListGroupInfoPanel convo={convoState.convo} />
) : (
<MessagesListInfoPanel convo={convoState.convo} />
)
) : null}
</>
}
// native only (prop is not supported on web)
renderScrollComponent={renderScrollComponent}
contentContainerStyle={{
paddingBottom: platform({
// ios is slightly larger as the input has no top padding
ios: tokens.space.lg,
android: tokens.space.md,
web: 0, // web uses ListFooterComponent instead for scroll reasons
}),
}}
ListFooterComponent={
<View
style={web({height: tokens.space.md + inputHeightJS})}
onLayout={onFooterLayout}
/>
}
style={web({
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_100} transparent`,
scrollbarGutter: 'stable',
})}
contentInset={{top: transparentHeaderHeight}}
scrollIndicatorInsets={{top: transparentHeaderHeight}}
/>
</ScrollProvider>
<Animated.View style={[a.flex_1, animatedListStyle]}>
<ScrollProvider onScroll={onScroll}>
<List
ref={flatListRef}
data={renderItems}
renderItem={renderItem}
keyExtractor={keyExtractor}
disableFullWindowScroll={true}
disableVirtualization={true}
// The extra two items account for the header and the footer components
initialNumToRender={IS_NATIVE ? 32 : 62}
maxToRenderPerBatch={IS_WEB ? 32 : 62}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{minIndexForVisible: 0}}
removeClippedSubviews={false}
sideBorders={false}
onContentSizeChange={onContentSizeChange}
onStartReached={onStartReached}
onScrollToIndexFailed={onScrollToIndexFailed}
showsVerticalScrollIndicator={!IS_ANDROID}
scrollEventThrottle={100}
ListHeaderComponent={
<>
<MaybeLoader isLoading={convoState.isFetchingHistory} />
{convoState.hasAllHistory ? (
convoState.convo?.kind === 'group' ? (
<MessagesListGroupInfoPanel convo={convoState.convo} />
) : (
<MessagesListInfoPanel convo={convoState.convo} />
)
) : null}
</>
}
// native only (prop is not supported on web)
renderScrollComponent={renderScrollComponent}
contentContainerStyle={{
paddingBottom: platform({
// ios is slightly larger as the input has no top padding
ios: tokens.space.lg,
android: tokens.space.md,
web: 0, // web uses ListFooterComponent instead for scroll reasons
}),
}}
ListFooterComponent={
<View
style={web({height: tokens.space.md + inputHeightJS})}
onLayout={onFooterLayout}
/>
}
style={[
web({
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_100} transparent`,
scrollbarGutter: 'stable',
}),
]}
pointerEvents={!hasScrolled ? 'none' : 'auto'}
contentInset={{top: transparentHeaderHeight}}
scrollIndicatorInsets={{top: transparentHeaderHeight}}
/>
</ScrollProvider>
</Animated.View>
<KeyboardStickyView
style={[a.absolute, a.bottom_0, a.left_0, a.right_0]}
onLayout={onInputLayout}
@@ -559,25 +580,35 @@ export function MessagesList({
<ConversationFooter
convoState={convoState}
hasAcceptOverride={hasAcceptOverride}>
{ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
<MessageComposer
textInputId={textInputId}
onSendMessage={(message: string) =>
void onSendMessage(message)
}
hasEmbed={!!embedUri}
setEmbed={setEmbed}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageComposer>
) : (
<MessageInput
textInputId={textInputId}
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageInput>
)}
{({loading}) =>
ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
<MessageComposer
textInputId={textInputId}
onSendMessage={(message: string) =>
void onSendMessage(message)
}
hasEmbed={!!embedUri}
setEmbed={setEmbed}
loading={loading}>
<MessageInputEmbed
embedUri={embedUri}
setEmbed={setEmbed}
/>
</MessageComposer>
) : (
<MessageInput
textInputId={textInputId}
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}
loading={loading}>
<MessageInputEmbed
embedUri={embedUri}
setEmbed={setEmbed}
/>
</MessageInput>
)
}
</ConversationFooter>
)}
</KeyboardStickyView>
@@ -657,23 +688,25 @@ function ConversationFooter({
}: {
convoState: ConvoState
hasAcceptOverride?: boolean
children?: React.ReactNode // message input
children?: ((props: {loading?: boolean}) => React.ReactNode) | React.ReactNode
}) {
if (!isConvoActive(convoState)) {
return null
}
const footerState = getFooterState(convoState, hasAcceptOverride)
const renderChildren = (loading?: boolean) =>
typeof children === 'function' ? children({loading}) : children
switch (footerState) {
case 'loading':
return null
return renderChildren(true)
case 'new-chat':
// new chat pill goes here - removed for now
return children
return renderChildren()
case 'request':
return <ChatStatusInfo convoState={convoState} />
case 'standard':
return children
return renderChildren()
}
}