[🐴] Remove extra spinner states from chat screen (#3947)

* remove extra loading states from chat

* nits

* fix scrolling animation to bottom

* nit

* move spinner to top
zio/stable
Hailey 2024-05-10 07:49:08 -07:00 committed by GitHub
parent 195c9f1045
commit 1a90426026
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 79 additions and 32 deletions

View File

@ -10,7 +10,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import {shortenLinks} from '#/lib/strings/rich-text-manip' import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {isIOS} from '#/platform/detection' import {isIOS, isNative} from '#/platform/detection'
import {useConvo} from '#/state/messages/convo' import {useConvo} from '#/state/messages/convo'
import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
@ -85,7 +85,7 @@ export function MessagesList() {
// Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not. // Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not.
const isMomentumScrolling = useSharedValue(false) const isMomentumScrolling = useSharedValue(false)
const [hasInitiallyScrolled, setHasInitiallyScrolled] = React.useState(false) const hasInitiallyScrolled = useSharedValue(false)
// Every time the content size changes, that means one of two things is happening: // Every time the content size changes, that means one of two things is happening:
// 1. New messages are being added from the log or from a message you have sent // 1. New messages are being added from the log or from a message you have sent
@ -101,7 +101,7 @@ export function MessagesList() {
(_: number, height: number) => { (_: number, height: number) => {
// Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
// previous offset whenever we add new content to the previous offset whenever we add new content to the list. // previous offset whenever we add new content to the previous offset whenever we add new content to the list.
if (isWeb && isAtTop.value && hasInitiallyScrolled) { if (isWeb && isAtTop.value && hasInitiallyScrolled.value) {
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
animated: false, animated: false,
offset: height - contentHeight.value, offset: height - contentHeight.value,
@ -116,7 +116,7 @@ export function MessagesList() {
} }
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
animated: hasInitiallyScrolled, animated: hasInitiallyScrolled.value,
offset: height, offset: height,
}) })
isMomentumScrolling.value = true isMomentumScrolling.value = true
@ -133,7 +133,7 @@ export function MessagesList() {
// The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached` // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached`
// immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls. // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls.
const onStartReached = useCallback(() => { const onStartReached = useCallback(() => {
if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled) { if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled.value) {
convo.fetchMessageHistory() convo.fetchMessageHistory()
} }
}, [convo, hasInitiallyScrolled]) }, [convo, hasInitiallyScrolled])
@ -178,8 +178,8 @@ export function MessagesList() {
// This number _must_ be the height of the MaybeLoader component. // This number _must_ be the height of the MaybeLoader component.
// We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which // We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which
// adds a 50 pixel offset. // adds a 50 pixel offset.
if (contentHeight.value > 50 && !hasInitiallyScrolled) { if (contentHeight.value > 50 && !hasInitiallyScrolled.value) {
runOnJS(setHasInitiallyScrolled)(true) hasInitiallyScrolled.value = true
} }
}, },
[contentHeight.value, hasInitiallyScrolled, isAtBottom, isAtTop], [contentHeight.value, hasInitiallyScrolled, isAtBottom, isAtTop],
@ -228,17 +228,20 @@ export function MessagesList() {
data={convo.items} data={convo.items}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
containWeb={true}
contentContainerStyle={{
paddingHorizontal: 10,
}}
disableVirtualization={true} disableVirtualization={true}
initialNumToRender={isWeb ? 50 : 25} initialNumToRender={isNative ? 30 : 60}
maxToRenderPerBatch={isWeb ? 50 : 25} maxToRenderPerBatch={isWeb ? 30 : 60}
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{ maintainVisibleContentPosition={{
minIndexForVisible: 1, minIndexForVisible: 1,
}} }}
containWeb={true}
contentContainerStyle={{paddingHorizontal: 10}}
removeClippedSubviews={false} removeClippedSubviews={false}
sideBorders={false}
onContentSizeChange={onContentSizeChange} onContentSizeChange={onContentSizeChange}
onStartReached={onStartReached} onStartReached={onStartReached}
onScrollToIndexFailed={onScrollToIndexFailed} onScrollToIndexFailed={onScrollToIndexFailed}
@ -246,7 +249,6 @@ export function MessagesList() {
ListHeaderComponent={ ListHeaderComponent={
<MaybeLoader isLoading={convo.isFetchingHistory} /> <MaybeLoader isLoading={convo.isFetchingHistory} />
} }
sideBorders={false}
/> />
</ScrollProvider> </ScrollProvider>
<MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} />

View File

@ -22,6 +22,7 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {ConvoMenu} from '#/components/dms/ConvoMenu' import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Error} from '#/components/Error' import {Error} from '#/components/Error'
import {ListMaybePlaceholder} from '#/components/Lists' import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {ClipClopGate} from '../gate' import {ClipClopGate} from '../gate'
@ -53,20 +54,27 @@ export function MessagesConversationScreen({route}: Props) {
} }
function Inner() { function Inner() {
const t = useTheme()
const convo = useConvo() const convo = useConvo()
const {_} = useLingui() const {_} = useLingui()
const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false)
// HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have
// to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause
// a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent
// this, we will wait until the first render has completed to remove the loading overlay.
React.useEffect(() => {
if ( if (
convo.status === ConvoStatus.Uninitialized || !hasInitiallyRendered &&
convo.status === ConvoStatus.Initializing convo.status === ConvoStatus.Ready &&
!convo.isFetchingHistory
) { ) {
return ( setTimeout(() => {
<CenteredView style={a.flex_1} sideBorders> setHasInitiallyRendered(true)
<Header /> }, 15)
<ListMaybePlaceholder isLoading />
</CenteredView>
)
} }
}, [convo.isFetchingHistory, convo.items, convo.status, hasInitiallyRendered])
if (convo.status === ConvoStatus.Error) { if (convo.status === ConvoStatus.Error) {
return ( return (
@ -88,8 +96,30 @@ function Inner() {
return ( return (
<KeyboardProvider> <KeyboardProvider>
<CenteredView style={a.flex_1} sideBorders> <CenteredView style={a.flex_1} sideBorders>
<Header profile={convo.recipients[0]} /> <Header profile={convo.recipients?.[0]} />
<View style={[a.flex_1]}>
{convo.status !== ConvoStatus.Ready ? (
<ListMaybePlaceholder isLoading />
) : (
<MessagesList /> <MessagesList />
)}
{!hasInitiallyRendered && (
<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>
</CenteredView> </CenteredView>
</KeyboardProvider> </KeyboardProvider>
) )
@ -128,7 +158,8 @@ let Header = ({
a.justify_between, a.justify_between,
a.align_start, a.align_start,
a.gap_lg, a.gap_lg,
a.px_lg, a.pl_xl,
a.pr_lg,
a.py_sm, a.py_sm,
]}> ]}>
{!gtTablet ? ( {!gtTablet ? (
@ -154,12 +185,19 @@ let Header = ({
)} )}
<View style={[a.align_center, a.gap_sm, a.flex_1]}> <View style={[a.align_center, a.gap_sm, a.flex_1]}>
{profile ? ( {profile ? (
<> <View style={[a.align_center]}>
<PreviewableUserAvatar size={32} profile={profile} /> <PreviewableUserAvatar size={32} profile={profile} />
<Text style={[a.text_lg, a.font_bold, a.text_center]}> <Text
style={[a.text_lg, a.font_bold, isWeb ? a.mt_md : a.mt_sm]}
numberOfLines={1}>
{profile.displayName} {profile.displayName}
</Text> </Text>
</> <Text
style={[t.atoms.text_contrast_medium, {fontSize: 15}]}
numberOfLines={1}>
@{profile.handle}
</Text>
</View>
) : ( ) : (
<> <>
<View <View
@ -171,10 +209,17 @@ let Header = ({
/> />
<View <View
style={[ style={[
{width: 120, height: 18}, {width: 120, height: 16},
a.rounded_xs,
t.atoms.bg_contrast_25,
a.mt_xs,
]}
/>
<View
style={[
{width: 175, height: 12},
a.rounded_xs, a.rounded_xs,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
a.mb_2xs,
]} ]}
/> />
</> </>

View File

@ -554,7 +554,7 @@ export class Convo {
{ {
cursor: nextCursor, cursor: nextCursor,
convoId: this.convoId, convoId: this.convoId,
limit: isNative ? 40 : 60, limit: isNative ? 30 : 60,
}, },
{ {
headers: { headers: {