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
-13
View File
@@ -282,19 +282,6 @@
"count": 1 "count": 1
} }
}, },
"src/components/PostControls/ShareMenu/ShareMenuItems.tsx": {
"typescript/no-floating-promises": {
"count": 3
},
"typescript/no-misused-promises": {
"count": 1
}
},
"src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx": {
"typescript/no-floating-promises": {
"count": 3
}
},
"src/components/PostControls/ShareMenu/index.tsx": { "src/components/PostControls/ShareMenu/index.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
+1 -1
View File
@@ -605,7 +605,7 @@ export type Events = {
// Group chat adoption // Group chat adoption
'groupchat:create': { 'groupchat:create': {
logContext: 'NewChatDialog' logContext: 'NewChatDialog' | 'SendViaChatDialog'
} }
'groupchat:landingPage:view': { 'groupchat:landingPage:view': {
hasSession: boolean hasSession: boolean
@@ -1,9 +1,7 @@
import {memo, useMemo} from 'react' import {memo, useMemo} from 'react'
import * as ExpoClipboard from 'expo-clipboard' import * as ExpoClipboard from 'expo-clipboard'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -37,7 +35,7 @@ let ShareMenuItems = ({
}: ShareMenuItemsProps): React.ReactNode => { }: ShareMenuItemsProps): React.ReactNode => {
const ax = useAnalytics() const ax = useAnalytics()
const {hasSession} = useSession() const {hasSession} = useSession()
const {_} = useLingui() const {t: l} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const sendViaChatControl = useDialogControl() const sendViaChatControl = useDialogControl()
const [devModeEnabled] = useDevMode() const [devModeEnabled] = useDevMode()
@@ -61,7 +59,7 @@ let ShareMenuItems = ({
const onSharePost = () => { const onSharePost = () => {
ax.metric('share:press:nativeShare', {}) ax.metric('share:press:nativeShare', {})
const url = toShareUrl(href) const url = toShareUrl(href)
shareUrl(url) void shareUrl(url)
onShareProp() onShareProp()
} }
@@ -74,7 +72,7 @@ let ShareMenuItems = ({
} else { } else {
await ExpoClipboard.setStringAsync(url) await ExpoClipboard.setStringAsync(url)
} }
Toast.show(_(msg`Copied to clipboard`), { Toast.show(l`Copied to clipboard`, {
type: 'success', type: 'success',
}) })
onShareProp() onShareProp()
@@ -93,11 +91,11 @@ let ShareMenuItems = ({
} }
const onShareATURI = () => { const onShareATURI = () => {
shareText(postUri) void shareText(postUri)
} }
const onShareAuthorDID = () => { const onShareAuthorDID = () => {
shareText(postAuthor.did) void shareText(postAuthor.did)
} }
return ( return (
@@ -113,13 +111,13 @@ let ShareMenuItems = ({
</Menu.ContainerItem> </Menu.ContainerItem>
<Menu.Item <Menu.Item
testID="postDropdownSendViaDMBtn" testID="postDropdownSendViaDMBtn"
label={_(msg`Send via direct message`)} label={l`Send via chat`}
onPress={() => { onPress={() => {
ax.metric('share:press:openDmSearch', {}) ax.metric('share:press:openDmSearch', {})
sendViaChatControl.open() sendViaChatControl.open()
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Send via direct message</Trans> <Trans>Send via chat</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemIcon icon={PaperPlaneIcon} position="right" /> <Menu.ItemIcon icon={PaperPlaneIcon} position="right" />
</Menu.Item> </Menu.Item>
@@ -129,7 +127,7 @@ let ShareMenuItems = ({
<Menu.Group> <Menu.Group>
<Menu.Item <Menu.Item
testID="postDropdownShareBtn" testID="postDropdownShareBtn"
label={_(msg`Share via...`)} label={l`Share via...`}
onPress={onSharePost}> onPress={onSharePost}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Share via...</Trans> <Trans>Share via...</Trans>
@@ -139,8 +137,8 @@ let ShareMenuItems = ({
<Menu.Item <Menu.Item
testID="postDropdownShareBtn" testID="postDropdownShareBtn"
label={_(msg`Copy link to post`)} label={l`Copy link to post`}
onPress={onCopyLink}> onPress={() => void onCopyLink()}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Copy link to post</Trans> <Trans>Copy link to post</Trans>
</Menu.ItemText> </Menu.ItemText>
@@ -164,7 +162,7 @@ let ShareMenuItems = ({
<Menu.Group> <Menu.Group>
<Menu.Item <Menu.Item
testID="postAtUriShareBtn" testID="postAtUriShareBtn"
label={_(msg`Share post at:// URI`)} label={l`Share post at:// URI`}
onPress={onShareATURI}> onPress={onShareATURI}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Share post at:// URI</Trans> <Trans>Share post at:// URI</Trans>
@@ -173,7 +171,7 @@ let ShareMenuItems = ({
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
testID="postAuthorDIDShareBtn" testID="postAuthorDIDShareBtn"
label={_(msg`Share author DID`)} label={l`Share author DID`}
onPress={onShareAuthorDID}> onPress={onShareAuthorDID}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Share author DID</Trans> <Trans>Share author DID</Trans>
@@ -183,7 +181,6 @@ let ShareMenuItems = ({
</Menu.Group> </Menu.Group>
)} )}
</Menu.Outer> </Menu.Outer>
<SendViaChatDialog <SendViaChatDialog
control={sendViaChatControl} control={sendViaChatControl}
onSelectChat={onSelectChatToShareTo} onSelectChat={onSelectChatToShareTo}
@@ -1,8 +1,6 @@
import {memo, useMemo} from 'react' import {memo, useMemo} from 'react'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
@@ -35,7 +33,7 @@ let ShareMenuItems = ({
const ax = useAnalytics() const ax = useAnalytics()
const {hasSession} = useSession() const {hasSession} = useSession()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {_} = useLingui() const {t: l} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const embedPostControl = useDialogControl() const embedPostControl = useDialogControl()
const sendViaChatControl = useDialogControl() const sendViaChatControl = useDialogControl()
@@ -60,7 +58,7 @@ let ShareMenuItems = ({
const onCopyLink = () => { const onCopyLink = () => {
ax.metric('share:press:copyLink', {}) ax.metric('share:press:copyLink', {})
const url = toShareUrl(href) const url = toShareUrl(href)
shareUrl(url) void shareUrl(url)
onShareProp() onShareProp()
} }
@@ -75,17 +73,17 @@ let ShareMenuItems = ({
const canEmbed = IS_WEB && gtMobile && !hideInPWI const canEmbed = IS_WEB && gtMobile && !hideInPWI
const onShareATURI = () => { const onShareATURI = () => {
shareText(postUri) void shareText(postUri)
} }
const onShareAuthorDID = () => { const onShareAuthorDID = () => {
shareText(postAuthor.did) void shareText(postAuthor.did)
} }
const copyLinkItem = ( const copyLinkItem = (
<Menu.Item <Menu.Item
testID="postDropdownShareBtn" testID="postDropdownShareBtn"
label={_(msg`Copy link to post`)} label={l`Copy link to post`}
onPress={onCopyLink}> onPress={onCopyLink}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Copy link to post</Trans> <Trans>Copy link to post</Trans>
@@ -102,13 +100,13 @@ let ShareMenuItems = ({
{hasSession && aa.state.access === aa.Access.Full && ( {hasSession && aa.state.access === aa.Access.Full && (
<Menu.Item <Menu.Item
testID="postDropdownSendViaDMBtn" testID="postDropdownSendViaDMBtn"
label={_(msg`Send via direct message`)} label={l`Send via chat`}
onPress={() => { onPress={() => {
ax.metric('share:press:openDmSearch', {}) ax.metric('share:press:openDmSearch', {})
sendViaChatControl.open() sendViaChatControl.open()
}}> }}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Send via direct message</Trans> <Trans>Send via chat</Trans>
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemIcon icon={Send} position="right" /> <Menu.ItemIcon icon={Send} position="right" />
</Menu.Item> </Menu.Item>
@@ -117,12 +115,12 @@ let ShareMenuItems = ({
{canEmbed && ( {canEmbed && (
<Menu.Item <Menu.Item
testID="postDropdownEmbedBtn" testID="postDropdownEmbedBtn"
label={_(msg`Embed post`)} label={l`Embed post`}
onPress={() => { onPress={() => {
ax.metric('share:press:embed', {}) ax.metric('share:press:embed', {})
embedPostControl.open() embedPostControl.open()
}}> }}>
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText> <Menu.ItemText>{l`Embed post`}</Menu.ItemText>
<Menu.ItemIcon icon={CodeBracketsIcon} position="right" /> <Menu.ItemIcon icon={CodeBracketsIcon} position="right" />
</Menu.Item> </Menu.Item>
)} )}
@@ -142,7 +140,7 @@ let ShareMenuItems = ({
<Menu.Divider /> <Menu.Divider />
<Menu.Item <Menu.Item
testID="postAtUriShareBtn" testID="postAtUriShareBtn"
label={_(msg`Copy post at:// URI`)} label={l`Copy post at:// URI`}
onPress={onShareATURI}> onPress={onShareATURI}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Copy post at:// URI</Trans> <Trans>Copy post at:// URI</Trans>
@@ -151,7 +149,7 @@ let ShareMenuItems = ({
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
testID="postAuthorDIDShareBtn" testID="postAuthorDIDShareBtn"
label={_(msg`Copy author DID`)} label={l`Copy author DID`}
onPress={onShareAuthorDID}> onPress={onShareAuthorDID}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Copy author DID</Trans> <Trans>Copy author DID</Trans>
@@ -161,7 +159,6 @@ let ShareMenuItems = ({
</> </>
)} )}
</Menu.Outer> </Menu.Outer>
{canEmbed && ( {canEmbed && (
<EmbedDialog <EmbedDialog
control={embedPostControl} control={embedPostControl}
@@ -172,7 +169,6 @@ let ShareMenuItems = ({
timestamp={timestamp} timestamp={timestamp}
/> />
)} )}
<SendViaChatDialog <SendViaChatDialog
control={sendViaChatControl} control={sendViaChatControl}
onSelectChat={onSelectChatToShareTo} onSelectChat={onSelectChatToShareTo}
+225 -12
View File
@@ -11,19 +11,33 @@ import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants' 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 {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status' import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {type ListMethods} from '#/view/com/util/List' import {type ListMethods} from '#/view/com/util/List'
import {android, atoms as a, native, useTheme, web} from '#/alf' import {android, atoms as a, native, useTheme, web} 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 * 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 TextField from '#/components/forms/TextField'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import { import {
@@ -33,18 +47,13 @@ import {
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance' import {useAgeAssurance} from '#/ageAssurance'
import {IS_NATIVE, IS_WEB} from '#/env' import {IS_NATIVE, IS_WEB} from '#/env'
import type * as bsky from '#/types/bsky' 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 NewGroupChatItem = {
type: 'newGroupChat' type: 'newGroupChat'
@@ -63,6 +72,12 @@ type ProfileItem = {
profile: bsky.profile.AnyProfileView profile: bsky.profile.AnyProfileView
} }
type ExistingChatItem = {
type: 'existingChat'
key: string
convo: ConvoWithDetails
}
type EmptyItem = { type EmptyItem = {
type: 'empty' type: 'empty'
key: string key: string
@@ -83,6 +98,7 @@ type Item =
| NewGroupChatItem | NewGroupChatItem
| LabelItem | LabelItem
| ProfileItem | ProfileItem
| ExistingChatItem
| EmptyItem | EmptyItem
| PlaceholderItem | PlaceholderItem
| ErrorItem | ErrorItem
@@ -212,11 +228,17 @@ export function InitiateChatFlow({
onSelectChat, onSelectChat,
onSelectGroupChat, onSelectGroupChat,
startInGroupChat = false, startInGroupChat = false,
showRecentConvos = false,
onSelectExistingChat,
sortByMessageDeclaration = false,
}: { }: {
title: string title: string
onSelectChat: (did: string) => void onSelectChat: (did: string) => void
onSelectGroupChat: (dids: string[], groupName: string) => void onSelectGroupChat: (dids: string[], groupName: string) => void
startInGroupChat?: boolean startInGroupChat?: boolean
showRecentConvos?: boolean
onSelectExistingChat?: (convoId: string) => void
sortByMessageDeclaration?: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -230,6 +252,12 @@ export function InitiateChatFlow({
const inputRef = useRef<TextInput>(null) const inputRef = useRef<TextInput>(null)
const accountTooNewPromptControl = Dialog.useDialogControl() const accountTooNewPromptControl = Dialog.useDialogControl()
const {data: convos} = useListConvosQuery({
enabled: showRecentConvos,
status: 'accepted',
lockStatus: 'unlocked',
})
const {data: chatStatus} = useChatActorStatusQuery() const {data: chatStatus} = useChatActorStatusQuery()
const canCreateGroups = chatStatus?.canCreateGroups ?? true const canCreateGroups = chatStatus?.canCreateGroups ?? true
const groupMemberLimit = chatStatus?.groupMemberLimit const groupMemberLimit = chatStatus?.groupMemberLimit
@@ -281,6 +309,10 @@ export function InitiateChatFlow({
let _items: Item[] = [] let _items: Item[] = []
const checker = const checker =
chatState === ChatState.NEW_GROUP_CHAT ? canBeAddedToGroup : canBeMessaged 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) { if (isError) {
_items.push({ _items.push({
@@ -310,9 +342,9 @@ export function InitiateChatFlow({
}) })
} }
_items = _items.sort(item => { if (sortByMessageDeclaration) {
return item.type === 'profile' && checker(item.profile) ? -1 : 1 _items = _items.sort(compareByMessageDeclaration)
}) }
} }
} else { } else {
const placeholders: Item[] = Array(10) const placeholders: Item[] = Array(10)
@@ -322,7 +354,57 @@ export function InitiateChatFlow({
key: i + '', 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 page of follows.pages) {
for (const profile of page.follows) { for (const profile of page.follows) {
if (!checker(profile)) continue if (!checker(profile)) continue
@@ -359,10 +441,19 @@ export function InitiateChatFlow({
_items.unshift({type: 'newGroupChat', key: 'newGroupChat'}) _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, isError,
chatState, chatState,
convos,
searchText, searchText,
l, l,
groupChatProfiles, groupChatProfiles,
@@ -370,6 +461,8 @@ export function InitiateChatFlow({
currentAccount?.did, currentAccount?.did,
follows, follows,
aa.flags.groupChatDisabled, aa.flags.groupChatDisabled,
showRecentConvos,
sortByMessageDeclaration,
]) ])
if (searchText && !isFetching && !items.length && !isError) { if (searchText && !isFetching && !items.length && !isError) {
@@ -429,6 +522,16 @@ export function InitiateChatFlow({
case 'label': { case 'label': {
return <UserLabel key={item.key} message={item.message} /> 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': { case 'profile': {
switch (chatState) { switch (chatState) {
case ChatState.NEW_CHAT: case ChatState.NEW_CHAT:
@@ -474,6 +577,8 @@ export function InitiateChatFlow({
handlePressNewGroupChat, handlePressNewGroupChat,
moderationOpts, moderationOpts,
onSelectChat, 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({ function DefaultProfileCard({
profile, profile,
moderationOpts, moderationOpts,
+2 -1
View File
@@ -89,7 +89,7 @@ export function NewChat({
}, },
onError: error => { onError: error => {
logger.error('Failed to create groupchat', {safeMessage: error}) logger.error('Failed to create groupchat', {safeMessage: error})
let errorMessage = l`An issue occurred creating the group chat, please try again.` let errorMessage = l`An issue occurred starting the group chat, please try again.`
if (isNetworkError(error)) { if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.` errorMessage = l`A network error occurred. Please check your internet connection.`
} else if ( } else if (
@@ -184,6 +184,7 @@ export function NewChat({
title={l`New chat`} title={l`New chat`}
onSelectChat={onCreateChat} onSelectChat={onCreateChat}
onSelectGroupChat={onCreateGroupChat} onSelectGroupChat={onCreateGroupChat}
sortByMessageDeclaration
startInGroupChat={startInGroupChat} startInGroupChat={startInGroupChat}
/> />
) : ( ) : (
+114 -10
View File
@@ -1,11 +1,17 @@
import {useCallback} from 'react' import {useCallback, useState} from 'react'
import {msg} from '@lingui/core/macro' import {
import {useLingui} from '@lingui/react' ChatBskyConvoGetConvoForMembers,
ChatBskyGroupCreateGroup,
} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList' import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
@@ -16,26 +22,39 @@ export function SendViaChatDialog({
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
onSelectChat: (chatId: string) => void onSelectChat: (chatId: string) => void
}) { }) {
const [flowKey, setFlowKey] = useState(0)
const onClose = useCallback(() => setFlowKey(key => key + 1), [])
return ( return (
<Dialog.Outer <Dialog.Outer
control={control} control={control}
testID="sendViaChatChatDialog" testID="sendViaChatChatDialog"
nativeOptions={{fullHeight: true}}> nativeOptions={{fullHeight: true}}
onClose={onClose}>
<Dialog.Handle /> <Dialog.Handle />
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} /> <SendViaChatDialogInner
control={control}
flowKey={flowKey}
onSelectChat={onSelectChat}
/>
</Dialog.Outer> </Dialog.Outer>
) )
} }
function SendViaChatDialogInner({ function SendViaChatDialogInner({
control, control,
flowKey,
onSelectChat, onSelectChat,
}: { }: {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
flowKey: number
onSelectChat: (chatId: string) => void onSelectChat: (chatId: string) => void
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const isGroupChatEnabled = !ax.features.enabled(ax.features.GroupChatsDisable)
const {mutate: createChat} = useGetConvoForMembers({ const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => { onSuccess: data => {
onSelectChat(data.convo.id) onSelectChat(data.convo.id)
@@ -46,8 +65,74 @@ function SendViaChatDialogInner({
ax.metric('chat:open', {logContext: 'SendViaChatDialog'}) ax.metric('chat:open', {logContext: 'SendViaChatDialog'})
}, },
onError: error => { onError: error => {
logger.error('Failed to share post to chat', {message: error}) logger.error('Failed to share post to chat', {safeMessage: error})
Toast.show(_(msg`An issue occurred while trying to open the chat`), { let errorMessage = l`An issue occurred starting the chat, please try again.`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (
error instanceof ChatBskyConvoGetConvoForMembers.AccountSuspendedError
) {
errorMessage = l`Suspended accounts cannot participate in chat.`
} else if (
error instanceof ChatBskyConvoGetConvoForMembers.BlockedActorError
) {
errorMessage = l`This user has blocked you and cannot be messaged.`
} else if (
error instanceof ChatBskyConvoGetConvoForMembers.MessagesDisabledError
) {
errorMessage = l`This user has disabled chat and cannot be messaged.`
} else if (
error instanceof
ChatBskyConvoGetConvoForMembers.NotFollowedBySenderError
) {
errorMessage = l`Chat recipient is not followed by the sender.`
} else if (
error instanceof ChatBskyConvoGetConvoForMembers.RecipientNotFoundError
) {
errorMessage = l`Unable to find the selected recipient.`
}
Toast.show(errorMessage, {
type: 'error',
})
},
})
const {mutate: createGroupChat} = useCreateGroupChat({
onSuccess: data => {
onSelectChat(data.convo.id)
ax.metric('groupchat:create', {logContext: 'SendViaChatDialog'})
},
onError: error => {
logger.error('Failed to share post to group chat', {safeMessage: error})
let errorMessage = l`An issue occurred starting the group chat, please try again.`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (
error instanceof ChatBskyGroupCreateGroup.AccountSuspendedError
) {
errorMessage = l`Suspended accounts cannot participate in a group chat.`
} else if (error instanceof ChatBskyGroupCreateGroup.BlockedActorError) {
errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.`
} else if (
error instanceof
ChatBskyGroupCreateGroup.NewAccountCannotCreateGroupError
) {
errorMessage = l`You cannot create a group chat yet.`
} else if (
error instanceof ChatBskyGroupCreateGroup.NotFollowedBySenderError
) {
errorMessage = l`A selected recipient is not followed by the sender.`
} else if (
error instanceof ChatBskyGroupCreateGroup.RecipientNotFoundError
) {
errorMessage = l`Unable to find a selected recipient.`
} else if (
error instanceof ChatBskyGroupCreateGroup.UserForbidsGroupsError
) {
errorMessage = l`One of the selected recipients does not allow group chats.`
}
Toast.show(errorMessage, {
type: 'error', type: 'error',
}) })
}, },
@@ -67,9 +152,28 @@ function SendViaChatDialogInner({
[control, createChat], [control, createChat],
) )
return ( const onCreateGroupChat = useCallback(
(members: string[], name: string) => {
control.close(() => {
createGroupChat({members, name})
})
},
[control, createGroupChat],
)
return isGroupChatEnabled ? (
<InitiateChatFlow
key={flowKey}
title={l`Send post to...`}
onSelectChat={onCreateChat}
onSelectExistingChat={onSelectExistingChat}
onSelectGroupChat={onCreateGroupChat}
showRecentConvos
sortByMessageDeclaration
/>
) : (
<SearchablePeopleList <SearchablePeopleList
title={_(msg`Send post to...`)} title={l`Send post to...`}
onSelectChat={chat => { onSelectChat={chat => {
if (chat.kind === 'user') { if (chat.kind === 'user') {
onCreateChat(chat.did) onCreateChat(chat.did)