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 {groupSystemMessages, type RenderItem} from './groupSystemMessages'
import {InviteLinkDialogProvider} from './InviteLinkDialogProvider' import {InviteLinkDialogProvider} from './InviteLinkDialogProvider'
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed' import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
import {MessagesListGroupInfoPanel} from './MessagesListGroupInfoPanel'
import {MessagesListInfoPanel} from './MessagesListInfoPanel' import {MessagesListInfoPanel} from './MessagesListInfoPanel'
import {KeyboardStickyView} from './vendor/KeyboardStickyView' import {KeyboardStickyView} from './vendor/KeyboardStickyView'
@@ -509,9 +510,12 @@ export function MessagesList({
ListHeaderComponent={ ListHeaderComponent={
<> <>
<MaybeLoader isLoading={convoState.isFetchingHistory} /> <MaybeLoader isLoading={convoState.isFetchingHistory} />
{convoState.convo?.kind === 'group' && {convoState.hasAllHistory ? (
convoState.hasAllHistory ? ( convoState.convo?.kind === 'group' ? (
<MessagesListGroupInfoPanel convo={convoState.convo} />
) : (
<MessagesListInfoPanel convo={convoState.convo} /> <MessagesListInfoPanel convo={convoState.convo} />
)
) : null} ) : 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,114 +1,77 @@
import {View} from 'react-native' 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 {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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' 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 {type ConvoWithDetails} from '#/components/dms/util'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' import {ProfileBadges} from '#/components/ProfileBadges'
import * as Toast from '#/components/Toast' import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {InviteLinkDialog} from './InviteLinkDialog'
export function MessagesListInfoPanel({ export function MessagesListInfoPanel({
convo, convo,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'direct'}>
}) { }) {
const navigation = useNavigation<NavigationProp>()
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const convoId = convo.view.id
const addMembersControl = Dialog.useDialogControl()
const inviteLinkControl = Dialog.useDialogControl()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, { const profile = convo.members.filter(
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, profile => profile.did !== currentAccount?.did,
)[0]
const handle = sanitizeHandle(profile.handle, '@')
const displayName = moderationOpts
? createSanitizedDisplayName(
profile,
true,
moderateProfile(profile, moderationOpts).ui('displayName'),
) )
: handle
let names: React.ReactNode = null const profileLink =
if (members.length === 1) { profile.handle && !isInvalidHandle(profile.handle)
names = ( ? profile.handle
<Trans>New chat with {createSanitizedDisplayName(members[0])}</Trans> : profile.did
)
} 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 ( return (
<>
<View style={[a.align_center, a.justify_center]}> <View style={[a.align_center, a.justify_center]}>
<AvatarBubbles animate={true} profiles={convo.members} /> <UserAvatar type="user" size={88} avatar={profile?.avatar} />
{convo.details.name ? ( <View
<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={[ style={[
a.px_lg, a.flex_row,
a.mt_xs, a.align_center,
a.text_center, a.justify_center,
a.text_sm, a.gap_xs,
t.atoms.text_contrast_high, a.mt_lg,
showButtons ? null : a.mb_4xl,
]}> ]}>
{names} <Text style={[a.text_2xl, a.font_bold, t.atoms.text]}>
{displayName}
</Text> </Text>
<ProfileBadges profile={profile} size="lg" />
</View>
<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} ) : null}
{showButtons ? (
<View <View
style={[ style={[
a.flex_row, a.flex_row,
@@ -118,59 +81,19 @@ export function MessagesListInfoPanel({
a.mt_lg, a.mt_lg,
a.mb_4xl, a.mb_4xl,
]}> ]}>
{isOwner ? (
<Button <Button
color="secondary" color="secondary"
size="small" size="small"
label={l`Click here to add people to this group chat`} label={l`Go to users profile`}
onPress={() => addMembersControl.open()}> onPress={() => {
<ButtonIcon icon={PersonPlusIcon} /> navigation.navigate('Profile', {name: profileLink})
}}>
<ButtonIcon icon={PersonIcon} />
<ButtonText> <ButtonText>
<Trans>Add people</Trans> <Trans>Go to profile</Trans>
</ButtonText> </ButtonText>
</Button> </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> </View>
) : null}
</View> </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>
</>
) )
} }