···1010import {AppBskyRichtextFacet, RichText} from '@atproto/api'
11111212import {shortenLinks} from '#/lib/strings/rich-text-manip'
1313-import {isIOS} from '#/platform/detection'
1313+import {isIOS, isNative} from '#/platform/detection'
1414import {useConvo} from '#/state/messages/convo'
1515import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
1616import {useAgent} from '#/state/session'
···8585 // Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not.
8686 const isMomentumScrolling = useSharedValue(false)
87878888- const [hasInitiallyScrolled, setHasInitiallyScrolled] = React.useState(false)
8888+ const hasInitiallyScrolled = useSharedValue(false)
89899090 // Every time the content size changes, that means one of two things is happening:
9191 // 1. New messages are being added from the log or from a message you have sent
···101101 (_: number, height: number) => {
102102 // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
103103 // previous offset whenever we add new content to the previous offset whenever we add new content to the list.
104104- if (isWeb && isAtTop.value && hasInitiallyScrolled) {
104104+ if (isWeb && isAtTop.value && hasInitiallyScrolled.value) {
105105 flatListRef.current?.scrollToOffset({
106106 animated: false,
107107 offset: height - contentHeight.value,
···116116 }
117117118118 flatListRef.current?.scrollToOffset({
119119- animated: hasInitiallyScrolled,
119119+ animated: hasInitiallyScrolled.value,
120120 offset: height,
121121 })
122122 isMomentumScrolling.value = true
···133133 // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached`
134134 // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls.
135135 const onStartReached = useCallback(() => {
136136- if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled) {
136136+ if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled.value) {
137137 convo.fetchMessageHistory()
138138 }
139139 }, [convo, hasInitiallyScrolled])
···178178 // This number _must_ be the height of the MaybeLoader component.
179179 // We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which
180180 // adds a 50 pixel offset.
181181- if (contentHeight.value > 50 && !hasInitiallyScrolled) {
182182- runOnJS(setHasInitiallyScrolled)(true)
181181+ if (contentHeight.value > 50 && !hasInitiallyScrolled.value) {
182182+ hasInitiallyScrolled.value = true
183183 }
184184 },
185185 [contentHeight.value, hasInitiallyScrolled, isAtBottom, isAtTop],
···228228 data={convo.items}
229229 renderItem={renderItem}
230230 keyExtractor={keyExtractor}
231231+ containWeb={true}
232232+ contentContainerStyle={{
233233+ paddingHorizontal: 10,
234234+ }}
231235 disableVirtualization={true}
232232- initialNumToRender={isWeb ? 50 : 25}
233233- maxToRenderPerBatch={isWeb ? 50 : 25}
236236+ initialNumToRender={isNative ? 30 : 60}
237237+ maxToRenderPerBatch={isWeb ? 30 : 60}
234238 keyboardDismissMode="on-drag"
235239 keyboardShouldPersistTaps="handled"
236240 maintainVisibleContentPosition={{
237241 minIndexForVisible: 1,
238242 }}
239239- containWeb={true}
240240- contentContainerStyle={{paddingHorizontal: 10}}
241243 removeClippedSubviews={false}
244244+ sideBorders={false}
242245 onContentSizeChange={onContentSizeChange}
243246 onStartReached={onStartReached}
244247 onScrollToIndexFailed={onScrollToIndexFailed}
···246249 ListHeaderComponent={
247250 <MaybeLoader isLoading={convo.isFetchingHistory} />
248251 }
249249- sideBorders={false}
250252 />
251253 </ScrollProvider>
252254 <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} />
+64-19
src/screens/Messages/Conversation/index.tsx
···2222import {ConvoMenu} from '#/components/dms/ConvoMenu'
2323import {Error} from '#/components/Error'
2424import {ListMaybePlaceholder} from '#/components/Lists'
2525+import {Loader} from '#/components/Loader'
2526import {Text} from '#/components/Typography'
2627import {ClipClopGate} from '../gate'
2728···5354}
54555556function Inner() {
5757+ const t = useTheme()
5658 const convo = useConvo()
5759 const {_} = useLingui()
58605959- if (
6060- convo.status === ConvoStatus.Uninitialized ||
6161- convo.status === ConvoStatus.Initializing
6262- ) {
6363- return (
6464- <CenteredView style={a.flex_1} sideBorders>
6565- <Header />
6666- <ListMaybePlaceholder isLoading />
6767- </CenteredView>
6868- )
6969- }
6161+ const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false)
6262+6363+ // HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have
6464+ // to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause
6565+ // a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent
6666+ // this, we will wait until the first render has completed to remove the loading overlay.
6767+ React.useEffect(() => {
6868+ if (
6969+ !hasInitiallyRendered &&
7070+ convo.status === ConvoStatus.Ready &&
7171+ !convo.isFetchingHistory
7272+ ) {
7373+ setTimeout(() => {
7474+ setHasInitiallyRendered(true)
7575+ }, 15)
7676+ }
7777+ }, [convo.isFetchingHistory, convo.items, convo.status, hasInitiallyRendered])
70787179 if (convo.status === ConvoStatus.Error) {
7280 return (
···8896 return (
8997 <KeyboardProvider>
9098 <CenteredView style={a.flex_1} sideBorders>
9191- <Header profile={convo.recipients[0]} />
9292- <MessagesList />
9999+ <Header profile={convo.recipients?.[0]} />
100100+ <View style={[a.flex_1]}>
101101+ {convo.status !== ConvoStatus.Ready ? (
102102+ <ListMaybePlaceholder isLoading />
103103+ ) : (
104104+ <MessagesList />
105105+ )}
106106+ {!hasInitiallyRendered && (
107107+ <View
108108+ style={[
109109+ a.absolute,
110110+ a.z_10,
111111+ a.w_full,
112112+ a.h_full,
113113+ a.justify_center,
114114+ a.align_center,
115115+ t.atoms.bg,
116116+ ]}>
117117+ <View style={[{marginBottom: 75}]}>
118118+ <Loader size="xl" />
119119+ </View>
120120+ </View>
121121+ )}
122122+ </View>
93123 </CenteredView>
94124 </KeyboardProvider>
95125 )
···128158 a.justify_between,
129159 a.align_start,
130160 a.gap_lg,
131131- a.px_lg,
161161+ a.pl_xl,
162162+ a.pr_lg,
132163 a.py_sm,
133164 ]}>
134165 {!gtTablet ? (
···154185 )}
155186 <View style={[a.align_center, a.gap_sm, a.flex_1]}>
156187 {profile ? (
157157- <>
188188+ <View style={[a.align_center]}>
158189 <PreviewableUserAvatar size={32} profile={profile} />
159159- <Text style={[a.text_lg, a.font_bold, a.text_center]}>
190190+ <Text
191191+ style={[a.text_lg, a.font_bold, isWeb ? a.mt_md : a.mt_sm]}
192192+ numberOfLines={1}>
160193 {profile.displayName}
161194 </Text>
162162- </>
195195+ <Text
196196+ style={[t.atoms.text_contrast_medium, {fontSize: 15}]}
197197+ numberOfLines={1}>
198198+ @{profile.handle}
199199+ </Text>
200200+ </View>
163201 ) : (
164202 <>
165203 <View
···171209 />
172210 <View
173211 style={[
174174- {width: 120, height: 18},
212212+ {width: 120, height: 16},
175213 a.rounded_xs,
176214 t.atoms.bg_contrast_25,
177177- a.mb_2xs,
215215+ a.mt_xs,
216216+ ]}
217217+ />
218218+ <View
219219+ style={[
220220+ {width: 175, height: 12},
221221+ a.rounded_xs,
222222+ t.atoms.bg_contrast_25,
178223 ]}
179224 />
180225 </>