Add header to group clip clops (#10254)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
DS Boyce
2026-04-16 10:48:30 -07:00
committed by GitHub
parent e10c05d735
commit 9f3c21e298
6 changed files with 147 additions and 12 deletions
+4 -4
View File
@@ -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()}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
@@ -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={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
+1 -1
View File
@@ -142,7 +142,7 @@ function Inner() {
return (
<>
<Layout.Center
style={[a.flex_1, IS_LIQUID_GLASS && {paddingTop: topInset}]}>
style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}>
{moderation ? (
<MessagesListHeader profile={recipient} moderation={moderation} />
) : (
@@ -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 (
<DateDividerToggleProvider>
<KeyboardGestureArea
@@ -449,7 +461,12 @@ export function MessagesList({
showsVerticalScrollIndicator={!IS_ANDROID}
scrollEventThrottle={100}
ListHeaderComponent={
<MaybeLoader isLoading={convoState.isFetchingHistory} />
<>
<MaybeLoader isLoading={convoState.isFetchingHistory} />
{convoState.isGroup() && convoState.hasAllHistory ? (
<MessagesListInfoPanel convoState={convoState} />
) : null}
</>
}
// native only (prop is not supported on web)
renderScrollComponent={renderScrollComponent}
@@ -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 = <Trans>New chat with {members[0].displayName}</Trans>
}
if (members.length === 2) {
names = (
<Trans>
New chat with {members[0].displayName} and {members[1].displayName}
</Trans>
)
}
if (members.length > 2) {
names = (
<Trans>
New chat with {members[0].displayName}, {members[1].displayName}, and{' '}
<Plural
value={members.length - 2}
one={`${members.length - 2} more`}
other={`${members.length - 2} more`}
/>
.
</Trans>
)
}
return (
<View style={[a.align_center, a.justify_center]}>
<AvatarBubbles animate={true} profiles={members} />
{groupName ? (
<Text style={[a.text_2xl, a.font_bold, a.mt_lg, t.atoms.text]}>
{groupName}
</Text>
) : null}
{names ? (
<Text style={[a.text_sm, a.mt_xs, t.atoms.text_contrast_high]}>
{names}
</Text>
) : null}
<View
style={[
a.flex_row,
a.align_center,
a.justify_center,
a.gap_sm,
a.mt_lg,
a.mb_4xl,
]}>
<Button
color="secondary"
size="small"
label={l`Click here to add people to this group chat`}
onPress={() => {}}>
<ButtonIcon icon={PersonPlusIcon} />
<ButtonText>
<Trans>Add people</Trans>
</ButtonText>
</Button>
<Button
color="secondary"
size="small"
label={l`Click here to view or create an invite link for this group chat`}
onPress={() => {}}>
<ButtonIcon icon={ChainLinkIcon} />
<ButtonText>
<Trans>Invite link</Trans>
</ButtonText>
</Button>
</View>
</View>
)
}
+16
View File
@@ -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) ||
+7
View File
@@ -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