Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move useListConvoMembersQuery inside AddMembersFlow (#10386)

authored by

DS Boyce and committed by
GitHub
f1764f1d 736c9625

+87 -44
+83 -33
src/components/dms/AddMembersFlow.tsx
··· 11 11 12 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 13 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 14 + import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members' 14 15 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 15 16 import {useSession} from '#/state/session' 16 17 import {type ListMethods} from '#/view/com/util/List' 17 18 import {android, atoms as a, native, useTheme, web} from '#/alf' 18 19 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19 20 import * as Dialog from '#/components/Dialog' 20 - import {canBeMessaged} from '#/components/dms/util' 21 + import {canBeMessaged, type ConvoWithDetails} from '#/components/dms/util' 21 22 import * as Toggle from '#/components/forms/Toggle' 22 23 import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' 23 24 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 25 + import {Loader} from '#/components/Loader' 24 26 import {Text} from '#/components/Typography' 25 27 import {IS_NATIVE, IS_WEB} from '#/env' 26 28 import type * as bsky from '#/types/bsky' ··· 54 56 key: string 55 57 } 56 58 57 - type ErrorItem = { 58 - type: 'error' 59 + type LoadingItem = { 60 + type: 'loading' 59 61 key: string 60 62 } 61 63 62 - type Item = LabelItem | ProfileItem | EmptyItem | PlaceholderItem | ErrorItem 64 + type Item = LabelItem | ProfileItem | EmptyItem | PlaceholderItem | LoadingItem 63 65 64 66 export type State = { 65 67 groupChatDids: string[] ··· 98 100 } 99 101 100 102 export function AddMembersFlow({ 101 - members, 103 + convo, 102 104 title, 103 105 onAddMembers, 104 106 }: { 105 - members: string[] 107 + convo: Extract<ConvoWithDetails, {kind: 'group'}> 106 108 title: string 107 109 onAddMembers: ( 108 110 dids: string[], ··· 112 114 const t = useTheme() 113 115 const {t: l} = useLingui() 114 116 const moderationOpts = useModerationOpts() 117 + const {currentAccount} = useSession() 118 + 115 119 const control = Dialog.useDialogContext() 120 + 116 121 const [headerHeight, setHeaderHeight] = useState(0) 117 122 const [footerHeight, setFooterHeight] = useState(0) 123 + const [searchText, setSearchText] = useState('') 124 + 118 125 const listRef = useRef<ListMethods>(null) 119 - const {currentAccount} = useSession() 120 126 const inputRef = useRef<TextInput>(null) 121 - 122 - const [searchText, setSearchText] = useState('') 123 127 124 128 const { 125 - data: results, 129 + data: autocompleteResults, 126 130 isError, 127 - isFetching, 131 + isFetching: isAutocompleteFetching, 128 132 } = useActorAutocompleteQuery(searchText, true, 12) 129 133 const {data: follows} = useProfileFollowsQuery(currentAccount?.did) 134 + const {data: memberListData = [], isPending: isMemberListPending} = 135 + useListConvoMembersQuery({ 136 + convoId: convo.view.id, 137 + placeholderData: convo.members, 138 + }) 139 + const memberDidSet = useMemo( 140 + () => new Set(memberListData.map(profile => profile.did)), 141 + [memberListData], 142 + ) 130 143 131 144 const [{groupChatDids, groupChatProfiles}, dispatch] = useReducer(reducer, { 132 145 groupChatDids: [], ··· 147 160 [groupChatDids, groupChatProfiles], 148 161 ) 149 162 150 - const items = useMemo(() => { 151 - let _items: Item[] = [] 163 + const items = useMemo<Item[]>(() => { 164 + if (isMemberListPending) { 165 + // Still fetching chat member DIDs for filtering, so force the loading state. 166 + return [] 167 + } 168 + 169 + const _items: Item[] = [] 152 170 153 171 if (isError) { 154 172 _items.push({ ··· 157 175 message: l`We’re having network issues, try again`, 158 176 }) 159 177 } else if (searchText.length) { 160 - if (results?.length) { 161 - for (const profile of results) { 178 + if (autocompleteResults?.length) { 179 + for (const profile of autocompleteResults) { 162 180 if ( 163 181 profile.did === currentAccount?.did || 164 - members.includes(profile.did) 182 + memberDidSet.has(profile.did) 165 183 ) 166 184 continue 167 185 _items.push({ ··· 171 189 }) 172 190 } 173 191 174 - _items = _items.sort(item => { 192 + _items.sort(item => { 175 193 return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1 176 194 }) 177 195 } 178 196 } else { 179 - const placeholders: Item[] = Array(10) 180 - .fill(0) 181 - .map((__, i) => ({ 182 - type: 'placeholder', 183 - key: i + '', 184 - })) 185 - 186 197 if (follows) { 187 198 for (const page of follows.pages) { 188 199 for (const profile of page.follows) { ··· 194 205 } 195 206 } 196 207 197 - _items = _items.sort(item => { 208 + _items.sort(item => { 198 209 return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1 199 210 }) 200 211 } else { 201 - _items.push(...placeholders) 212 + for (let i = 0; i < 10; i++) { 213 + _items.push({type: 'placeholder', key: i + ''}) 214 + } 202 215 } 203 216 } 204 217 ··· 210 223 }) 211 224 } 212 225 226 + if (searchText && isAutocompleteFetching && _items.length > 0) { 227 + // Stale results are still showing while autocomplete refetches - 228 + // append an inline indicator so the user sees that work is happening. 229 + _items.push({type: 'loading', key: 'loading'}) 230 + } else if ( 231 + searchText && 232 + !isAutocompleteFetching && 233 + !_items.length && 234 + !isError 235 + ) { 236 + _items.push({type: 'empty', key: 'empty', message: l`No results`}) 237 + } 238 + 213 239 return _items 214 - }, [isError, searchText, l, results, currentAccount?.did, members, follows]) 215 - 216 - if (searchText && !isFetching && !items.length && !isError) { 217 - items.push({type: 'empty', key: 'empty', message: l`No results`}) 218 - } 240 + }, [ 241 + autocompleteResults, 242 + currentAccount?.did, 243 + follows, 244 + isAutocompleteFetching, 245 + isError, 246 + isMemberListPending, 247 + l, 248 + memberDidSet, 249 + searchText, 250 + ]) 219 251 220 252 const handlePressBack = useCallback(() => { 221 253 control.close() ··· 242 274 } 243 275 case 'placeholder': { 244 276 return <ProfileCardSkeleton key={item.key} /> 277 + } 278 + case 'loading': { 279 + return ( 280 + <View style={[a.px_lg, a.py_xl, a.align_center]}> 281 + <Loader size="lg" /> 282 + </View> 283 + ) 245 284 } 246 285 case 'empty': { 247 286 return <EmptyMemberList key={item.key} message={item.message} /> ··· 436 475 renderItem={renderItems} 437 476 ListHeaderComponent={listHeader} 438 477 stickyHeaderIndices={[0]} 478 + ListEmptyComponent={ 479 + isMemberListPending || isAutocompleteFetching ? ( 480 + <View style={[a.flex_1, a.align_center, a.justify_center]}> 481 + <Loader size="xl" /> 482 + </View> 483 + ) : null 484 + } 439 485 keyExtractor={(item: Item) => item.key} 440 486 style={[ 441 487 web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), 442 488 native({height: '100%'}), 443 489 ]} 444 - webInnerContentContainerStyle={[a.py_0, {paddingBottom: footerHeight}]} 490 + contentContainerStyle={items.length === 0 ? {flexGrow: 1} : undefined} 491 + webInnerContentContainerStyle={[ 492 + a.py_0, 493 + {paddingBottom: footerHeight}, 494 + items.length === 0 && {flexGrow: 1}, 495 + ]} 445 496 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} 446 497 scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}} 447 498 keyboardDismissMode="on-drag" ··· 457 508 onPress={handlePressBack}> 458 509 <ButtonIcon icon={ArrowLeftIcon} size="md" /> 459 510 <ButtonText> 460 - {' '} 461 511 <Trans>Back</Trans> 462 512 </ButtonText> 463 513 </Button>
+2 -4
src/screens/Messages/ConversationSettings/AddMembersLink.tsx
··· 16 16 17 17 export function AddMembersLink({ 18 18 convo, 19 - members, 20 19 }: { 21 - convo: ConvoWithDetails 22 - members: string[] 20 + convo: Extract<ConvoWithDetails, {kind: 'group'}> 23 21 }) { 24 22 const t = useTheme() 25 23 const {t: l} = useLingui() ··· 96 94 nativeOptions={{fullHeight: true}}> 97 95 <Dialog.Handle /> 98 96 <AddMembersFlow 99 - members={members} 97 + convo={convo} 100 98 title={l`Add members`} 101 99 onAddMembers={(members, profiles) => { 102 100 addGroupMembers({members, profiles})
+1 -6
src/screens/Messages/ConversationSettings/index.tsx
··· 239 239 /> 240 240 ) 241 241 case 'ADD_MEMBERS_LINK': 242 - return convo ? ( 243 - <AddMembersLink 244 - convo={convo} 245 - members={memberListData.map(profile => profile.did)} 246 - /> 247 - ) : null 242 + return convo ? <AddMembersLink convo={convo} /> : null 248 243 case 'CHAT_MEMBER': 249 244 return convo ? ( 250 245 <Member
+1 -1
src/screens/Messages/components/MessagesListInfoPanel.tsx
··· 150 150 nativeOptions={{fullHeight: true}}> 151 151 <Dialog.Handle /> 152 152 <AddMembersFlow 153 - members={members.map(profile => profile.did)} 153 + convo={convo} 154 154 title={l`Add people`} 155 155 onAddMembers={(members, profiles) => 156 156 addGroupMembers({members, profiles})