Enable sharing a post to a new group chat (#11191)

This commit is contained in:
DS Boyce
2026-07-22 15:54:46 -07:00
committed by GitHub
parent 8d7a7369aa
commit f38f84e1a3
7 changed files with 367 additions and 69 deletions
+225 -12
View File
@@ -11,19 +11,33 @@ import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {type ListMethods} from '#/view/com/util/List'
import {android, atoms as a, native, useTheme, web} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {canBeAddedToGroup, canBeMessaged} from '#/components/dms/util'
import {ChatProfileTabs} from '#/components/dms/ChatProfileTabs'
import {EmptyMemberList} from '#/components/dms/components/EmptyMemberList'
import {GroupChatProfileCard} from '#/components/dms/components/GroupChatProfileCard'
import {ProfileCardSkeleton} from '#/components/dms/components/ProfileCardSkeleton'
import {UserLabel} from '#/components/dms/components/UserLabel'
import {UserSearchInput} from '#/components/dms/components/UserSearchInput'
import {
canBeAddedToGroup,
canBeMessaged,
type ConvoWithDetails,
parseConvoView,
} from '#/components/dms/util'
import * as TextField from '#/components/forms/TextField'
import * as Toggle from '#/components/forms/Toggle'
import {
@@ -33,18 +47,13 @@ import {
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as ProfileCard from '#/components/ProfileCard'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance'
import {IS_NATIVE, IS_WEB} from '#/env'
import type * as bsky from '#/types/bsky'
import {ChatProfileTabs} from './ChatProfileTabs'
import {EmptyMemberList} from './components/EmptyMemberList'
import {GroupChatProfileCard} from './components/GroupChatProfileCard'
import {ProfileCardSkeleton} from './components/ProfileCardSkeleton'
import {UserLabel} from './components/UserLabel'
import {UserSearchInput} from './components/UserSearchInput'
type NewGroupChatItem = {
type: 'newGroupChat'
@@ -63,6 +72,12 @@ type ProfileItem = {
profile: bsky.profile.AnyProfileView
}
type ExistingChatItem = {
type: 'existingChat'
key: string
convo: ConvoWithDetails
}
type EmptyItem = {
type: 'empty'
key: string
@@ -83,6 +98,7 @@ type Item =
| NewGroupChatItem
| LabelItem
| ProfileItem
| ExistingChatItem
| EmptyItem
| PlaceholderItem
| ErrorItem
@@ -212,11 +228,17 @@ export function InitiateChatFlow({
onSelectChat,
onSelectGroupChat,
startInGroupChat = false,
showRecentConvos = false,
onSelectExistingChat,
sortByMessageDeclaration = false,
}: {
title: string
onSelectChat: (did: string) => void
onSelectGroupChat: (dids: string[], groupName: string) => void
startInGroupChat?: boolean
showRecentConvos?: boolean
onSelectExistingChat?: (convoId: string) => void
sortByMessageDeclaration?: boolean
}) {
const t = useTheme()
const {t: l} = useLingui()
@@ -230,6 +252,12 @@ export function InitiateChatFlow({
const inputRef = useRef<TextInput>(null)
const accountTooNewPromptControl = Dialog.useDialogControl()
const {data: convos} = useListConvosQuery({
enabled: showRecentConvos,
status: 'accepted',
lockStatus: 'unlocked',
})
const {data: chatStatus} = useChatActorStatusQuery()
const canCreateGroups = chatStatus?.canCreateGroups ?? true
const groupMemberLimit = chatStatus?.groupMemberLimit
@@ -281,6 +309,10 @@ export function InitiateChatFlow({
let _items: Item[] = []
const checker =
chatState === ChatState.NEW_GROUP_CHAT ? canBeAddedToGroup : canBeMessaged
const messageDeclarationRank = (item: Item) =>
item.type === 'profile' && checker(item.profile) ? 0 : 1
const compareByMessageDeclaration = (a: Item, b: Item) =>
messageDeclarationRank(a) - messageDeclarationRank(b)
if (isError) {
_items.push({
@@ -310,9 +342,9 @@ export function InitiateChatFlow({
})
}
_items = _items.sort(item => {
return item.type === 'profile' && checker(item.profile) ? -1 : 1
})
if (sortByMessageDeclaration) {
_items = _items.sort(compareByMessageDeclaration)
}
}
} else {
const placeholders: Item[] = Array(10)
@@ -322,7 +354,57 @@ export function InitiateChatFlow({
key: i + '',
}))
if (follows) {
if (
chatState === ChatState.NEW_CHAT &&
showRecentConvos &&
convos &&
follows
) {
const usedDids = new Set()
for (const page of convos.pages) {
for (const convoView of page.convos) {
const convo = parseConvoView(convoView, currentAccount?.did)
if (!convo) continue
if (convo.kind === 'group') {
_items.push({
type: 'existingChat',
key: convo.view.id,
convo,
})
} else {
if (convo.primaryMember.handle === 'missing.invalid') continue
if (usedDids.has(convo.primaryMember.did)) continue
usedDids.add(convo.primaryMember.did)
_items.push({
type: 'existingChat',
key: convo.view.id,
convo,
})
}
}
}
let followsItems: ProfileItem[] = []
for (const page of follows.pages) {
for (const profile of page.follows) {
if (usedDids.has(profile.did)) continue
if (!checker(profile)) continue
followsItems.push({
type: 'profile',
key: profile.did,
profile,
})
}
}
_items.push(...followsItems)
} else if (follows) {
for (const page of follows.pages) {
for (const profile of page.follows) {
if (!checker(profile)) continue
@@ -359,10 +441,19 @@ export function InitiateChatFlow({
_items.unshift({type: 'newGroupChat', key: 'newGroupChat'})
}
return _items
const profileDids = new Set<string>()
return _items.filter(item => {
if (item.type !== 'profile') return true
if (profileDids.has(item.profile.did)) return false
profileDids.add(item.profile.did)
return true
})
}, [
isError,
chatState,
convos,
searchText,
l,
groupChatProfiles,
@@ -370,6 +461,8 @@ export function InitiateChatFlow({
currentAccount?.did,
follows,
aa.flags.groupChatDisabled,
showRecentConvos,
sortByMessageDeclaration,
])
if (searchText && !isFetching && !items.length && !isError) {
@@ -429,6 +522,16 @@ export function InitiateChatFlow({
case 'label': {
return <UserLabel key={item.key} message={item.message} />
}
case 'existingChat': {
return showRecentConvos && onSelectExistingChat ? (
<ExistingChatCard
key={item.key}
convo={item.convo}
moderationOpts={moderationOpts!}
onPress={onSelectExistingChat}
/>
) : null
}
case 'profile': {
switch (chatState) {
case ChatState.NEW_CHAT:
@@ -474,6 +577,8 @@ export function InitiateChatFlow({
handlePressNewGroupChat,
moderationOpts,
onSelectChat,
onSelectExistingChat,
showRecentConvos,
],
)
@@ -845,6 +950,114 @@ function NewGroupChatButton({
)
}
function ExistingChatCard({
convo,
moderationOpts,
onPress,
}: {
convo: ConvoWithDetails
moderationOpts: ModerationOpts
onPress: (convoId: string) => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const enabled =
convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true
const name =
convo.kind === 'group'
? convo.details.name
: createSanitizedDisplayName(
convo.primaryMember,
true,
moderateProfile(convo.primaryMember, moderationOpts).ui(
'displayName',
),
)
const handleOnPress = useCallback(() => {
onPress(convo.view.id)
}, [onPress, convo.view.id])
return (
<Button
disabled={!enabled}
label={l`Select chat "${name}"`}
onPress={handleOnPress}>
{({hovered, pressed, focused}) => (
<View
style={[
a.flex_1,
a.py_sm,
a.px_lg,
!enabled
? {opacity: 0.5}
: pressed || focused || hovered
? t.atoms.bg_contrast_25
: t.atoms.bg,
]}>
<ProfileCard.Header>
{convo.kind === 'group' ? (
<AvatarBubbles profiles={convo.members} size={40} />
) : (
<ProfileCard.Avatar
profile={convo.primaryMember}
moderationOpts={moderationOpts}
disabledPreview
/>
)}
<View style={[a.flex_1]}>
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
<Text
emoji
style={[
a.text_md,
a.font_semi_bold,
a.leading_snug,
a.self_start,
a.flex_shrink,
]}
numberOfLines={1}>
{name}
</Text>
{convo.kind === 'direct' && (
<ProfileBadges
profile={convo.primaryMember}
size="md"
style={[a.pl_xs]}
/>
)}
</View>
{convo.kind === 'direct' ? (
<ProfileCard.Handle profile={convo.primaryMember} />
) : (
<>
{enabled ? (
<Text
style={[a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={2}>
<Plural
value={convo.details.memberCount}
one="# member"
other="# members"
/>
</Text>
) : (
<Text
style={[a.leading_snug, t.atoms.text_contrast_high]}
numberOfLines={2}>
<Trans>Group is locked</Trans>
</Text>
)}
</>
)}
</View>
</ProfileCard.Header>
</View>
)}
</Button>
)
}
function DefaultProfileCard({
profile,
moderationOpts,