From 9f3c21e298ab6a2f61327c55ddbcfc1ffcb5b9bb Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:48:30 -0700 Subject: [PATCH] Add header to group clip clops (#10254) Co-authored-by: Samuel Newman --- src/screens/Messages/ChatList.tsx | 8 +- src/screens/Messages/Conversation.tsx | 2 +- .../Messages/components/MessagesList.tsx | 31 ++++-- .../components/MessagesListInfoPanel.tsx | 95 +++++++++++++++++++ src/state/messages/convo/agent.ts | 16 ++++ src/state/messages/convo/types.ts | 7 ++ 6 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 src/screens/Messages/components/MessagesListInfoPanel.tsx diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 9a59352953..3463b04ed0 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -242,7 +242,7 @@ export function MessagesScreenInner({navigation, route}: Props) { if (!isScreenFocused) { return } - return listenSoftReset(onSoftReset) + return listenSoftReset(() => void onSoftReset()) }, [onSoftReset, isScreenFocused]) // NOTE(APiligrim) @@ -292,7 +292,7 @@ export function MessagesScreenInner({navigation, route}: Props) { size="small" color="secondary_inverted" variant="solid" - onPress={() => refetch()}> + onPress={() => void refetch()}> Retry @@ -342,8 +342,8 @@ export function MessagesScreenInner({navigation, route}: Props) { renderItem={renderItem} keyExtractor={keyExtractor} refreshing={isPTRing} - onRefresh={onRefresh} - onEndReached={onEndReached} + onRefresh={() => void onRefresh()} + onEndReached={() => void onEndReached()} ListFooterComponent={ + style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}> {moderation ? ( ) : ( diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index ca6280ec22..8ffb382704 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -1,4 +1,11 @@ -import {useCallback, useEffect, useId, useRef, useState} from 'react' +import { + useCallback, + useEffect, + useId, + useLayoutEffect, + useRef, + useState, +} from 'react' import {type LayoutChangeEvent, type ScrollViewProps, View} from 'react-native' import { KeyboardChatScrollView, @@ -62,6 +69,7 @@ import {useAnalytics} from '#/analytics' import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' import {ChatStatusInfo} from './ChatStatusInfo' import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed' +import {MessagesListInfoPanel} from './MessagesListInfoPanel' import {KeyboardStickyView} from './vendor/KeyboardStickyView' function MaybeLoader({isLoading}: {isLoading: boolean}) { @@ -151,10 +159,12 @@ export function MessagesList({ // Reset when hasScrolled goes back to false (e.g. convo re-initialization after backgrounding). const hasInitiallyScrolled = useRef(false) const prevHasScrolled = useRef(hasScrolled) - if (prevHasScrolled.current && !hasScrolled) { - hasInitiallyScrolled.current = false - } - prevHasScrolled.current = hasScrolled + useLayoutEffect(() => { + if (prevHasScrolled.current && !hasScrolled) { + hasInitiallyScrolled.current = false + } + prevHasScrolled.current = hasScrolled + }, [hasScrolled]) // -- Keep track of background state and positioning for new pill const layoutHeight = useSharedValue(0) @@ -384,7 +394,7 @@ export function MessagesList({ profile={convoState.convo.members.find( member => member.did === item.message.sender.did, )} - isGroupChat={convoState.getGroupInfo?.() != null} + isGroupChat={convoState.isGroup()} /> ) } else if (item.type === 'deleted-message') { @@ -417,6 +427,8 @@ export function MessagesList({ [inputHeightUI], ) + console.log('DEBUG >>>', 'convoState.hasAllHistory', convoState.hasAllHistory) + return ( + <> + + {convoState.isGroup() && convoState.hasAllHistory ? ( + + ) : null} + } // native only (prop is not supported on web) renderScrollComponent={renderScrollComponent} diff --git a/src/screens/Messages/components/MessagesListInfoPanel.tsx b/src/screens/Messages/components/MessagesListInfoPanel.tsx new file mode 100644 index 0000000000..9f904e1eb3 --- /dev/null +++ b/src/screens/Messages/components/MessagesListInfoPanel.tsx @@ -0,0 +1,95 @@ +import {View} from 'react-native' +import {Plural, Trans, useLingui} from '@lingui/react/macro' + +import {type ConvoState} from '#/state/messages/convo/types' +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' +import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import {Text} from '#/components/Typography' + +export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) { + const t = useTheme() + const {t: l} = useLingui() + + const {currentAccount} = useSession() + + const groupName = convoState.getGroupInfo?.()?.name + + const members = (convoState?.convo?.members ?? []).filter( + profile => profile.did !== currentAccount?.did, + ) + + let names: React.ReactNode | null = null + if (members.length === 1) { + names = New chat with {members[0].displayName} + } + if (members.length === 2) { + names = ( + + New chat with {members[0].displayName} and {members[1].displayName} + + ) + } + if (members.length > 2) { + names = ( + + New chat with {members[0].displayName}, {members[1].displayName}, and{' '} + + . + + ) + } + + return ( + + + {groupName ? ( + + {groupName} + + ) : null} + {names ? ( + + {names} + + ) : null} + + + + + + ) +} diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index ef2b251cce..9f0693c4a3 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -153,6 +153,8 @@ export class Convo { sender: this.sender, recipients: this.recipients, isFetchingHistory: this.isFetchingHistory, + // Explicit null check since the value is initially undefined. + hasAllHistory: this.oldestRev === null, deleteMessage: undefined, sendMessage: undefined, fetchMessageHistory: undefined, @@ -176,6 +178,8 @@ export class Convo { sender: this.sender!, recipients: this.recipients!, isFetchingHistory: this.isFetchingHistory, + // Explicit null check since the value is initially undefined. + hasAllHistory: this.oldestRev === null, deleteMessage: this.deleteMessage, sendMessage: this.sendMessage, fetchMessageHistory: this.fetchMessageHistory, @@ -196,6 +200,7 @@ export class Convo { sender: undefined, recipients: undefined, isFetchingHistory: false, + hasAllHistory: false, deleteMessage: undefined, sendMessage: undefined, fetchMessageHistory: undefined, @@ -216,6 +221,8 @@ export class Convo { sender: this.sender, recipients: this.recipients, isFetchingHistory: false, + // Explicit null check since the value is initially undefined. + hasAllHistory: this.oldestRev === null, deleteMessage: undefined, sendMessage: undefined, fetchMessageHistory: undefined, @@ -627,6 +634,7 @@ export class Convo { /* * If oldestRev is null, we've fetched all history. + * Needs to explicitly check for `null` since this is initially `undefined`. */ if (this.oldestRev === null) return @@ -660,6 +668,14 @@ export class Convo { this.oldestRev = cursor ?? null + /* + * If the response contained fewer messages than the limit, we know + * there are no more pages, regardless of whether a cursor was returned. + */ + if (messages.length < (IS_NATIVE ? 30 : 60)) { + this.oldestRev = null + } + for (const message of messages) { if ( ChatBskyConvoDefs.isMessageView(message) || diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index d7adb51c6d..f27610053a 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -156,6 +156,7 @@ export type ConvoStateUninitialized = { sender: ChatBskyActorDefs.ProfileViewBasic | undefined recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined isFetchingHistory: false + hasAllHistory: boolean deleteMessage: undefined sendMessage: undefined fetchMessageHistory: undefined @@ -174,6 +175,7 @@ export type ConvoStateInitializing = { sender: ChatBskyActorDefs.ProfileViewBasic | undefined recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined isFetchingHistory: boolean + hasAllHistory: boolean deleteMessage: undefined sendMessage: undefined fetchMessageHistory: undefined @@ -192,6 +194,7 @@ export type ConvoStateReady = { sender: ChatBskyActorDefs.ProfileViewBasic recipients: ChatBskyActorDefs.ProfileViewBasic[] isFetchingHistory: boolean + hasAllHistory: boolean deleteMessage: DeleteMessage sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory @@ -210,6 +213,7 @@ export type ConvoStateBackgrounded = { sender: ChatBskyActorDefs.ProfileViewBasic recipients: ChatBskyActorDefs.ProfileViewBasic[] isFetchingHistory: boolean + hasAllHistory: boolean deleteMessage: DeleteMessage sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory @@ -228,6 +232,7 @@ export type ConvoStateSuspended = { sender: ChatBskyActorDefs.ProfileViewBasic recipients: ChatBskyActorDefs.ProfileViewBasic[] isFetchingHistory: boolean + hasAllHistory: boolean deleteMessage: DeleteMessage sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory @@ -246,6 +251,7 @@ export type ConvoStateError = { sender: undefined recipients: undefined isFetchingHistory: false + hasAllHistory: false deleteMessage: undefined sendMessage: undefined fetchMessageHistory: undefined @@ -264,6 +270,7 @@ export type ConvoStateDisabled = { sender: ChatBskyActorDefs.ProfileViewBasic recipients: ChatBskyActorDefs.ProfileViewBasic[] isFetchingHistory: boolean + hasAllHistory: boolean deleteMessage: DeleteMessage sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory