Groupchats feature branch (#10181)

Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-04-15 12:15:45 -07:00
committed by GitHub
parent 75c9e2c181
commit d3f5093817
31 changed files with 2826 additions and 635 deletions
+102 -89
View File
@@ -5,24 +5,29 @@ import {
type ModerationCause,
type ModerationDecision,
} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/profile-shadow'
import {isConvoActive, useConvo} from '#/state/messages/convo'
import {type ConvoItem} from '#/state/messages/convo/types'
import {useSession} from '#/state/session'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, ButtonIcon} from '#/components/Button'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
@@ -48,7 +53,7 @@ export function MessagesListHeader({
}, [moderation])
return (
<Layout.Header.Outer>
<Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}>
<View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}>
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.BackButton />
@@ -72,19 +77,12 @@ export function MessagesListHeader({
<View style={a.gap_xs}>
<View
style={[
{width: 120, height: 16},
{width: 150, height: 16},
a.rounded_xs,
t.atoms.bg_contrast_25,
a.mt_xs,
]}
/>
<View
style={[
{width: 175, height: 12},
a.rounded_xs,
t.atoms.bg_contrast_25,
]}
/>
</View>
</View>
@@ -108,22 +106,27 @@ function HeaderReady({
userBlock?: ModerationCause
}
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const convoState = useConvo()
const {currentAccount} = useSession()
const navigation = useNavigation<NavigationProp>()
const groupInfo = convoState.getGroupInfo?.()
const isGroupChat = groupInfo != null
const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount
? _(msg`Deleted Account`)
: sanitizeDisplayName(
profile.displayName || profile.handle,
moderation.ui('displayName'),
)
const displayName = isGroupChat
? (groupInfo.name ?? l`${profile.handle}'s group chat`)
: isDeletedAccount
? l`Deleted Account`
: createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
// @ts-ignore findLast is polyfilled - esb
const latestMessageFromOther = convoState.items.findLast(
(item: ConvoItem) =>
item.type === 'message' && item.message.sender.did === profile.did,
item.type === 'message' &&
item.message.sender.did !== currentAccount?.did,
)
const latestReportableMessage =
@@ -131,85 +134,95 @@ function HeaderReady({
? latestMessageFromOther.message
: undefined
const handleNavigateToSettings = () => {
const convoId = convoState.convo?.id
if (convoId) {
navigation.navigate('MessagesConversationSettings', {
conversation: convoId,
})
} else {
logger.error(`handleNavigateToSettings: missing convo ID`)
}
}
return (
<View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
<Link
label={_(msg`View ${displayName}'s profile`)}
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}>
<PreviewableUserAvatar
size={PFP_SIZE}
profile={profile}
moderation={moderation.ui('avatar')}
disableHoverCard={moderation.blocked}
/>
<View style={[a.flex_1]}>
<View style={[a.flex_row, a.align_center]}>
<Text
emoji
style={[
a.text_md,
a.font_semi_bold,
a.self_start,
web(a.leading_normal),
]}
numberOfLines={1}>
{displayName}
</Text>
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
</View>
{!isDeletedAccount && (
<Text
style={[
t.atoms.text_contrast_medium,
a.text_xs,
web([a.leading_normal, {marginTop: -2}]),
]}
numberOfLines={1}>
@{profile.handle}
{isGroupChat ? (
<View
style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}>
<AvatarBubbles
size="small"
profiles={convoState.recipients ?? []}
/>
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
{displayName}
</Text>
</View>
) : (
<Link
label={l`View ${displayName}'s profile`}
style={[a.flex_row, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}>
<PreviewableUserAvatar
size={PFP_SIZE}
profile={profile}
moderation={moderation.ui('avatar')}
disableHoverCard={moderation.blocked}
/>
<View style={[a.flex_1]}>
<View style={[a.flex_row, a.align_center]}>
<Text
emoji
style={[a.text_md, a.font_semi_bold, a.self_start]}
numberOfLines={1}>
{displayName}
</Text>
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
{convoState.convo?.muted && (
<>
{' '}
&middot;{' '}
<BellStroke
size="xs"
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
{' '}
&middot;{' '}
</Text>
<BellOffIcon
size="sm"
style={t.atoms.text_contrast_medium}
/>
</>
)}
</Text>
)}
</View>
</Link>
</View>
</View>
</Link>
)}
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.Slot>
{isConvoActive(convoState) && (
<ConvoMenu
convo={convoState.convo}
profile={profile}
currentScreen="conversation"
blockInfo={blockInfo}
latestReportableMessage={latestReportableMessage}
/>
)}
{isConvoActive(convoState) ? (
isGroupChat ? (
<Button
label={l`Open group chat settings`}
size="small"
color="secondary"
shape="round"
variant="ghost"
style={[a.bg_transparent]}
onPress={handleNavigateToSettings}>
<ButtonIcon icon={DotsHorizontalIcon} size="md" />
</Button>
) : (
<ConvoMenu
convo={convoState.convo}
profile={profile}
currentScreen="conversation"
blockInfo={blockInfo}
latestReportableMessage={latestReportableMessage}
/>
)
) : null}
</Layout.Header.Slot>
</View>
</View>
<View
style={[
{
paddingLeft: PFP_SIZE + a.gap_md.gap,
},
]}>
<PostAlerts
modui={moderation.ui('contentList')}
size="lg"
style={[a.pt_xs]}
/>
</View>
</View>
)
}