Improve initial loading state for chat settings (#10424)

This commit is contained in:
DS Boyce
2026-05-07 12:00:42 -07:00
committed by GitHub
parent eddbf6ba9f
commit 10cb0dbf21
3 changed files with 69 additions and 60 deletions
@@ -16,8 +16,10 @@ import {Text} from '#/components/Typography'
export function AddMembersLink({ export function AddMembersLink({
convo, convo,
disabled,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
disabled?: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -41,7 +43,7 @@ export function AddMembersLink({
return ( return (
<> <>
<Button <Button
disabled={isAddPending} disabled={disabled || isAddPending}
label={l`Add members`} label={l`Add members`}
onPress={addMembersControl.open}> onPress={addMembersControl.open}>
{({interacting}) => ( {({interacting}) => (
@@ -23,21 +23,19 @@ export function MembersAndRequests({
return ( return (
<View style={[a.flex_row, a.justify_between, a.px_xl, a.pt_xl, a.pb_sm]}> <View style={[a.flex_row, a.justify_between, a.px_xl, a.pt_xl, a.pb_sm]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}> <View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Text style={[a.text_lg, a.font_semi_bold, t.atoms.text]}> <Text style={[a.text_lg, a.font_semi_bold, t.atoms.text]}>
<Trans>Members</Trans> <Trans comment="The heading above the list of chat members.">
Members
</Trans>
</Text>
<Text style={[a.text_xs, a.font_medium, t.atoms.text_contrast_medium]}>
{l({
message: `${memberCount}/${MEMBER_LIMIT}`,
comment:
'The number of group chat members out of the total number of permitted users.',
})}
</Text> </Text>
<View
style={[a.px_xs, a.py_2xs, t.atoms.bg_contrast_50, a.rounded_full]}>
<Text
style={[a.text_xs, a.font_medium, {color: t.palette.contrast_500}]}>
{l({
message: `${memberCount}/${MEMBER_LIMIT}`,
comment:
'The number of group chat members out of the total number of permitted users.',
})}
</Text>
</View>
</View> </View>
{isOwner && requestCount > 0 ? ( {isOwner && requestCount > 0 ? (
<InlineLinkText <InlineLinkText
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import { import {
ChatBskyActorDefs, ChatBskyActorDefs,
ChatBskyConvoDefs, ChatBskyConvoDefs,
ModerationOpts, type ModerationOpts,
} from '@atproto/api' } from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -117,7 +117,7 @@ function SettingsInner() {
) )
} }
if (!isConvoActive(convoState) || !moderationOpts) { if (!convoState.convo || !moderationOpts) {
return ( return (
<View style={[a.flex_1, a.align_center, a.justify_center]}> <View style={[a.flex_1, a.align_center, a.justify_center]}>
<Loader size="xl" /> <Loader size="xl" />
@@ -125,7 +125,7 @@ function SettingsInner() {
) )
} }
if (convoState.convo?.kind !== 'group') { if (convoState.convo.kind !== 'group') {
return ( return (
<Error <Error
title={l`Wrong kind of conversation`} title={l`Wrong kind of conversation`}
@@ -142,7 +142,11 @@ function SettingsInner() {
} }
return ( return (
<GroupSettings convo={convoState.convo} moderationOpts={moderationOpts} /> <GroupSettings
convo={convoState.convo}
moderationOpts={moderationOpts}
isReady={isConvoActive(convoState)}
/>
) )
} }
@@ -166,9 +170,11 @@ function isGroupMember(
function GroupSettings({ function GroupSettings({
convo, convo,
moderationOpts, moderationOpts,
isReady,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
isReady: boolean
}) { }) {
const initialNumToRender = useInitialNumToRender({minItemHeight: 68}) const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
const bottomBarOffset = useBottomBarOffset() const bottomBarOffset = useBottomBarOffset()
@@ -178,7 +184,7 @@ function GroupSettings({
const primaryMember = convo.primaryMember const primaryMember = convo.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
const {data: memberListData = [], isPending} = useListConvoMembersQuery({ const {data: memberListData = []} = useListConvoMembersQuery({
convoId: convo.view.id, convoId: convo.view.id,
placeholderData: convo.members, placeholderData: convo.members,
}) })
@@ -197,6 +203,16 @@ function GroupSettings({
0, 0,
) ?? 0 ) ?? 0
const groupMembers = memberListData.filter(isGroupMember).sort((a, b) => {
const aIsOwner = a.did === primaryMember?.did
const bIsOwner = b.did === primaryMember?.did
const aIsSelf = a.did === currentAccount?.did
const bIsSelf = b.did === currentAccount?.did
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
if (aIsSelf !== bIsSelf) return aIsSelf ? -1 : 1
return 0
})
const items: Item[] = [ const items: Item[] = [
{ {
type: 'MEMBERS_AND_REQUESTS', type: 'MEMBERS_AND_REQUESTS',
@@ -206,41 +222,30 @@ function GroupSettings({
? [{type: 'ADD_MEMBERS_LINK', key: 'add-members-link'} as const] ? [{type: 'ADD_MEMBERS_LINK', key: 'add-members-link'} as const]
: []), : []),
] ]
if (isPending) { items.push(
// should never be pending if we correctly set the query cache data ...groupMembers.map(
items.push( (profile): Item => ({
...Array.from({length: 5}, (_, i) => ({ type: 'CHAT_MEMBER',
type: 'CHAT_MEMBER_PLACEHOLDER' as const, key: profile.did,
key: `chat-member-placeholder-${i}`, profile,
})), status:
) primaryMember?.did === profile.did
} else { ? 'owner'
items.push( : invites.includes(profile.did)
...memberListData ? 'invited'
.filter(isGroupMember) : 'standard',
.sort((a, b) => { }),
const aIsOwner = a.did === primaryMember?.did ),
const bIsOwner = b.did === primaryMember?.did )
const aIsSelf = a.did === currentAccount?.did const placeholderCount = Math.max(
const bIsSelf = b.did === currentAccount?.did 0,
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1 convo.details.memberCount - groupMembers.length,
if (aIsSelf !== bIsSelf) return aIsSelf ? -1 : 1 )
return 0 for (let i = 0; i < placeholderCount; i++) {
}) items.push({
.map( type: 'CHAT_MEMBER_PLACEHOLDER',
(profile): Item => ({ key: `chat-member-placeholder-${i}`,
type: 'CHAT_MEMBER', })
key: profile.did,
profile,
status:
primaryMember?.did === profile.did
? 'owner'
: invites.includes(profile.did)
? 'invited'
: 'standard',
}),
),
)
} }
function renderItem({item}: {item: Item}) { function renderItem({item}: {item: Item}) {
@@ -255,7 +260,7 @@ function GroupSettings({
/> />
) )
case 'ADD_MEMBERS_LINK': case 'ADD_MEMBERS_LINK':
return <AddMembersLink convo={convo} /> return <AddMembersLink convo={convo} disabled={!isReady} />
case 'CHAT_MEMBER': case 'CHAT_MEMBER':
return ( return (
<Member <Member
@@ -286,6 +291,7 @@ function GroupSettings({
convo={convo} convo={convo}
isOwner={isOwner} isOwner={isOwner}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
isReady={isReady}
/> />
} }
renderItem={renderItem} renderItem={renderItem}
@@ -299,10 +305,12 @@ function SettingsHeader({
convo, convo,
isOwner, isOwner,
moderationOpts, moderationOpts,
isReady,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
isOwner: boolean isOwner: boolean
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
isReady: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {i18n, t: l} = useLingui() const {i18n, t: l} = useLingui()
@@ -462,7 +470,7 @@ function SettingsHeader({
]}> ]}>
<SettingsButton <SettingsButton
color={convo.view.muted ? 'negative_subtle' : 'secondary'} color={convo.view.muted ? 'negative_subtle' : 'secondary'}
disabled={isMuting} disabled={!isReady || isMuting}
icon={convo.view.muted ? BellOffIcon : BellIcon} icon={convo.view.muted ? BellOffIcon : BellIcon}
label={ label={
convo.view.muted convo.view.muted
@@ -474,7 +482,7 @@ function SettingsHeader({
/> />
{isOwner ? ( {isOwner ? (
<SettingsButton <SettingsButton
disabled={isEditingName} disabled={!isReady || isEditingName}
icon={EditIcon} icon={EditIcon}
label={l`Edit this group chats name`} label={l`Edit this group chats name`}
text={l`Edit name`} text={l`Edit name`}
@@ -483,7 +491,7 @@ function SettingsHeader({
) : null} ) : null}
{isJoinLinkEnabled ? ( {isJoinLinkEnabled ? (
<SettingsButton <SettingsButton
disabled={lockStatus !== 'unlocked'} disabled={!isReady || lockStatus !== 'unlocked'}
icon={ChainLinkIcon} icon={ChainLinkIcon}
label={ label={
isOwner isOwner
@@ -497,7 +505,7 @@ function SettingsHeader({
{canLockGroupChat ? ( {canLockGroupChat ? (
<SettingsButton <SettingsButton
color={lockStatus === 'locked' ? 'negative_subtle' : 'secondary'} color={lockStatus === 'locked' ? 'negative_subtle' : 'secondary'}
disabled={isLocking} disabled={!isReady || isLocking}
icon={LockIcon} icon={LockIcon}
label={ label={
lockStatus === 'locked' lockStatus === 'locked'
@@ -512,6 +520,7 @@ function SettingsHeader({
) : null} ) : null}
{!isOwner && isReportLinkEnabled && ( {!isOwner && isReportLinkEnabled && (
<SettingsButton <SettingsButton
disabled={!isReady}
icon={FlagIcon} icon={FlagIcon}
label={l`Report this group chat`} label={l`Report this group chat`}
text={l`Report`} text={l`Report`}
@@ -520,7 +529,7 @@ function SettingsHeader({
)} )}
{!isOwner && ( {!isOwner && (
<SettingsButton <SettingsButton
disabled={isLeaving} disabled={!isReady || isLeaving}
icon={ArrowBoxLeftIcon} icon={ArrowBoxLeftIcon}
label={l`Leave this group chat`} label={l`Leave this group chat`}
text={l`Leave`} text={l`Leave`}