Re-enable group invite link management (#10407)
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.19.11",
|
||||
"@atproto/api": "^0.19.12",
|
||||
"@atproto/syntax": "0.5.2",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
|
||||
@@ -72,7 +72,7 @@ export function Member({
|
||||
true,
|
||||
moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'),
|
||||
)}`
|
||||
: `Added by invite link`
|
||||
: l`Added by invite link`
|
||||
|
||||
return (
|
||||
<SubtleHoverWrapper>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
} from '#/components/icons/Bell2'
|
||||
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||
import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig'
|
||||
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
|
||||
import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock'
|
||||
@@ -47,18 +46,13 @@ import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {InviteLinkDialog} from '../components/InviteLinkDialog'
|
||||
import {AddMembersLink} from './AddMembersLink'
|
||||
import {Member, MemberPlaceholder} from './Member'
|
||||
import {MembersAndRequests} from './MembersAndRequests'
|
||||
import {EditNamePrompt, LeaveChatPrompt, LockChatPrompt} from './prompts'
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
type Item =
|
||||
| {type: 'MEMBERS_AND_REQUESTS'; key: string}
|
||||
| {type: 'ADD_MEMBERS_LINK'; key: string}
|
||||
@@ -119,11 +113,9 @@ function SettingsInner() {
|
||||
|
||||
if (!isConvoActive(convoState)) {
|
||||
return (
|
||||
<Layout.Content>
|
||||
<View style={[a.align_center, a.justify_center, a.flex_1, a.py_4xl]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
</Layout.Content>
|
||||
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,6 +142,19 @@ function keyExtractor(item: Item) {
|
||||
return item.key
|
||||
}
|
||||
|
||||
function isGroupMember(
|
||||
member: ChatBskyActorDefs.ProfileViewBasic,
|
||||
): member is GroupConvoMember {
|
||||
// Kind is missing when the account has been deleted.
|
||||
return (
|
||||
member.kind === undefined ||
|
||||
bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>(
|
||||
member.kind,
|
||||
ChatBskyActorDefs.isGroupConvoMember,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function GroupSettings({
|
||||
convo,
|
||||
}: {
|
||||
@@ -160,12 +165,12 @@ function GroupSettings({
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const primaryMember = convo?.primaryMember
|
||||
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
|
||||
const primaryMember = convo.primaryMember
|
||||
const isOwner = primaryMember.did === currentAccount?.did
|
||||
|
||||
const {data: memberListData = [], isPending} = useListConvoMembersQuery({
|
||||
convoId: convo.view.id,
|
||||
placeholderData: convo?.members,
|
||||
placeholderData: convo.members,
|
||||
})
|
||||
|
||||
// TODO Need this data in order to populate this array. -dsb
|
||||
@@ -193,18 +198,19 @@ function GroupSettings({
|
||||
]
|
||||
if (isPending) {
|
||||
// should never be pending if we correctly set the query cache data
|
||||
Array.from({length: 5}).forEach((_, i) =>
|
||||
items.push({
|
||||
type: 'CHAT_MEMBER_PLACEHOLDER',
|
||||
items.push(
|
||||
...Array.from({length: 5}, (_, i) => ({
|
||||
type: 'CHAT_MEMBER_PLACEHOLDER' as const,
|
||||
key: `chat-member-placeholder-${i}`,
|
||||
}),
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
items.push(
|
||||
...memberListData
|
||||
.filter(isGroupMember)
|
||||
.sort((a, b) => {
|
||||
const aIsOwner = a.did === primaryMember?.did
|
||||
const bIsOwner = b.did === primaryMember?.did
|
||||
const aIsOwner = a.did === primaryMember.did
|
||||
const bIsOwner = b.did === primaryMember.did
|
||||
const aIsSelf = a.did === currentAccount?.did
|
||||
const bIsSelf = b.did === currentAccount?.did
|
||||
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
|
||||
@@ -215,9 +221,9 @@ function GroupSettings({
|
||||
(profile): Item => ({
|
||||
type: 'CHAT_MEMBER',
|
||||
key: profile.did,
|
||||
profile: profile as GroupConvoMember,
|
||||
profile,
|
||||
status:
|
||||
primaryMember?.did === profile.did
|
||||
primaryMember.did === profile.did
|
||||
? 'owner'
|
||||
: invites.includes(profile.did)
|
||||
? 'invited'
|
||||
@@ -239,16 +245,16 @@ function GroupSettings({
|
||||
/>
|
||||
)
|
||||
case 'ADD_MEMBERS_LINK':
|
||||
return convo ? <AddMembersLink convo={convo} /> : null
|
||||
return <AddMembersLink convo={convo} />
|
||||
case 'CHAT_MEMBER':
|
||||
return convo ? (
|
||||
return (
|
||||
<Member
|
||||
convo={convo}
|
||||
profile={item.profile}
|
||||
status={item.status}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
case 'CHAT_MEMBER_PLACEHOLDER':
|
||||
return <MemberPlaceholder />
|
||||
default:
|
||||
@@ -265,13 +271,7 @@ function GroupSettings({
|
||||
desktopFixedHeight
|
||||
initialNumToRender={initialNumToRender}
|
||||
keyExtractor={keyExtractor}
|
||||
ListHeaderComponent={
|
||||
convo?.kind === 'group' ? (
|
||||
<SettingsHeader convo={convo} isOwner={isOwner} />
|
||||
) : (
|
||||
<SettingsHeaderPlaceholder />
|
||||
)
|
||||
}
|
||||
ListHeaderComponent={<SettingsHeader convo={convo} isOwner={isOwner} />}
|
||||
renderItem={renderItem}
|
||||
sideBorders={false}
|
||||
windowSize={11}
|
||||
@@ -287,7 +287,7 @@ function SettingsHeader({
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {i18n, t: l} = useLingui()
|
||||
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
@@ -296,24 +296,25 @@ function SettingsHeader({
|
||||
|
||||
const lockStatus = convo.details.lockStatus
|
||||
|
||||
// TODO Enable this once the feature is working end-to-end. -dsb
|
||||
// const {joinLink} = convo.details
|
||||
const isJoinLinkEnabled = false
|
||||
// const isJoinLinkEnabled =
|
||||
// isOwner || (!isOwner && joinLink?.enabledStatus === 'enabled')
|
||||
const {joinLink} = convo.details
|
||||
const isJoinLinkEnabled = isOwner || joinLink?.enabledStatus === 'enabled'
|
||||
|
||||
// TODO Enable this once the feature is working end-to-end. -dsb
|
||||
const isReportLinkEnabled = false
|
||||
|
||||
const {mutate: editGroupName} = useEditGroupChatName(convo.view.id, {
|
||||
onError: e => {
|
||||
setNewGroupName(groupName)
|
||||
logger.error('Failed to edit group chat name', {message: e})
|
||||
Toast.show(l`Failed to edit group chat name`, {type: 'error'})
|
||||
},
|
||||
})
|
||||
const {mutate: editGroupName, isPending: isEditingName} =
|
||||
useEditGroupChatName(convo.view.id, {
|
||||
onSuccess: () => {
|
||||
Toast.show(l({message: 'Group chat name updated', context: 'toast'}))
|
||||
},
|
||||
onError: e => {
|
||||
setNewGroupName(groupName)
|
||||
logger.error('Failed to edit group chat name', {message: e})
|
||||
Toast.show(l`Failed to edit group chat name`, {type: 'error'})
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: muteConvo} = useMuteConvo(convo.view.id, {
|
||||
const {mutate: muteConvo, isPending: isMuting} = useMuteConvo(convo.view.id, {
|
||||
onSuccess: data => {
|
||||
if (data.convo.muted) {
|
||||
Toast.show(l({message: 'Group chat muted', context: 'toast'}))
|
||||
@@ -327,38 +328,44 @@ function SettingsHeader({
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, {
|
||||
onSuccess: () => {
|
||||
// Settings > Chat > Chat list
|
||||
navigation.dispatch(StackActions.pop(2))
|
||||
const {mutate: leaveConvo, isPending: isLeaving} = useLeaveConvo(
|
||||
convo.view.id,
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigation.replace('Messages', {animation: 'pop'})
|
||||
},
|
||||
onError: e => {
|
||||
logger.error('Failed to leave group chat', {message: e})
|
||||
Toast.show(
|
||||
l({message: 'Failed to leave group chat', context: 'toast'}),
|
||||
{type: 'error'},
|
||||
)
|
||||
},
|
||||
},
|
||||
onError: e => {
|
||||
logger.error('Failed to leave group chat', {message: e})
|
||||
Toast.show(l({message: 'Failed to leave group chat', context: 'toast'}), {
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const {mutate: lockConvo} = useLockConvo(convo.view.id, {
|
||||
onSuccess: data => {
|
||||
const kind = data.convo.kind as ChatBskyConvoDefs.GroupConvo
|
||||
if (kind.lockStatus === 'locked') {
|
||||
Toast.show(l({message: 'Group chat locked', context: 'toast'}))
|
||||
} else {
|
||||
Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
|
||||
}
|
||||
const {mutate: lockConvo, isPending: isLocking} = useLockConvo(
|
||||
convo.view.id,
|
||||
{
|
||||
onSuccess: data => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(data.convo.kind)) return
|
||||
if (data.convo.kind.lockStatus === 'locked') {
|
||||
Toast.show(l({message: 'Group chat locked', context: 'toast'}))
|
||||
} else {
|
||||
Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
|
||||
}
|
||||
},
|
||||
onError: (e, {lock}) => {
|
||||
if (lock) {
|
||||
logger.error('Failed to lock group chat', {message: e})
|
||||
Toast.show(l`Failed to lock group chat`, {type: 'error'})
|
||||
} else {
|
||||
logger.error('Failed to unlock group chat', {message: e})
|
||||
Toast.show(l`Failed to unlock group chat`, {type: 'error'})
|
||||
}
|
||||
},
|
||||
},
|
||||
onError: (e, {lock}) => {
|
||||
if (lock) {
|
||||
logger.error('Failed to lock group chat', {message: e})
|
||||
Toast.show(l`Failed to lock group chat`, {type: 'error'})
|
||||
} else {
|
||||
logger.error('Failed to unlock group chat', {message: e})
|
||||
Toast.show(l`Failed to unlock group chat`, {type: 'error'})
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const inviteLinkDialog = Dialog.useDialogControl()
|
||||
const editNamePrompt = Prompt.usePromptControl()
|
||||
@@ -389,9 +396,7 @@ function SettingsHeader({
|
||||
lockConvo({lock: false})
|
||||
}
|
||||
|
||||
// TODO The creation date doesn't exist yet. -dsb
|
||||
const showCreatedAt = false
|
||||
const createdAt = new Date()
|
||||
const createdAt = new Date(convo.details.createdAt)
|
||||
|
||||
const canLockGroupChat = isOwner && lockStatus !== 'locked-permanently'
|
||||
|
||||
@@ -412,18 +417,23 @@ function SettingsHeader({
|
||||
]}>
|
||||
{groupName}
|
||||
</Text>
|
||||
{showCreatedAt ? (
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.text_center,
|
||||
a.pt_xs,
|
||||
a.px_xl,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Created {dateFormatter.format(createdAt)}</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.text_center,
|
||||
a.pt_xs,
|
||||
a.px_xl,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>
|
||||
Created{' '}
|
||||
{i18n.date(createdAt, {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
@@ -434,6 +444,7 @@ function SettingsHeader({
|
||||
]}>
|
||||
<SettingsButton
|
||||
color={convo.view.muted ? 'negative_subtle' : 'secondary'}
|
||||
disabled={isMuting}
|
||||
icon={convo.view.muted ? BellOffIcon : BellIcon}
|
||||
label={
|
||||
convo.view.muted
|
||||
@@ -445,6 +456,7 @@ function SettingsHeader({
|
||||
/>
|
||||
{isOwner ? (
|
||||
<SettingsButton
|
||||
disabled={isEditingName}
|
||||
icon={EditIcon}
|
||||
label={l`Edit this group chat’s name`}
|
||||
text={l`Edit name`}
|
||||
@@ -453,6 +465,7 @@ function SettingsHeader({
|
||||
) : null}
|
||||
{isJoinLinkEnabled ? (
|
||||
<SettingsButton
|
||||
disabled={lockStatus !== 'unlocked'}
|
||||
icon={ChainLinkIcon}
|
||||
label={
|
||||
isOwner
|
||||
@@ -466,6 +479,7 @@ function SettingsHeader({
|
||||
{canLockGroupChat ? (
|
||||
<SettingsButton
|
||||
color={lockStatus === 'locked' ? 'negative_subtle' : 'secondary'}
|
||||
disabled={isLocking}
|
||||
icon={LockIcon}
|
||||
label={
|
||||
lockStatus === 'locked'
|
||||
@@ -478,18 +492,17 @@ function SettingsHeader({
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{isOwner ? null : isReportLinkEnabled ? (
|
||||
{!isOwner && isReportLinkEnabled && (
|
||||
<SettingsButton
|
||||
color="secondary"
|
||||
icon={FlagIcon}
|
||||
label={l`Report this group chat`}
|
||||
text={l`Report`}
|
||||
onPress={handleReportChat}
|
||||
/>
|
||||
) : null}
|
||||
{isOwner ? null : (
|
||||
)}
|
||||
{!isOwner && (
|
||||
<SettingsButton
|
||||
color="secondary"
|
||||
disabled={isLeaving}
|
||||
icon={ArrowBoxLeftIcon}
|
||||
label={l`Leave this group chat`}
|
||||
text={l`Leave`}
|
||||
@@ -519,45 +532,6 @@ function SettingsHeader({
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsHeaderPlaceholder() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}>
|
||||
<View style={[a.align_center, a.justify_center]}>
|
||||
<AvatarBubbles profiles={[]} />
|
||||
</View>
|
||||
<Text
|
||||
style={[a.text_2xl, a.font_bold, a.text_center, a.pt_lg, t.atoms.text]}>
|
||||
…
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.text_center,
|
||||
a.pt_xs,
|
||||
a.px_xl,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
…
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.gap_2xl,
|
||||
a.pt_2xl,
|
||||
]}>
|
||||
<SettingsButtonPlaceholder />
|
||||
<SettingsButtonPlaceholder />
|
||||
<SettingsButtonPlaceholder />
|
||||
<SettingsButtonPlaceholder />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsButton({
|
||||
color = 'secondary',
|
||||
disabled,
|
||||
@@ -606,27 +580,3 @@ function SettingsButton({
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsButtonPlaceholder() {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<View style={[a.align_center]}>
|
||||
<Button color="secondary" size="large" shape="round" label={l`Loading…`}>
|
||||
<ButtonIcon icon={EllipsisIcon} size="md" />
|
||||
</Button>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.font_medium,
|
||||
a.text_center,
|
||||
a.pt_xs,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
…
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,33 +21,31 @@ export function EditNamePrompt({
|
||||
|
||||
return (
|
||||
<Prompt.Outer control={control}>
|
||||
<>
|
||||
<Prompt.Content>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Edit group name</Trans>
|
||||
</Prompt.TitleText>
|
||||
<View style={[a.my_sm]}>
|
||||
<TextField.Root isInvalid={false}>
|
||||
<TextField.Input
|
||||
label={l`Edit group name`}
|
||||
placeholder={l`Group name`}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
returnKeyType="done"
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
autoFocus
|
||||
onSubmitEditing={onConfirm}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
</Prompt.Content>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action cta={l`Save`} onPress={onConfirm} />
|
||||
<Prompt.Cancel />
|
||||
</Prompt.Actions>
|
||||
</>
|
||||
<Prompt.Content>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Edit group name</Trans>
|
||||
</Prompt.TitleText>
|
||||
<View style={[a.my_sm]}>
|
||||
<TextField.Root isInvalid={false}>
|
||||
<TextField.Input
|
||||
label={l`Edit group name`}
|
||||
placeholder={l`Group name`}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
returnKeyType="done"
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
autoFocus
|
||||
onSubmitEditing={onConfirm}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
</Prompt.Content>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action cta={l`Save`} onPress={onConfirm} />
|
||||
<Prompt.Cancel />
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@atproto/api@^0.19.11":
|
||||
version "0.19.11"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.11.tgz#73885a47959907f22b68d671011ee70f80afd44b"
|
||||
integrity sha512-7V4Sg6hcv/UxoXobjfvy/Ox2ioKQtZ3DzbsiFndYCcBfsZ5GO8rNEroHPq3hT0CFBJK1NAD6JfOtTBN2z267Xg==
|
||||
"@atproto/api@^0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.12.tgz#0c21d7de070270f6596f908e6d8dd590b2933395"
|
||||
integrity sha512-5CxDPBPxWx30ZM0UwjsmY67l+Kl8lTiLnH/8QvymsuJuRwA47CVfqo+QzEwJSkojNm7s0mTp7Ln2mR+OcFGBuw==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.4.21"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
|
||||
Reference in New Issue
Block a user