Add full member list to settings
This commit is contained in:
@@ -11,9 +11,11 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
import {
|
||||||
|
type ConvoWithDetails,
|
||||||
|
type GroupConvoMember,
|
||||||
|
} from '#/components/dms/util'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import type * as bsky from '#/types/bsky'
|
|
||||||
import {MemberMenu} from './MemberMenu'
|
import {MemberMenu} from './MemberMenu'
|
||||||
import {StatusBadge} from './StatusBadge'
|
import {StatusBadge} from './StatusBadge'
|
||||||
import {SubtleHoverWrapper} from './SubtleHoverWrapper'
|
import {SubtleHoverWrapper} from './SubtleHoverWrapper'
|
||||||
@@ -25,7 +27,7 @@ export function Member({
|
|||||||
isOwner,
|
isOwner,
|
||||||
}: {
|
}: {
|
||||||
convo: ConvoWithDetails
|
convo: ConvoWithDetails
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: GroupConvoMember
|
||||||
status: 'owner' | 'standard' | 'invited'
|
status: 'owner' | 'standard' | 'invited'
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
}) {
|
}) {
|
||||||
@@ -36,12 +38,10 @@ export function Member({
|
|||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const moderation = moderationOpts
|
|
||||||
? moderateProfile(profile, moderationOpts)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// TODO Render a skeleton here. -dsb
|
// TODO Render a skeleton here. -dsb
|
||||||
if (!moderation) return null
|
if (!moderationOpts) return null
|
||||||
|
|
||||||
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
|
|
||||||
const isDeletedAccount = profile.handle === 'missing.invalid'
|
const isDeletedAccount = profile.handle === 'missing.invalid'
|
||||||
const displayName = isDeletedAccount
|
const displayName = isDeletedAccount
|
||||||
@@ -66,6 +66,14 @@ export function Member({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const joinedReason = profile.kind?.addedBy
|
||||||
|
? l`Added by ${createSanitizedDisplayName(
|
||||||
|
profile.kind.addedBy,
|
||||||
|
true,
|
||||||
|
moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'),
|
||||||
|
)}`
|
||||||
|
: `Added by invite link`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubtleHoverWrapper>
|
<SubtleHoverWrapper>
|
||||||
<View style={[a.flex_row, a.align_center, a.justify_between, a.mx_xl]}>
|
<View style={[a.flex_row, a.align_center, a.justify_between, a.mx_xl]}>
|
||||||
@@ -94,6 +102,14 @@ export function Member({
|
|||||||
]}>
|
]}>
|
||||||
{sanitizeHandle(profile.handle, '@')}
|
{sanitizeHandle(profile.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
{color: t.palette.contrast_500},
|
||||||
|
web(a.pt_2xs),
|
||||||
|
]}>
|
||||||
|
{joinedReason}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View>{statusBadge}</View>
|
<View>{statusBadge}</View>
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
type NavigationProp,
|
type NavigationProp,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {ConvoProvider, useConvo} from '#/state/messages/convo'
|
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
|
||||||
import {ConvoStatus} from '#/state/messages/convo/types'
|
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||||
import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name'
|
import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name'
|
||||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||||
|
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
|
||||||
import {useListJoinRequestsQuery} from '#/state/queries/messages/list-join-requests'
|
import {useListJoinRequestsQuery} from '#/state/queries/messages/list-join-requests'
|
||||||
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
@@ -23,7 +24,10 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
|||||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||||
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
import {
|
||||||
|
type ConvoWithDetails,
|
||||||
|
type GroupConvoMember,
|
||||||
|
} from '#/components/dms/util'
|
||||||
import {Error} from '#/components/Error'
|
import {Error} from '#/components/Error'
|
||||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
||||||
import {
|
import {
|
||||||
@@ -37,11 +41,10 @@ import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/Ed
|
|||||||
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'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
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 {IS_NATIVE} from '#/env'
|
|
||||||
import type * as bsky from '#/types/bsky'
|
|
||||||
import {InviteLinkDialog} from '../components/InviteLinkDialog'
|
import {InviteLinkDialog} from '../components/InviteLinkDialog'
|
||||||
import {AddMembersLink} from './AddMembersLink'
|
import {AddMembersLink} from './AddMembersLink'
|
||||||
import {ROW_SPACING} from './constants'
|
import {ROW_SPACING} from './constants'
|
||||||
@@ -56,17 +59,14 @@ const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
type Item =
|
type Item =
|
||||||
| {
|
| {type: 'MEMBERS_AND_REQUESTS'}
|
||||||
type: 'MEMBERS_AND_REQUESTS'
|
| {type: 'ADD_MEMBERS_LINK'}
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'ADD_MEMBERS_LINK'
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'CHAT_MEMBER'
|
type: 'CHAT_MEMBER'
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: GroupConvoMember
|
||||||
status: 'owner' | 'standard' | 'invited'
|
status: 'owner' | 'standard' | 'invited'
|
||||||
}
|
}
|
||||||
|
| {type: 'LOADING'}
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<
|
type Props = NativeStackScreenProps<
|
||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
@@ -90,36 +90,84 @@ export function MessagesConversationSettingsScreen({route}: Props) {
|
|||||||
<Layout.Header.Slot />
|
<Layout.Header.Slot />
|
||||||
</Layout.Header.Outer>
|
</Layout.Header.Outer>
|
||||||
<ConvoProvider key={convoId} convoId={convoId}>
|
<ConvoProvider key={convoId} convoId={convoId}>
|
||||||
<SettingsInner convoId={convoId} />
|
<SettingsInner />
|
||||||
</ConvoProvider>
|
</ConvoProvider>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SettingsInner() {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const convoState = useConvo()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
if (convoState.status === ConvoStatus.Error) {
|
||||||
|
return (
|
||||||
|
<Error
|
||||||
|
title={l`Something went wrong`}
|
||||||
|
message={l`We couldn’t load this conversation’s settings`}
|
||||||
|
onRetry={() => convoState.error.retry()}
|
||||||
|
sideBorders={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConvoActive(convoState)) {
|
||||||
|
return (
|
||||||
|
<Layout.Content
|
||||||
|
contentContainerStyle={[a.flex_1, a.align_center, a.justify_center]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</Layout.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convoState.convo?.kind !== 'group') {
|
||||||
|
return (
|
||||||
|
<Error
|
||||||
|
title={l`Wrong kind of conversation`}
|
||||||
|
message={l`This screen is only available for group conversations.`}
|
||||||
|
onGoBack={() => {
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.replace('Messages', {animation: 'pop'})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <GroupSettings convo={convoState.convo} />
|
||||||
|
}
|
||||||
|
|
||||||
function keyExtractor(item: Item) {
|
function keyExtractor(item: Item) {
|
||||||
return item.type === 'CHAT_MEMBER' ? item.profile.did : item.type
|
return item.type === 'CHAT_MEMBER' ? item.profile.did : item.type
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsInner({convoId}: {convoId: string}) {
|
function GroupSettings({
|
||||||
const {t: l} = useLingui()
|
convo,
|
||||||
|
}: {
|
||||||
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
|
}) {
|
||||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
||||||
const bottomBarOffset = useBottomBarOffset()
|
const bottomBarOffset = useBottomBarOffset()
|
||||||
|
|
||||||
const convoState = useConvo()
|
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const convo = convoState.convo
|
|
||||||
const primaryMember = convo?.primaryMember
|
const primaryMember = convo?.primaryMember
|
||||||
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
|
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
|
||||||
|
|
||||||
const data: bsky.profile.AnyProfileView[] = convo?.members ?? []
|
const {data: memberListData = [], isPending} = useListConvoMembersQuery({
|
||||||
|
convoId: convo.view.id,
|
||||||
|
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
|
||||||
const invites: string[] = []
|
const invites: string[] = []
|
||||||
|
|
||||||
const {data: joinRequestsData, hasNextPage: hasMoreRequests} =
|
const {data: joinRequestsData, hasNextPage: hasMoreRequests} =
|
||||||
useListJoinRequestsQuery({
|
useListJoinRequestsQuery({
|
||||||
convoId,
|
convoId: convo.view.id,
|
||||||
enabled: isOwner,
|
enabled: isOwner,
|
||||||
})
|
})
|
||||||
const requestCount =
|
const requestCount =
|
||||||
@@ -133,7 +181,7 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
type: 'MEMBERS_AND_REQUESTS',
|
type: 'MEMBERS_AND_REQUESTS',
|
||||||
},
|
},
|
||||||
...(isOwner ? [{type: 'ADD_MEMBERS_LINK'} as const] : []),
|
...(isOwner ? [{type: 'ADD_MEMBERS_LINK'} as const] : []),
|
||||||
...[...data]
|
...[...memberListData]
|
||||||
.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
|
||||||
@@ -146,7 +194,7 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
.map(
|
.map(
|
||||||
(profile): Item => ({
|
(profile): Item => ({
|
||||||
type: 'CHAT_MEMBER',
|
type: 'CHAT_MEMBER',
|
||||||
profile,
|
profile: profile as GroupConvoMember,
|
||||||
status:
|
status:
|
||||||
primaryMember?.did === profile.did
|
primaryMember?.did === profile.did
|
||||||
? 'owner'
|
? 'owner'
|
||||||
@@ -155,6 +203,7 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
: 'standard',
|
: 'standard',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
...(isPending ? [{type: 'LOADING' as const}] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
function renderItem({item}: {item: Item}) {
|
function renderItem({item}: {item: Item}) {
|
||||||
@@ -162,7 +211,7 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
case 'MEMBERS_AND_REQUESTS':
|
case 'MEMBERS_AND_REQUESTS':
|
||||||
return (
|
return (
|
||||||
<MembersAndRequests
|
<MembersAndRequests
|
||||||
memberCount={data.length}
|
memberCount={convo.details.memberCount}
|
||||||
requestCount={requestCount}
|
requestCount={requestCount}
|
||||||
hasMoreRequests={!!hasMoreRequests}
|
hasMoreRequests={!!hasMoreRequests}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
@@ -172,7 +221,7 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
return convo ? (
|
return convo ? (
|
||||||
<AddMembersLink
|
<AddMembersLink
|
||||||
convo={convo}
|
convo={convo}
|
||||||
members={data.map(profile => profile.did)}
|
members={memberListData.map(profile => profile.did)}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
case 'CHAT_MEMBER':
|
case 'CHAT_MEMBER':
|
||||||
@@ -184,22 +233,17 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
|
case 'LOADING':
|
||||||
|
return (
|
||||||
|
<View style={[a.w_full, a.align_center, a.py_2xl]}>
|
||||||
|
<Loader size="lg" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convoState.status === ConvoStatus.Error) {
|
|
||||||
return (
|
|
||||||
<Error
|
|
||||||
title={l`Something went wrong`}
|
|
||||||
message={l`We couldn’t load this conversation’s settings`}
|
|
||||||
onRetry={() => convoState.error.retry()}
|
|
||||||
sideBorders={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
data={items}
|
data={items}
|
||||||
@@ -220,9 +264,6 @@ function SettingsInner({convoId}: {convoId: string}) {
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
windowSize={11}
|
windowSize={11}
|
||||||
// TODO Paginate on relatedProfiles. -dsb
|
|
||||||
onEndReached={() => {}}
|
|
||||||
onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function ConvoProvider({
|
|||||||
const [root, id] = getConvoKey(convoId)
|
const [root, id] = getConvoKey(convoId)
|
||||||
return queryClient.getQueryCache().subscribe(event => {
|
return queryClient.getQueryCache().subscribe(event => {
|
||||||
const queryKey = event.query.queryKey as string[]
|
const queryKey = event.query.queryKey as string[]
|
||||||
if (queryKey[0] === root && queryKey[1] === id) {
|
if (queryKey[0] === root && queryKey[1] === id && !queryKey[2]) {
|
||||||
const data = event.query.state.data as
|
const data = event.query.state.data as
|
||||||
| ChatBskyConvoDefs.ConvoView
|
| ChatBskyConvoDefs.ConvoView
|
||||||
| undefined
|
| undefined
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
RQKEY_ROOT as LIST_CONVOS_KEY,
|
RQKEY_ROOT as LIST_CONVOS_KEY,
|
||||||
} from './list-conversations'
|
} from './list-conversations'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'convo'
|
export const RQKEY_ROOT = 'convo'
|
||||||
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
||||||
|
|
||||||
export function useConvoQuery({convoId}: {convoId: string}) {
|
export function useConvoQuery({convoId}: {convoId: string}) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {type ChatBskyActorDefs} from '@atproto/api'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
|
import {STALE} from '#/state/queries'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
import {RQKEY_ROOT as GET_CONVOS_KEY} from './conversation'
|
||||||
|
|
||||||
|
export const RQKEY_SEGMENT = 'members'
|
||||||
|
export const RQKEY = (convoId: string) => [
|
||||||
|
GET_CONVOS_KEY,
|
||||||
|
convoId,
|
||||||
|
RQKEY_SEGMENT,
|
||||||
|
]
|
||||||
|
|
||||||
|
// group chat size is 50, so should fetch the whole list in one go
|
||||||
|
const LIMIT = 50
|
||||||
|
|
||||||
|
export function useListConvoMembersQuery({
|
||||||
|
convoId,
|
||||||
|
placeholderData,
|
||||||
|
}: {
|
||||||
|
convoId: string
|
||||||
|
placeholderData?: ChatBskyActorDefs.ProfileViewBasic[]
|
||||||
|
}) {
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: RQKEY(convoId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const members = []
|
||||||
|
let cursor
|
||||||
|
|
||||||
|
do {
|
||||||
|
const {data} = await agent.chat.bsky.convo.getConvoMembers(
|
||||||
|
{convoId, cursor, limit: LIMIT},
|
||||||
|
{headers: DM_SERVICE_HEADERS},
|
||||||
|
)
|
||||||
|
members.push(...data.members)
|
||||||
|
cursor = data.cursor
|
||||||
|
} while (cursor)
|
||||||
|
|
||||||
|
return members
|
||||||
|
},
|
||||||
|
staleTime: STALE.MINUTES.THIRTY,
|
||||||
|
placeholderData,
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user