Support group chats across other surfaces (#10266)

This commit is contained in:
Samuel Newman
2026-04-16 09:55:30 -07:00
committed by GitHub
parent cc8f22887f
commit f51602b3fe
11 changed files with 436 additions and 161 deletions
@@ -2,7 +2,6 @@ import {useCallback, useMemo, useState} from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {
AppBskyEmbedRecord,
ChatBskyActorDefs,
ChatBskyConvoDefs,
moderateProfile,
type ModerationDecision,
@@ -38,6 +37,7 @@ import {AvatarBubbles} from '#/components/AvatarBubbles'
import {useDialogControl} from '#/components/Dialog'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen'
import {Trash_Stroke2_Corner0_Rounded} from '#/components/icons/Trash'
@@ -49,7 +49,7 @@ import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import * as bsky from '#/types/bsky'
import type * as bsky from '#/types/bsky'
export const ChatListItemPortal = createPortalGroup()
@@ -60,7 +60,7 @@ export const ChatListItemPortal = createPortalGroup()
*/
export function ChatListItem({
convo,
convo: convoView,
showMenu = true,
children,
}: {
@@ -75,83 +75,47 @@ export function ChatListItem({
return null
}
if (
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>(
convo.kind,
ChatBskyConvoDefs.isGroupConvo,
)
) {
const owner = convo.members.find(r => {
if (
bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>(
r.kind,
ChatBskyActorDefs.isGroupConvoMember,
)
) {
return r.kind.role === 'owner'
} else {
throw new Error(
'Expected a GroupConvoMember, got an unknown kind of member',
)
}
})
if (!owner) {
// TODO: Determine if this is the right thing to do here. Throwing here so that
// if it turns out to be wrong it'll be very visible
throw new Error('Could not find the group owner in the group members')
const convo = parseConvoView(convoView, currentAccount?.did)
switch (convo?.kind) {
case 'direct': {
return (
<DirectChatItem
convo={convo}
moderationOpts={moderationOpts}
showMenu={showMenu}>
{children}
</DirectChatItem>
)
}
return (
<GroupChatItem
convo={convo}
groupOwner={owner}
groupInfo={convo.kind}
moderationOpts={moderationOpts}
showMenu={showMenu}
/>
)
} else if (
bsky.dangerousIsType<ChatBskyConvoDefs.DirectConvo>(
convo.kind,
ChatBskyConvoDefs.isDirectConvo,
)
) {
const otherMember = convo.members.find(
member => member.did !== currentAccount?.did,
)
if (!otherMember) {
case 'group': {
return (
<GroupChatItem
convo={convo}
moderationOpts={moderationOpts}
showMenu={showMenu}
/>
)
}
default: {
return null
}
return (
<DirectChatItem
convo={convo}
profile={otherMember}
moderationOpts={moderationOpts}
showMenu={showMenu}>
{children}
</DirectChatItem>
)
} else {
return null
}
}
function DirectChatItem({
convo,
profile: profileUnshadowed,
moderationOpts,
showMenu,
children,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: bsky.profile.AnyProfileView
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
moderationOpts: ModerationOpts
showMenu?: boolean
children?: React.ReactNode
}) {
const {t: l} = useLingui()
const profile = useProfileShadow(profileUnshadowed)
const profile = useProfileShadow(convo.primaryMember)
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
@@ -165,7 +129,7 @@ function DirectChatItem({
return (
<BaseChatItem
convo={convo}
convo={convo.view}
avatar={
<PreviewableUserAvatar
profile={profile}
@@ -176,7 +140,9 @@ function DirectChatItem({
primaryProfile={profile}
primaryProfileModeration={moderation}
title={displayName}
subtitle={isDeletedAccount ? undefined : sanitizeHandle(profile.handle)}
subtitle={
isDeletedAccount ? undefined : sanitizeHandle(profile.handle, '@')
}
accessibilityHint={
!isDeletedAccount
? l`Go to conversation with ${profile.handle}`
@@ -200,32 +166,28 @@ function DirectChatItem({
function GroupChatItem({
convo,
groupOwner: groupOwnerUnshadowed,
groupInfo,
moderationOpts,
showMenu,
children,
}: {
convo: ChatBskyConvoDefs.ConvoView
groupOwner: bsky.profile.AnyProfileView
groupInfo: ChatBskyConvoDefs.GroupConvo
convo: Extract<ConvoWithDetails, {kind: 'group'}>
moderationOpts: ModerationOpts
showMenu?: boolean
children?: React.ReactNode
}) {
const {t: l} = useLingui()
const groupOwner = useProfileShadow(groupOwnerUnshadowed)
const groupOwner = useProfileShadow(convo.primaryMember)
const moderation = useMemo(
() => moderateProfile(groupOwner, moderationOpts),
[groupOwner, moderationOpts],
)
const chatName = groupInfo.name ?? l`${groupOwner.handle}'s group chat`
const chatName = convo.details.name
return (
<BaseChatItem
convo={convo}
convo={convo.view}
avatar={<AvatarBubbles profiles={convo.members} size="medium" />}
title={chatName}
accessibilityHint={l`Go to the group chat named "${chatName}"`}
@@ -507,7 +469,7 @@ function BaseChatItem({
label={title}
accessibilityHint={accessibilityHint}
accessibilityActions={
IS_NATIVE
showMenu && IS_NATIVE
? [
{
name: 'magicTap',
@@ -521,8 +483,8 @@ function BaseChatItem({
: undefined
}
onPress={onPress}
onLongPress={IS_NATIVE ? onLongPress : undefined}
onAccessibilityAction={onLongPress}>
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
onAccessibilityAction={showMenu ? onLongPress : undefined}>
{({hovered, pressed, focused}) => (
<View
style={[
@@ -5,31 +5,34 @@ import {Trans} from '@lingui/react/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
import {atoms as a, tokens} from '#/alf'
import {parseConvoView} from '#/components/dms/util'
import {KnownFollowers} from '#/components/KnownFollowers'
import {Text} from '#/components/Typography'
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
export function RequestListItem({
convo: convoView,
}: {
convo: ChatBskyConvoDefs.ConvoView
}) {
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const otherUser = convo.members.find(
member => member.did !== currentAccount?.did,
)
const convo = parseConvoView(convoView, currentAccount?.did)
if (!otherUser || !moderationOpts) {
if (!convo || !moderationOpts) {
return null
}
const isDeletedAccount = otherUser.handle === 'missing.invalid'
const isDeletedAccount = convo.primaryMember.handle === 'missing.invalid'
return (
<View style={[a.relative, a.flex_1]}>
<ChatListItem convo={convo} showMenu={false}>
<ChatListItem convo={convo.view} showMenu={false}>
<View style={[a.pt_xs, a.pb_2xs]}>
<KnownFollowers
profile={otherUser}
profile={convo.primaryMember}
moderationOpts={moderationOpts}
minimal
showIfEmpty
@@ -59,17 +62,17 @@ export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
]}>
{!isDeletedAccount ? (
<>
<AcceptChatButton convo={convo} currentScreen="list" />
<AcceptChatButton convo={convo.view} currentScreen="list" />
<RejectMenu
convo={convo}
profile={otherUser}
convo={convo.view}
profile={convo.primaryMember}
showDeleteConvo
currentScreen="list"
/>
</>
) : (
<>
<DeleteChatButton convo={convo} currentScreen="list" />
<DeleteChatButton convo={convo.view} currentScreen="list" />
<View style={a.flex_1} />
</>
)}