Update loading state for chats (#10596)
This commit is contained in:
@@ -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 (
|
||||
<Layout.Center style={[a.flex_1]}>
|
||||
{!readyToShow && (
|
||||
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
|
||||
<MessagesListHeader convo={convo} />
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.flex_1]}>
|
||||
<InnerReady
|
||||
convo={convo}
|
||||
@@ -166,22 +151,6 @@ function Inner({convoId}: {convoId: string}) {
|
||||
isActive={isConvoActive(convoState)}
|
||||
isDisabled={convoState.status === ConvoStatus.Disabled}
|
||||
/>
|
||||
{!readyToShow && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<View style={[{marginBottom: 75}]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Layout.Center>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user