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() } }