diff --git a/src/components/dms/AddMembersFlow.tsx b/src/components/dms/AddMembersFlow.tsx index bc3313b07c..68f48bcd45 100644 --- a/src/components/dms/AddMembersFlow.tsx +++ b/src/components/dms/AddMembersFlow.tsx @@ -11,16 +11,18 @@ import {Trans, useLingui} from '@lingui/react/macro' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' +import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members' import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useSession} from '#/state/session' import {type ListMethods} from '#/view/com/util/List' import {android, atoms as a, native, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' 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 {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' +import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_NATIVE, IS_WEB} from '#/env' import type * as bsky from '#/types/bsky' @@ -54,12 +56,12 @@ type PlaceholderItem = { key: string } -type ErrorItem = { - type: 'error' +type LoadingItem = { + type: 'loading' key: string } -type Item = LabelItem | ProfileItem | EmptyItem | PlaceholderItem | ErrorItem +type Item = LabelItem | ProfileItem | EmptyItem | PlaceholderItem | LoadingItem export type State = { groupChatDids: string[] @@ -98,11 +100,11 @@ function reducer(state: State, action: Action): State { } export function AddMembersFlow({ - members, + convo, title, onAddMembers, }: { - members: string[] + convo: Extract title: string onAddMembers: ( dids: string[], @@ -112,21 +114,32 @@ export function AddMembersFlow({ const t = useTheme() const {t: l} = useLingui() const moderationOpts = useModerationOpts() + const {currentAccount} = useSession() + const control = Dialog.useDialogContext() + const [headerHeight, setHeaderHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0) - const listRef = useRef(null) - const {currentAccount} = useSession() - const inputRef = useRef(null) - const [searchText, setSearchText] = useState('') + const listRef = useRef(null) + const inputRef = useRef(null) + const { - data: results, + data: autocompleteResults, isError, - isFetching, + isFetching: isAutocompleteFetching, } = useActorAutocompleteQuery(searchText, true, 12) 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, { groupChatDids: [], @@ -147,8 +160,13 @@ export function AddMembersFlow({ [groupChatDids, groupChatProfiles], ) - const items = useMemo(() => { - let _items: Item[] = [] + const items = useMemo(() => { + if (isMemberListPending) { + // Still fetching chat member DIDs for filtering, so force the loading state. + return [] + } + + const _items: Item[] = [] if (isError) { _items.push({ @@ -157,11 +175,11 @@ export function AddMembersFlow({ message: l`We’re having network issues, try again`, }) } else if (searchText.length) { - if (results?.length) { - for (const profile of results) { + if (autocompleteResults?.length) { + for (const profile of autocompleteResults) { if ( profile.did === currentAccount?.did || - members.includes(profile.did) + memberDidSet.has(profile.did) ) continue _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 }) } } else { - const placeholders: Item[] = Array(10) - .fill(0) - .map((__, i) => ({ - type: 'placeholder', - key: i + '', - })) - if (follows) { for (const page of follows.pages) { 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 }) } else { - _items.push(...placeholders) + for (let i = 0; i < 10; i++) { + _items.push({type: 'placeholder', key: i + ''}) + } } } @@ -210,12 +223,31 @@ export function AddMembersFlow({ }) } - return _items - }, [isError, searchText, l, results, currentAccount?.did, members, follows]) + if (searchText && isAutocompleteFetching && _items.length > 0) { + // Stale results are still showing while autocomplete refetches - + // append an inline indicator so the user sees that work is happening. + _items.push({type: 'loading', key: 'loading'}) + } else if ( + searchText && + !isAutocompleteFetching && + !_items.length && + !isError + ) { + _items.push({type: 'empty', key: 'empty', message: l`No results`}) + } - if (searchText && !isFetching && !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(() => { control.close() @@ -243,6 +275,13 @@ export function AddMembersFlow({ case 'placeholder': { return } + case 'loading': { + return ( + + + + ) + } case 'empty': { return } @@ -436,12 +475,24 @@ export function AddMembersFlow({ renderItem={renderItems} ListHeaderComponent={listHeader} stickyHeaderIndices={[0]} + ListEmptyComponent={ + isMemberListPending || isAutocompleteFetching ? ( + + + + ) : null + } keyExtractor={(item: Item) => item.key} style={[ web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), 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}]} scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}} keyboardDismissMode="on-drag" @@ -457,7 +508,6 @@ export function AddMembersFlow({ onPress={handlePressBack}> - {' '} Back diff --git a/src/screens/Messages/ConversationSettings/AddMembersLink.tsx b/src/screens/Messages/ConversationSettings/AddMembersLink.tsx index 67c32f6a77..58674d830a 100644 --- a/src/screens/Messages/ConversationSettings/AddMembersLink.tsx +++ b/src/screens/Messages/ConversationSettings/AddMembersLink.tsx @@ -16,10 +16,8 @@ import {Text} from '#/components/Typography' export function AddMembersLink({ convo, - members, }: { - convo: ConvoWithDetails - members: string[] + convo: Extract }) { const t = useTheme() const {t: l} = useLingui() @@ -96,7 +94,7 @@ export function AddMembersLink({ nativeOptions={{fullHeight: true}}> { addGroupMembers({members, profiles}) diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 6e88146f77..d770b4a5cb 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -239,12 +239,7 @@ function GroupSettings({ /> ) case 'ADD_MEMBERS_LINK': - return convo ? ( - profile.did)} - /> - ) : null + return convo ? : null case 'CHAT_MEMBER': return convo ? ( profile.did)} + convo={convo} title={l`Add people`} onAddMembers={(members, profiles) => addGroupMembers({members, profiles})