[🐴] Remove extra spinner states from chat screen (#3947)
* remove extra loading states from chat * nits * fix scrolling animation to bottom * nit * move spinner to topzio/stable
parent
195c9f1045
commit
1a90426026
|
@ -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} />
|
||||||
|
|
|
@ -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,
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in New Issue