Re-enable group invite link management (#10407)

This commit is contained in:
DS Boyce
2026-05-04 23:44:27 +01:00
committed by GitHub
parent e34080c4a0
commit 482fb77e36
5 changed files with 142 additions and 194 deletions
+1 -1
View File
@@ -81,7 +81,7 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.19.11", "@atproto/api": "^0.19.12",
"@atproto/syntax": "0.5.2", "@atproto/syntax": "0.5.2",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
@@ -72,7 +72,7 @@ export function Member({
true, true,
moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'), moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'),
)}` )}`
: `Added by invite link` : l`Added by invite link`
return ( return (
<SubtleHoverWrapper> <SubtleHoverWrapper>
@@ -1,8 +1,8 @@
import {useState} from 'react' import {useState} from 'react'
import {View} from 'react-native' 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 {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 {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
@@ -38,7 +38,6 @@ import {
} from '#/components/icons/Bell2' } from '#/components/icons/Bell2'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {type Props as SVGIconProps} from '#/components/icons/common' 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 {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag' import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' 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 Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {InviteLinkDialog} from '../components/InviteLinkDialog' import {InviteLinkDialog} from '../components/InviteLinkDialog'
import {AddMembersLink} from './AddMembersLink' import {AddMembersLink} from './AddMembersLink'
import {Member, MemberPlaceholder} from './Member' import {Member, MemberPlaceholder} from './Member'
import {MembersAndRequests} from './MembersAndRequests' import {MembersAndRequests} from './MembersAndRequests'
import {EditNamePrompt, LeaveChatPrompt, LockChatPrompt} from './prompts' import {EditNamePrompt, LeaveChatPrompt, LockChatPrompt} from './prompts'
const dateFormatter = new Intl.DateTimeFormat(undefined, {
month: 'long',
day: 'numeric',
year: 'numeric',
})
type Item = type Item =
| {type: 'MEMBERS_AND_REQUESTS'; key: string} | {type: 'MEMBERS_AND_REQUESTS'; key: string}
| {type: 'ADD_MEMBERS_LINK'; key: string} | {type: 'ADD_MEMBERS_LINK'; key: string}
@@ -119,11 +113,9 @@ function SettingsInner() {
if (!isConvoActive(convoState)) { if (!isConvoActive(convoState)) {
return ( return (
<Layout.Content> <View style={[a.flex_1, a.align_center, a.justify_center]}>
<View style={[a.align_center, a.justify_center, a.flex_1, a.py_4xl]}>
<Loader size="xl" /> <Loader size="xl" />
</View> </View>
</Layout.Content>
) )
} }
@@ -150,6 +142,19 @@ function keyExtractor(item: Item) {
return item.key 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({ function GroupSettings({
convo, convo,
}: { }: {
@@ -160,12 +165,12 @@ function GroupSettings({
const {currentAccount} = useSession() const {currentAccount} = useSession()
const primaryMember = convo?.primaryMember const primaryMember = convo.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did const isOwner = primaryMember.did === currentAccount?.did
const {data: memberListData = [], isPending} = useListConvoMembersQuery({ const {data: memberListData = [], isPending} = useListConvoMembersQuery({
convoId: convo.view.id, convoId: convo.view.id,
placeholderData: convo?.members, placeholderData: convo.members,
}) })
// TODO Need this data in order to populate this array. -dsb // TODO Need this data in order to populate this array. -dsb
@@ -193,18 +198,19 @@ function GroupSettings({
] ]
if (isPending) { if (isPending) {
// should never be pending if we correctly set the query cache data // should never be pending if we correctly set the query cache data
Array.from({length: 5}).forEach((_, i) => items.push(
items.push({ ...Array.from({length: 5}, (_, i) => ({
type: 'CHAT_MEMBER_PLACEHOLDER', type: 'CHAT_MEMBER_PLACEHOLDER' as const,
key: `chat-member-placeholder-${i}`, key: `chat-member-placeholder-${i}`,
}), })),
) )
} else { } else {
items.push( items.push(
...memberListData ...memberListData
.filter(isGroupMember)
.sort((a, b) => { .sort((a, b) => {
const aIsOwner = a.did === primaryMember?.did const aIsOwner = a.did === primaryMember.did
const bIsOwner = b.did === primaryMember?.did const bIsOwner = b.did === primaryMember.did
const aIsSelf = a.did === currentAccount?.did const aIsSelf = a.did === currentAccount?.did
const bIsSelf = b.did === currentAccount?.did const bIsSelf = b.did === currentAccount?.did
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1 if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
@@ -215,9 +221,9 @@ function GroupSettings({
(profile): Item => ({ (profile): Item => ({
type: 'CHAT_MEMBER', type: 'CHAT_MEMBER',
key: profile.did, key: profile.did,
profile: profile as GroupConvoMember, profile,
status: status:
primaryMember?.did === profile.did primaryMember.did === profile.did
? 'owner' ? 'owner'
: invites.includes(profile.did) : invites.includes(profile.did)
? 'invited' ? 'invited'
@@ -239,16 +245,16 @@ function GroupSettings({
/> />
) )
case 'ADD_MEMBERS_LINK': case 'ADD_MEMBERS_LINK':
return convo ? <AddMembersLink convo={convo} /> : null return <AddMembersLink convo={convo} />
case 'CHAT_MEMBER': case 'CHAT_MEMBER':
return convo ? ( return (
<Member <Member
convo={convo} convo={convo}
profile={item.profile} profile={item.profile}
status={item.status} status={item.status}
isOwner={isOwner} isOwner={isOwner}
/> />
) : null )
case 'CHAT_MEMBER_PLACEHOLDER': case 'CHAT_MEMBER_PLACEHOLDER':
return <MemberPlaceholder /> return <MemberPlaceholder />
default: default:
@@ -265,13 +271,7 @@ function GroupSettings({
desktopFixedHeight desktopFixedHeight
initialNumToRender={initialNumToRender} initialNumToRender={initialNumToRender}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
ListHeaderComponent={ ListHeaderComponent={<SettingsHeader convo={convo} isOwner={isOwner} />}
convo?.kind === 'group' ? (
<SettingsHeader convo={convo} isOwner={isOwner} />
) : (
<SettingsHeaderPlaceholder />
)
}
renderItem={renderItem} renderItem={renderItem}
sideBorders={false} sideBorders={false}
windowSize={11} windowSize={11}
@@ -287,7 +287,7 @@ function SettingsHeader({
isOwner: boolean isOwner: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {i18n, t: l} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@@ -296,16 +296,17 @@ function SettingsHeader({
const lockStatus = convo.details.lockStatus const lockStatus = convo.details.lockStatus
// TODO Enable this once the feature is working end-to-end. -dsb const {joinLink} = convo.details
// const {joinLink} = convo.details const isJoinLinkEnabled = isOwner || joinLink?.enabledStatus === 'enabled'
const isJoinLinkEnabled = false
// const isJoinLinkEnabled =
// isOwner || (!isOwner && joinLink?.enabledStatus === 'enabled')
// TODO Enable this once the feature is working end-to-end. -dsb // TODO Enable this once the feature is working end-to-end. -dsb
const isReportLinkEnabled = false const isReportLinkEnabled = false
const {mutate: editGroupName} = useEditGroupChatName(convo.view.id, { const {mutate: editGroupName, isPending: isEditingName} =
useEditGroupChatName(convo.view.id, {
onSuccess: () => {
Toast.show(l({message: 'Group chat name updated', context: 'toast'}))
},
onError: e => { onError: e => {
setNewGroupName(groupName) setNewGroupName(groupName)
logger.error('Failed to edit group chat name', {message: e}) logger.error('Failed to edit group chat name', {message: e})
@@ -313,7 +314,7 @@ function SettingsHeader({
}, },
}) })
const {mutate: muteConvo} = useMuteConvo(convo.view.id, { const {mutate: muteConvo, isPending: isMuting} = useMuteConvo(convo.view.id, {
onSuccess: data => { onSuccess: data => {
if (data.convo.muted) { if (data.convo.muted) {
Toast.show(l({message: 'Group chat muted', context: 'toast'})) Toast.show(l({message: 'Group chat muted', context: 'toast'}))
@@ -327,23 +328,28 @@ function SettingsHeader({
}, },
}) })
const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, { const {mutate: leaveConvo, isPending: isLeaving} = useLeaveConvo(
convo.view.id,
{
onSuccess: () => { onSuccess: () => {
// Settings > Chat > Chat list navigation.replace('Messages', {animation: 'pop'})
navigation.dispatch(StackActions.pop(2))
}, },
onError: e => { onError: e => {
logger.error('Failed to leave group chat', {message: e}) logger.error('Failed to leave group chat', {message: e})
Toast.show(l({message: 'Failed to leave group chat', context: 'toast'}), { Toast.show(
type: 'error', l({message: 'Failed to leave group chat', context: 'toast'}),
}) {type: 'error'},
)
}, },
}) },
)
const {mutate: lockConvo} = useLockConvo(convo.view.id, { const {mutate: lockConvo, isPending: isLocking} = useLockConvo(
convo.view.id,
{
onSuccess: data => { onSuccess: data => {
const kind = data.convo.kind as ChatBskyConvoDefs.GroupConvo if (!ChatBskyConvoDefs.isGroupConvo(data.convo.kind)) return
if (kind.lockStatus === 'locked') { if (data.convo.kind.lockStatus === 'locked') {
Toast.show(l({message: 'Group chat locked', context: 'toast'})) Toast.show(l({message: 'Group chat locked', context: 'toast'}))
} else { } else {
Toast.show(l({message: 'Group chat unlocked', context: 'toast'})) Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
@@ -358,7 +364,8 @@ function SettingsHeader({
Toast.show(l`Failed to unlock group chat`, {type: 'error'}) Toast.show(l`Failed to unlock group chat`, {type: 'error'})
} }
}, },
}) },
)
const inviteLinkDialog = Dialog.useDialogControl() const inviteLinkDialog = Dialog.useDialogControl()
const editNamePrompt = Prompt.usePromptControl() const editNamePrompt = Prompt.usePromptControl()
@@ -389,9 +396,7 @@ function SettingsHeader({
lockConvo({lock: false}) lockConvo({lock: false})
} }
// TODO The creation date doesn't exist yet. -dsb const createdAt = new Date(convo.details.createdAt)
const showCreatedAt = false
const createdAt = new Date()
const canLockGroupChat = isOwner && lockStatus !== 'locked-permanently' const canLockGroupChat = isOwner && lockStatus !== 'locked-permanently'
@@ -412,7 +417,6 @@ function SettingsHeader({
]}> ]}>
{groupName} {groupName}
</Text> </Text>
{showCreatedAt ? (
<Text <Text
style={[ style={[
a.text_sm, a.text_sm,
@@ -421,9 +425,15 @@ function SettingsHeader({
a.px_xl, a.px_xl,
t.atoms.text_contrast_high, t.atoms.text_contrast_high,
]}> ]}>
<Trans>Created {dateFormatter.format(createdAt)}</Trans> <Trans>
Created{' '}
{i18n.date(createdAt, {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</Trans>
</Text> </Text>
) : null}
<View <View
style={[ style={[
a.flex_row, a.flex_row,
@@ -434,6 +444,7 @@ function SettingsHeader({
]}> ]}>
<SettingsButton <SettingsButton
color={convo.view.muted ? 'negative_subtle' : 'secondary'} color={convo.view.muted ? 'negative_subtle' : 'secondary'}
disabled={isMuting}
icon={convo.view.muted ? BellOffIcon : BellIcon} icon={convo.view.muted ? BellOffIcon : BellIcon}
label={ label={
convo.view.muted convo.view.muted
@@ -445,6 +456,7 @@ function SettingsHeader({
/> />
{isOwner ? ( {isOwner ? (
<SettingsButton <SettingsButton
disabled={isEditingName}
icon={EditIcon} icon={EditIcon}
label={l`Edit this group chats name`} label={l`Edit this group chats name`}
text={l`Edit name`} text={l`Edit name`}
@@ -453,6 +465,7 @@ function SettingsHeader({
) : null} ) : null}
{isJoinLinkEnabled ? ( {isJoinLinkEnabled ? (
<SettingsButton <SettingsButton
disabled={lockStatus !== 'unlocked'}
icon={ChainLinkIcon} icon={ChainLinkIcon}
label={ label={
isOwner isOwner
@@ -466,6 +479,7 @@ function SettingsHeader({
{canLockGroupChat ? ( {canLockGroupChat ? (
<SettingsButton <SettingsButton
color={lockStatus === 'locked' ? 'negative_subtle' : 'secondary'} color={lockStatus === 'locked' ? 'negative_subtle' : 'secondary'}
disabled={isLocking}
icon={LockIcon} icon={LockIcon}
label={ label={
lockStatus === 'locked' lockStatus === 'locked'
@@ -478,18 +492,17 @@ function SettingsHeader({
} }
/> />
) : null} ) : null}
{isOwner ? null : isReportLinkEnabled ? ( {!isOwner && isReportLinkEnabled && (
<SettingsButton <SettingsButton
color="secondary"
icon={FlagIcon} icon={FlagIcon}
label={l`Report this group chat`} label={l`Report this group chat`}
text={l`Report`} text={l`Report`}
onPress={handleReportChat} onPress={handleReportChat}
/> />
) : null} )}
{isOwner ? null : ( {!isOwner && (
<SettingsButton <SettingsButton
color="secondary" disabled={isLeaving}
icon={ArrowBoxLeftIcon} icon={ArrowBoxLeftIcon}
label={l`Leave this group chat`} label={l`Leave this group chat`}
text={l`Leave`} 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({ function SettingsButton({
color = 'secondary', color = 'secondary',
disabled, disabled,
@@ -606,27 +580,3 @@ function SettingsButton({
</View> </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,7 +21,6 @@ export function EditNamePrompt({
return ( return (
<Prompt.Outer control={control}> <Prompt.Outer control={control}>
<>
<Prompt.Content> <Prompt.Content>
<Prompt.TitleText> <Prompt.TitleText>
<Trans>Edit group name</Trans> <Trans>Edit group name</Trans>
@@ -47,7 +46,6 @@ export function EditNamePrompt({
<Prompt.Action cta={l`Save`} onPress={onConfirm} /> <Prompt.Action cta={l`Save`} onPress={onConfirm} />
<Prompt.Cancel /> <Prompt.Cancel />
</Prompt.Actions> </Prompt.Actions>
</>
</Prompt.Outer> </Prompt.Outer>
) )
} }
+4 -4
View File
@@ -20,10 +20,10 @@
"@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/trace-mapping" "^0.3.9"
"@atproto/api@^0.19.11": "@atproto/api@^0.19.12":
version "0.19.11" version "0.19.12"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.11.tgz#73885a47959907f22b68d671011ee70f80afd44b" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.12.tgz#0c21d7de070270f6596f908e6d8dd590b2933395"
integrity sha512-7V4Sg6hcv/UxoXobjfvy/Ox2ioKQtZ3DzbsiFndYCcBfsZ5GO8rNEroHPq3hT0CFBJK1NAD6JfOtTBN2z267Xg== integrity sha512-5CxDPBPxWx30ZM0UwjsmY67l+Kl8lTiLnH/8QvymsuJuRwA47CVfqo+QzEwJSkojNm7s0mTp7Ln2mR+OcFGBuw==
dependencies: dependencies:
"@atproto/common-web" "^0.4.21" "@atproto/common-web" "^0.4.21"
"@atproto/lexicon" "^0.6.2" "@atproto/lexicon" "^0.6.2"