From 537a1f7120ee8a094904703edd5389be8f2b51e2 Mon Sep 17 00:00:00 2001
From: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Date: Mon, 25 May 2026 01:58:46 -0700
Subject: [PATCH] Update loading state for chats (#10596)
---
src/screens/Messages/Conversation.tsx | 33 +--
.../Messages/components/MessageComposer.tsx | 4 +-
.../Messages/components/MessageInput.web.tsx | 80 +++----
.../Messages/components/MessagesList.tsx | 197 ++++++++++--------
4 files changed, 163 insertions(+), 151 deletions(-)
diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx
index 512ebcdc5d..9966e5c59d 100644
--- a/src/screens/Messages/Conversation.tsx
+++ b/src/screens/Messages/Conversation.tsx
@@ -31,7 +31,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useConvoQuery} from '#/state/queries/messages/conversation'
import {useSession} from '#/state/session'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
-import {atoms as a, useTheme, web} from '#/alf'
+import {atoms as a, web} from '#/alf'
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import * as Dialog from '#/components/Dialog'
@@ -44,7 +44,6 @@ import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
import {Error} from '#/components/Error'
import * as Layout from '#/components/Layout'
-import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
@@ -100,7 +99,6 @@ export function MessagesConversationScreenInner({route}: Props) {
}
function Inner({convoId}: {convoId: string}) {
- const t = useTheme()
const convoState = useConvo()
const {t: l} = useLingui()
const {currentAccount} = useSession()
@@ -114,15 +112,7 @@ function Inner({convoId}: {convoId: string}) {
? parseConvoView(convoData, currentAccount?.did)
: null
- // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
- // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
- // empty. So, we also check for that possible state as well and render once we can.
const [hasScrolled, setHasScrolled] = useState(false)
- const readyToShow =
- hasScrolled ||
- (isConvoActive(convoState) &&
- !convoState.isFetchingHistory &&
- convoState.items.length === 0)
// Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
// state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
@@ -153,11 +143,6 @@ function Inner({convoId}: {convoId: string}) {
return (
- {!readyToShow && (
-
-
-
- )}
- {!readyToShow && (
-
-
-
-
-
- )}
)
diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx
index b486ceddc7..0aa132e232 100644
--- a/src/screens/Messages/components/MessageComposer.tsx
+++ b/src/screens/Messages/components/MessageComposer.tsx
@@ -165,7 +165,7 @@ export function MessageComposer({
fallbackStyle={[t.atoms.bg_contrast_50]}>
{children}
- {IS_WEB && (
+ {IS_WEB && !loading ? (
composerInternalApiRef.current?.insert(emoji.native)
@@ -207,7 +207,7 @@ export function MessageComposer({
- )}
+ ) : null}
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)}>
-
-
- {({props, state}) => (
-
- )}
-
-
-
+ ]}
+ label={props.accessibilityLabel}
+ {...props}>
+
+
+
+
+ )}
+
+
+
+ )}
{
+ 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 (
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
-
-
-
- {convoState.hasAllHistory ? (
- convoState.convo?.kind === 'group' ? (
-
- ) : (
-
- )
- ) : 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={
-
- }
- style={web({
- scrollbarWidth: 'thin',
- scrollbarColor: `${t.palette.contrast_100} transparent`,
- scrollbarGutter: 'stable',
- })}
- contentInset={{top: transparentHeaderHeight}}
- scrollIndicatorInsets={{top: transparentHeaderHeight}}
- />
-
+
+
+
+
+ {convoState.hasAllHistory ? (
+ convoState.convo?.kind === 'group' ? (
+
+ ) : (
+
+ )
+ ) : 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={
+
+ }
+ style={[
+ web({
+ scrollbarWidth: 'thin',
+ scrollbarColor: `${t.palette.contrast_100} transparent`,
+ scrollbarGutter: 'stable',
+ }),
+ ]}
+ pointerEvents={!hasScrolled ? 'none' : 'auto'}
+ contentInset={{top: transparentHeaderHeight}}
+ scrollIndicatorInsets={{top: transparentHeaderHeight}}
+ />
+
+
- {ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
-
- void onSendMessage(message)
- }
- hasEmbed={!!embedUri}
- setEmbed={setEmbed}>
-
-
- ) : (
-
-
-
- )}
+ {({loading}) =>
+ ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
+
+ void onSendMessage(message)
+ }
+ hasEmbed={!!embedUri}
+ setEmbed={setEmbed}
+ loading={loading}>
+
+
+ ) : (
+
+
+
+ )
+ }
)}
@@ -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
case 'standard':
- return children
+ return renderChildren()
}
}