diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx index 8d7cee84fc..8274d3cc35 100644 --- a/src/screens/Messages/ConversationSettings/Member.tsx +++ b/src/screens/Messages/ConversationSettings/Member.tsx @@ -11,9 +11,11 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 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 type * as bsky from '#/types/bsky' import {MemberMenu} from './MemberMenu' import {StatusBadge} from './StatusBadge' import {SubtleHoverWrapper} from './SubtleHoverWrapper' @@ -25,7 +27,7 @@ export function Member({ isOwner, }: { convo: ConvoWithDetails - profile: bsky.profile.AnyProfileView + profile: GroupConvoMember status: 'owner' | 'standard' | 'invited' isOwner: boolean }) { @@ -36,12 +38,10 @@ export function Member({ const profile = useProfileShadow(profileUnshadowed) const {currentAccount} = useSession() const moderationOpts = useModerationOpts() - const moderation = moderationOpts - ? moderateProfile(profile, moderationOpts) - : undefined - // 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 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 ( @@ -94,6 +102,14 @@ export function Member({ ]}> {sanitizeHandle(profile.handle, '@')} + + {joinedReason} + {statusBadge} diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index b664d7e1e3..0fe677571d 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -11,10 +11,11 @@ import { type NavigationProp, } from '#/lib/routes/types' 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 {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name' 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 {useMuteConvo} from '#/state/queries/messages/mute-conversation' import {useSession} from '#/state/session' @@ -23,7 +24,10 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' 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 {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft' 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 {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' import * as Layout from '#/components/Layout' +import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' -import {IS_NATIVE} from '#/env' -import type * as bsky from '#/types/bsky' import {InviteLinkDialog} from '../components/InviteLinkDialog' import {AddMembersLink} from './AddMembersLink' import {ROW_SPACING} from './constants' @@ -56,17 +59,14 @@ const dateFormatter = new Intl.DateTimeFormat(undefined, { }) type Item = - | { - type: 'MEMBERS_AND_REQUESTS' - } - | { - type: 'ADD_MEMBERS_LINK' - } + | {type: 'MEMBERS_AND_REQUESTS'} + | {type: 'ADD_MEMBERS_LINK'} | { type: 'CHAT_MEMBER' - profile: bsky.profile.AnyProfileView + profile: GroupConvoMember status: 'owner' | 'standard' | 'invited' } + | {type: 'LOADING'} type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -90,36 +90,84 @@ export function MessagesConversationSettingsScreen({route}: Props) { - + ) } +function SettingsInner() { + const {t: l} = useLingui() + const convoState = useConvo() + const navigation = useNavigation() + + if (convoState.status === ConvoStatus.Error) { + return ( + convoState.error.retry()} + sideBorders={false} + /> + ) + } + + if (!isConvoActive(convoState)) { + return ( + + + + ) + } + + if (convoState.convo?.kind !== 'group') { + return ( + { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.replace('Messages', {animation: 'pop'}) + } + }} + /> + ) + } + + return +} + function keyExtractor(item: Item) { return item.type === 'CHAT_MEMBER' ? item.profile.did : item.type } -function SettingsInner({convoId}: {convoId: string}) { - const {t: l} = useLingui() - +function GroupSettings({ + convo, +}: { + convo: Extract +}) { const initialNumToRender = useInitialNumToRender({minItemHeight: 68}) const bottomBarOffset = useBottomBarOffset() - const convoState = useConvo() const {currentAccount} = useSession() - const convo = convoState.convo const primaryMember = convo?.primaryMember 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 const invites: string[] = [] const {data: joinRequestsData, hasNextPage: hasMoreRequests} = useListJoinRequestsQuery({ - convoId, + convoId: convo.view.id, enabled: isOwner, }) const requestCount = @@ -133,7 +181,7 @@ function SettingsInner({convoId}: {convoId: string}) { type: 'MEMBERS_AND_REQUESTS', }, ...(isOwner ? [{type: 'ADD_MEMBERS_LINK'} as const] : []), - ...[...data] + ...[...memberListData] .sort((a, b) => { const aIsOwner = a.did === primaryMember?.did const bIsOwner = b.did === primaryMember?.did @@ -146,7 +194,7 @@ function SettingsInner({convoId}: {convoId: string}) { .map( (profile): Item => ({ type: 'CHAT_MEMBER', - profile, + profile: profile as GroupConvoMember, status: primaryMember?.did === profile.did ? 'owner' @@ -155,6 +203,7 @@ function SettingsInner({convoId}: {convoId: string}) { : 'standard', }), ), + ...(isPending ? [{type: 'LOADING' as const}] : []), ] function renderItem({item}: {item: Item}) { @@ -162,7 +211,7 @@ function SettingsInner({convoId}: {convoId: string}) { case 'MEMBERS_AND_REQUESTS': return ( profile.did)} + members={memberListData.map(profile => profile.did)} /> ) : null case 'CHAT_MEMBER': @@ -184,22 +233,17 @@ function SettingsInner({convoId}: {convoId: string}) { isOwner={isOwner} /> ) : null + case 'LOADING': + return ( + + + + ) default: return null } } - if (convoState.status === ConvoStatus.Error) { - return ( - convoState.error.retry()} - sideBorders={false} - /> - ) - } - return ( {}} - onEndReachedThreshold={IS_NATIVE ? 1.5 : 0} /> ) } diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index ca2bbdda0f..0d47159882 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -133,7 +133,7 @@ export function ConvoProvider({ const [root, id] = getConvoKey(convoId) return queryClient.getQueryCache().subscribe(event => { 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 | ChatBskyConvoDefs.ConvoView | undefined diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts index b8f26cc88c..76557991d1 100644 --- a/src/state/queries/messages/conversation.ts +++ b/src/state/queries/messages/conversation.ts @@ -16,7 +16,7 @@ import { RQKEY_ROOT as LIST_CONVOS_KEY, } from './list-conversations' -const RQKEY_ROOT = 'convo' +export const RQKEY_ROOT = 'convo' export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] export function useConvoQuery({convoId}: {convoId: string}) { diff --git a/src/state/queries/messages/list-convo-members.ts b/src/state/queries/messages/list-convo-members.ts new file mode 100644 index 0000000000..b353107927 --- /dev/null +++ b/src/state/queries/messages/list-convo-members.ts @@ -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, + }) +}