Add Info Panel to direct chats (#10576)

This commit is contained in:
DS Boyce
2026-05-22 10:34:21 -07:00
committed by GitHub
parent f2e7028709
commit 4bdedc55be
3 changed files with 257 additions and 154 deletions
@@ -65,6 +65,7 @@ import {ChatStatusInfo} from './ChatStatusInfo'
import {groupSystemMessages, type RenderItem} from './groupSystemMessages'
import {InviteLinkDialogProvider} from './InviteLinkDialogProvider'
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
import {MessagesListGroupInfoPanel} from './MessagesListGroupInfoPanel'
import {MessagesListInfoPanel} from './MessagesListInfoPanel'
import {KeyboardStickyView} from './vendor/KeyboardStickyView'
@@ -509,9 +510,12 @@ export function MessagesList({
ListHeaderComponent={
<>
<MaybeLoader isLoading={convoState.isFetchingHistory} />
{convoState.convo?.kind === 'group' &&
convoState.hasAllHistory ? (
<MessagesListInfoPanel convo={convoState.convo} />
{convoState.hasAllHistory ? (
convoState.convo?.kind === 'group' ? (
<MessagesListGroupInfoPanel convo={convoState.convo} />
) : (
<MessagesListInfoPanel convo={convoState.convo} />
)
) : null}
</>
}
@@ -0,0 +1,176 @@
import {View} from 'react-native'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
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 * as Dialog from '#/components/Dialog'
import {AddMembersFlow} from '#/components/dms/AddMembersFlow'
import {type ConvoWithDetails} from '#/components/dms/util'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {InviteLinkDialog} from './InviteLinkDialog'
export function MessagesListGroupInfoPanel({
convo,
}: {
convo: Extract<ConvoWithDetails, {kind: 'group'}>
}) {
const t = useTheme()
const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const convoId = convo.view.id
const addMembersControl = Dialog.useDialogControl()
const inviteLinkControl = Dialog.useDialogControl()
const {currentAccount} = useSession()
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
onSuccess: () => {
addMembersControl.close()
},
onError: e => {
logger.error('Failed to add group chat members', {message: e})
Toast.show(l`Failed to add members`, {type: 'error'})
},
})
// TODO Enable this once the feature is working end-to-end. -dsb
// const joinLink = groupConvo?.details.joinLink
const isJoinLinkEnabled = false
// (isOwner && groupConvo) ||
// (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled')
const isOwner = convo.primaryMember?.did === currentAccount?.did
const members = (convo.members ?? []).filter(
profile => profile.did !== currentAccount?.did,
)
let names: React.ReactNode = null
if (members.length === 1) {
names = (
<Trans>New chat with {createSanitizedDisplayName(members[0])}</Trans>
)
} else if (members.length === 2) {
names = (
<Trans>
New chat with {createSanitizedDisplayName(members[0])} and{' '}
{createSanitizedDisplayName(members[1])}
</Trans>
)
} else if (members.length > 2) {
const memberCount = convo.details.memberCount - 2
names = (
<Trans>
New chat with {createSanitizedDisplayName(members[0])},{' '}
{createSanitizedDisplayName(members[1])}, and{' '}
<Plural
value={memberCount}
one={`${memberCount} more`}
other={`${memberCount} more`}
/>
.
</Trans>
)
}
const showButtons = isOwner || isJoinLinkEnabled
return (
<>
<View style={[a.align_center, a.justify_center]}>
<AvatarBubbles animate={true} profiles={convo.members} />
{convo.details.name ? (
<Text
style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_lg, t.atoms.text]}>
{convo.details.name}
</Text>
) : null}
{names ? (
<Text
style={[
a.px_lg,
a.mt_xs,
a.text_center,
a.text_sm,
t.atoms.text_contrast_high,
showButtons ? null : a.mb_4xl,
]}>
{names}
</Text>
) : null}
{showButtons ? (
<View
style={[
a.flex_row,
a.align_center,
a.justify_center,
a.gap_sm,
a.mt_lg,
a.mb_4xl,
]}>
{isOwner ? (
<Button
color="secondary"
size="small"
label={l`Click here to add people to this group chat`}
onPress={() => addMembersControl.open()}>
<ButtonIcon icon={PersonPlusIcon} />
<ButtonText>
<Trans>Add people</Trans>
</ButtonText>
</Button>
) : null}
{isJoinLinkEnabled ? (
<Button
color="secondary"
size="small"
label={
isOwner
? l`Click here to create or manage an invite link for this group chat`
: l`Click here to view the invite link for this group chat`
}
onPress={inviteLinkControl.open}>
<ButtonIcon icon={ChainLinkIcon} />
<ButtonText>
<Trans>Invite link</Trans>
</ButtonText>
</Button>
) : null}
</View>
) : null}
</View>
{convo.primaryMember && moderationOpts && (
<InviteLinkDialog
convo={convo}
owner={convo.primaryMember}
moderationOpts={moderationOpts}
isOwner={isOwner}
control={inviteLinkControl}
/>
)}
<Dialog.Outer
control={addMembersControl}
testID="addChatMembersDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle />
<AddMembersFlow
convo={convo}
title={l`Add people`}
onAddMembers={(members, profiles) =>
addGroupMembers({members, profiles})
}
/>
</Dialog.Outer>
</>
)
}
@@ -1,176 +1,99 @@
import {View} from 'react-native'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {moderateProfile} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {logger} from '#/logger'
import {type NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {AddMembersFlow} from '#/components/dms/AddMembersFlow'
import {type ConvoWithDetails} from '#/components/dms/util'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import * as Toast from '#/components/Toast'
import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import {InviteLinkDialog} from './InviteLinkDialog'
export function MessagesListInfoPanel({
convo,
}: {
convo: Extract<ConvoWithDetails, {kind: 'group'}>
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
}) {
const navigation = useNavigation<NavigationProp>()
const t = useTheme()
const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const convoId = convo.view.id
const addMembersControl = Dialog.useDialogControl()
const inviteLinkControl = Dialog.useDialogControl()
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
onSuccess: () => {
addMembersControl.close()
},
onError: e => {
logger.error('Failed to add group chat members', {message: e})
Toast.show(l`Failed to add members`, {type: 'error'})
},
})
// TODO Enable this once the feature is working end-to-end. -dsb
// const joinLink = groupConvo?.details.joinLink
const isJoinLinkEnabled = false
// (isOwner && groupConvo) ||
// (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled')
const isOwner = convo.primaryMember?.did === currentAccount?.did
const members = (convo.members ?? []).filter(
const profile = convo.members.filter(
profile => profile.did !== currentAccount?.did,
)
let names: React.ReactNode = null
if (members.length === 1) {
names = (
<Trans>New chat with {createSanitizedDisplayName(members[0])}</Trans>
)
} else if (members.length === 2) {
names = (
<Trans>
New chat with {createSanitizedDisplayName(members[0])} and{' '}
{createSanitizedDisplayName(members[1])}
</Trans>
)
} else if (members.length > 2) {
const memberCount = convo.details.memberCount - 2
names = (
<Trans>
New chat with {createSanitizedDisplayName(members[0])},{' '}
{createSanitizedDisplayName(members[1])}, and{' '}
<Plural
value={memberCount}
one={`${memberCount} more`}
other={`${memberCount} more`}
/>
.
</Trans>
)
}
const showButtons = isOwner || isJoinLinkEnabled
)[0]
const handle = sanitizeHandle(profile.handle, '@')
const displayName = moderationOpts
? createSanitizedDisplayName(
profile,
true,
moderateProfile(profile, moderationOpts).ui('displayName'),
)
: handle
const profileLink =
profile.handle && !isInvalidHandle(profile.handle)
? profile.handle
: profile.did
return (
<>
<View style={[a.align_center, a.justify_center]}>
<AvatarBubbles animate={true} profiles={convo.members} />
{convo.details.name ? (
<Text
style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_lg, t.atoms.text]}>
{convo.details.name}
</Text>
) : null}
{names ? (
<Text
style={[
a.px_lg,
a.mt_xs,
a.text_center,
a.text_sm,
t.atoms.text_contrast_high,
showButtons ? null : a.mb_4xl,
]}>
{names}
</Text>
) : null}
{showButtons ? (
<View
style={[
a.flex_row,
a.align_center,
a.justify_center,
a.gap_sm,
a.mt_lg,
a.mb_4xl,
]}>
{isOwner ? (
<Button
color="secondary"
size="small"
label={l`Click here to add people to this group chat`}
onPress={() => addMembersControl.open()}>
<ButtonIcon icon={PersonPlusIcon} />
<ButtonText>
<Trans>Add people</Trans>
</ButtonText>
</Button>
) : null}
{isJoinLinkEnabled ? (
<Button
color="secondary"
size="small"
label={
isOwner
? l`Click here to create or manage an invite link for this group chat`
: l`Click here to view the invite link for this group chat`
}
onPress={inviteLinkControl.open}>
<ButtonIcon icon={ChainLinkIcon} />
<ButtonText>
<Trans>Invite link</Trans>
</ButtonText>
</Button>
) : null}
</View>
) : null}
<View style={[a.align_center, a.justify_center]}>
<UserAvatar type="user" size={88} avatar={profile?.avatar} />
<View
style={[
a.flex_row,
a.align_center,
a.justify_center,
a.gap_xs,
a.mt_lg,
]}>
<Text style={[a.text_2xl, a.font_bold, t.atoms.text]}>
{displayName}
</Text>
<ProfileBadges profile={profile} size="lg" />
</View>
{convo.primaryMember && moderationOpts && (
<InviteLinkDialog
convo={convo}
owner={convo.primaryMember}
moderationOpts={moderationOpts}
isOwner={isOwner}
control={inviteLinkControl}
/>
)}
<Dialog.Outer
control={addMembersControl}
testID="addChatMembersDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle />
<AddMembersFlow
convo={convo}
title={l`Add people`}
onAddMembers={(members, profiles) =>
addGroupMembers({members, profiles})
}
/>
</Dialog.Outer>
</>
<Text style={[a.text_sm, a.mt_xs, t.atoms.text_contrast_high]}>
{handle}
</Text>
{moderationOpts ? (
<View style={[a.mt_xs]}>
<ProfileCard.Labels
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
) : 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`Go to users profile`}
onPress={() => {
navigation.navigate('Profile', {name: profileLink})
}}>
<ButtonIcon icon={PersonIcon} />
<ButtonText>
<Trans>Go to profile</Trans>
</ButtonText>
</Button>
</View>
</View>
)
}