Enable sharing a post to a new group chat (#11191)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user