Move useListConvoMembersQuery inside AddMembersFlow (#10386)

This commit is contained in:
DS Boyce
2026-04-29 16:49:47 +01:00
committed by GitHub
parent 736c9625ed
commit f1764f1d61
4 changed files with 88 additions and 45 deletions
+84 -34
View File
@@ -11,16 +11,18 @@ import {Trans, useLingui} from '@lingui/react/macro'
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 {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
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 {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 {canBeMessaged} from '#/components/dms/util' import {canBeMessaged, type ConvoWithDetails} from '#/components/dms/util'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
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'
@@ -54,12 +56,12 @@ type PlaceholderItem = {
key: string key: string
} }
type ErrorItem = { type LoadingItem = {
type: 'error' type: 'loading'
key: string key: string
} }
type Item = LabelItem | ProfileItem | EmptyItem | PlaceholderItem | ErrorItem type Item = LabelItem | ProfileItem | EmptyItem | PlaceholderItem | LoadingItem
export type State = { export type State = {
groupChatDids: string[] groupChatDids: string[]
@@ -98,11 +100,11 @@ function reducer(state: State, action: Action): State {
} }
export function AddMembersFlow({ export function AddMembersFlow({
members, convo,
title, title,
onAddMembers, onAddMembers,
}: { }: {
members: string[] convo: Extract<ConvoWithDetails, {kind: 'group'}>
title: string title: string
onAddMembers: ( onAddMembers: (
dids: string[], dids: string[],
@@ -112,21 +114,32 @@ export function AddMembersFlow({
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {currentAccount} = useSession()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const [headerHeight, setHeaderHeight] = useState(0) const [headerHeight, setHeaderHeight] = useState(0)
const [footerHeight, setFooterHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0)
const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInput>(null)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null)
const { const {
data: results, data: autocompleteResults,
isError, isError,
isFetching, isFetching: isAutocompleteFetching,
} = useActorAutocompleteQuery(searchText, true, 12) } = useActorAutocompleteQuery(searchText, true, 12)
const {data: follows} = useProfileFollowsQuery(currentAccount?.did) const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
const {data: memberListData = [], isPending: isMemberListPending} =
useListConvoMembersQuery({
convoId: convo.view.id,
placeholderData: convo.members,
})
const memberDidSet = useMemo(
() => new Set(memberListData.map(profile => profile.did)),
[memberListData],
)
const [{groupChatDids, groupChatProfiles}, dispatch] = useReducer(reducer, { const [{groupChatDids, groupChatProfiles}, dispatch] = useReducer(reducer, {
groupChatDids: [], groupChatDids: [],
@@ -147,8 +160,13 @@ export function AddMembersFlow({
[groupChatDids, groupChatProfiles], [groupChatDids, groupChatProfiles],
) )
const items = useMemo(() => { const items = useMemo<Item[]>(() => {
let _items: Item[] = [] if (isMemberListPending) {
// Still fetching chat member DIDs for filtering, so force the loading state.
return []
}
const _items: Item[] = []
if (isError) { if (isError) {
_items.push({ _items.push({
@@ -157,11 +175,11 @@ export function AddMembersFlow({
message: l`We’re having network issues, try again`, message: l`We’re having network issues, try again`,
}) })
} else if (searchText.length) { } else if (searchText.length) {
if (results?.length) { if (autocompleteResults?.length) {
for (const profile of results) { for (const profile of autocompleteResults) {
if ( if (
profile.did === currentAccount?.did || profile.did === currentAccount?.did ||
members.includes(profile.did) memberDidSet.has(profile.did)
) )
continue continue
_items.push({ _items.push({
@@ -171,18 +189,11 @@ export function AddMembersFlow({
}) })
} }
_items = _items.sort(item => { _items.sort(item => {
return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1 return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
}) })
} }
} else { } else {
const placeholders: Item[] = Array(10)
.fill(0)
.map((__, i) => ({
type: 'placeholder',
key: i + '',
}))
if (follows) { 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) {
@@ -194,11 +205,13 @@ export function AddMembersFlow({
} }
} }
_items = _items.sort(item => { _items.sort(item => {
return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1 return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
}) })
} else { } else {
_items.push(...placeholders) for (let i = 0; i < 10; i++) {
_items.push({type: 'placeholder', key: i + ''})
}
} }
} }
@@ -210,13 +223,32 @@ export function AddMembersFlow({
}) })
} }
return _items if (searchText && isAutocompleteFetching && _items.length > 0) {
}, [isError, searchText, l, results, currentAccount?.did, members, follows]) // Stale results are still showing while autocomplete refetches -
// append an inline indicator so the user sees that work is happening.
if (searchText && !isFetching && !items.length && !isError) { _items.push({type: 'loading', key: 'loading'})
items.push({type: 'empty', key: 'empty', message: l`No results`}) } else if (
searchText &&
!isAutocompleteFetching &&
!_items.length &&
!isError
) {
_items.push({type: 'empty', key: 'empty', message: l`No results`})
} }
return _items
}, [
autocompleteResults,
currentAccount?.did,
follows,
isAutocompleteFetching,
isError,
isMemberListPending,
l,
memberDidSet,
searchText,
])
const handlePressBack = useCallback(() => { const handlePressBack = useCallback(() => {
control.close() control.close()
}, [control]) }, [control])
@@ -243,6 +275,13 @@ export function AddMembersFlow({
case 'placeholder': { case 'placeholder': {
return <ProfileCardSkeleton key={item.key} /> return <ProfileCardSkeleton key={item.key} />
} }
case 'loading': {
return (
<View style={[a.px_lg, a.py_xl, a.align_center]}>
<Loader size="lg" />
</View>
)
}
case 'empty': { case 'empty': {
return <EmptyMemberList key={item.key} message={item.message} /> return <EmptyMemberList key={item.key} message={item.message} />
} }
@@ -436,12 +475,24 @@ export function AddMembersFlow({
renderItem={renderItems} renderItem={renderItems}
ListHeaderComponent={listHeader} ListHeaderComponent={listHeader}
stickyHeaderIndices={[0]} stickyHeaderIndices={[0]}
ListEmptyComponent={
isMemberListPending || isAutocompleteFetching ? (
<View style={[a.flex_1, a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : null
}
keyExtractor={(item: Item) => item.key} keyExtractor={(item: Item) => item.key}
style={[ style={[
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({height: '100%'}), native({height: '100%'}),
]} ]}
webInnerContentContainerStyle={[a.py_0, {paddingBottom: footerHeight}]} contentContainerStyle={items.length === 0 ? {flexGrow: 1} : undefined}
webInnerContentContainerStyle={[
a.py_0,
{paddingBottom: footerHeight},
items.length === 0 && {flexGrow: 1},
]}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}} scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}}
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
@@ -457,7 +508,6 @@ export function AddMembersFlow({
onPress={handlePressBack}> onPress={handlePressBack}>
<ButtonIcon icon={ArrowLeftIcon} size="md" /> <ButtonIcon icon={ArrowLeftIcon} size="md" />
<ButtonText> <ButtonText>
{' '}
<Trans>Back</Trans> <Trans>Back</Trans>
</ButtonText> </ButtonText>
</Button> </Button>
@@ -16,10 +16,8 @@ import {Text} from '#/components/Typography'
export function AddMembersLink({ export function AddMembersLink({
convo, convo,
members,
}: { }: {
convo: ConvoWithDetails convo: Extract<ConvoWithDetails, {kind: 'group'}>
members: string[]
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -96,7 +94,7 @@ export function AddMembersLink({
nativeOptions={{fullHeight: true}}> nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<AddMembersFlow <AddMembersFlow
members={members} convo={convo}
title={l`Add members`} title={l`Add members`}
onAddMembers={(members, profiles) => { onAddMembers={(members, profiles) => {
addGroupMembers({members, profiles}) addGroupMembers({members, profiles})
@@ -239,12 +239,7 @@ function GroupSettings({
/> />
) )
case 'ADD_MEMBERS_LINK': case 'ADD_MEMBERS_LINK':
return convo ? ( return convo ? <AddMembersLink convo={convo} /> : null
<AddMembersLink
convo={convo}
members={memberListData.map(profile => profile.did)}
/>
) : null
case 'CHAT_MEMBER': case 'CHAT_MEMBER':
return convo ? ( return convo ? (
<Member <Member
@@ -150,7 +150,7 @@ export function MessagesListInfoPanel({
nativeOptions={{fullHeight: true}}> nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<AddMembersFlow <AddMembersFlow
members={members.map(profile => profile.did)} convo={convo}
title={l`Add people`} title={l`Add people`}
onAddMembers={(members, profiles) => onAddMembers={(members, profiles) =>
addGroupMembers({members, profiles}) addGroupMembers({members, profiles})