An ATproto social media client -- with an independent Appview.
6
fork

Configure Feed

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

Improve typeahead search with inclusion of followed users (temporary solution) (#1612)

* Update follows cache to maintain some user info

* Prioritize follows in composer autocomplete

* Clean up logic and add new autocomplete to search

* Update follow hook

authored by

Paul Frazee and committed by
GitHub
bd7db8af 19f8389f

+232 -177
-46
src/lib/hooks/useFollowDid.ts
··· 1 - import React from 'react' 2 - 3 - import {useStores} from 'state/index' 4 - import {FollowState} from 'state/models/cache/my-follows' 5 - 6 - export function useFollowDid({did}: {did: string}) { 7 - const store = useStores() 8 - const state = store.me.follows.getFollowState(did) 9 - 10 - return { 11 - state, 12 - following: state === FollowState.Following, 13 - toggle: React.useCallback(async () => { 14 - if (state === FollowState.Following) { 15 - try { 16 - await store.agent.deleteFollow(store.me.follows.getFollowUri(did)) 17 - store.me.follows.removeFollow(did) 18 - return { 19 - state: FollowState.NotFollowing, 20 - following: false, 21 - } 22 - } catch (e: any) { 23 - store.log.error('Failed to delete follow', e) 24 - throw e 25 - } 26 - } else if (state === FollowState.NotFollowing) { 27 - try { 28 - const res = await store.agent.follow(did) 29 - store.me.follows.addFollow(did, res.uri) 30 - return { 31 - state: FollowState.Following, 32 - following: true, 33 - } 34 - } catch (e: any) { 35 - store.log.error('Failed to create follow', e) 36 - throw e 37 - } 38 - } 39 - 40 - return { 41 - state: FollowState.Unknown, 42 - following: false, 43 - } 44 - }, [store, did, state]), 45 - } 46 - }
+54
src/lib/hooks/useFollowProfile.ts
··· 1 + import React from 'react' 2 + import {AppBskyActorDefs} from '@atproto/api' 3 + import {useStores} from 'state/index' 4 + import {FollowState} from 'state/models/cache/my-follows' 5 + 6 + export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) { 7 + const store = useStores() 8 + const state = store.me.follows.getFollowState(profile.did) 9 + 10 + return { 11 + state, 12 + following: state === FollowState.Following, 13 + toggle: React.useCallback(async () => { 14 + if (state === FollowState.Following) { 15 + try { 16 + await store.agent.deleteFollow( 17 + store.me.follows.getFollowUri(profile.did), 18 + ) 19 + store.me.follows.removeFollow(profile.did) 20 + return { 21 + state: FollowState.NotFollowing, 22 + following: false, 23 + } 24 + } catch (e: any) { 25 + store.log.error('Failed to delete follow', e) 26 + throw e 27 + } 28 + } else if (state === FollowState.NotFollowing) { 29 + try { 30 + const res = await store.agent.follow(profile.did) 31 + store.me.follows.addFollow(profile.did, { 32 + followRecordUri: res.uri, 33 + did: profile.did, 34 + handle: profile.handle, 35 + displayName: profile.displayName, 36 + avatar: profile.avatar, 37 + }) 38 + return { 39 + state: FollowState.Following, 40 + following: true, 41 + } 42 + } catch (e: any) { 43 + store.log.error('Failed to create follow', e) 44 + throw e 45 + } 46 + } 47 + 48 + return { 49 + state: FollowState.Unknown, 50 + following: false, 51 + } 52 + }, [store, profile, state]), 53 + } 54 + }
+67 -32
src/state/models/cache/my-follows.ts
··· 1 1 import {makeAutoObservable} from 'mobx' 2 - import {AppBskyActorDefs} from '@atproto/api' 2 + import { 3 + AppBskyActorDefs, 4 + AppBskyGraphGetFollows as GetFollows, 5 + moderateProfile, 6 + } from '@atproto/api' 3 7 import {RootStoreModel} from '../root-store' 4 8 9 + const MAX_SYNC_PAGES = 10 10 + const SYNC_TTL = 60e3 * 10 // 10 minutes 11 + 5 12 type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView 6 13 7 14 export enum FollowState { ··· 10 17 Unknown, 11 18 } 12 19 20 + export interface FollowInfo { 21 + did: string 22 + followRecordUri: string | undefined 23 + handle: string 24 + displayName: string | undefined 25 + avatar: string | undefined 26 + } 27 + 13 28 /** 14 29 * This model is used to maintain a synced local cache of the user's 15 30 * follows. It should be periodically refreshed and updated any time ··· 17 32 */ 18 33 export class MyFollowsCache { 19 34 // data 20 - followDidToRecordMap: Record<string, string | boolean> = {} 35 + byDid: Record<string, FollowInfo> = {} 21 36 lastSync = 0 22 - myDid?: string 23 37 24 38 constructor(public rootStore: RootStoreModel) { 25 39 makeAutoObservable( ··· 35 49 // = 36 50 37 51 clear() { 38 - this.followDidToRecordMap = {} 39 - this.lastSync = 0 40 - this.myDid = undefined 52 + this.byDid = {} 53 + } 54 + 55 + /** 56 + * Syncs a subset of the user's follows 57 + * for performance reasons, caps out at 1000 follows 58 + */ 59 + async syncIfNeeded() { 60 + if (this.lastSync > Date.now() - SYNC_TTL) { 61 + return 62 + } 63 + 64 + let cursor 65 + for (let i = 0; i < MAX_SYNC_PAGES; i++) { 66 + const res: GetFollows.Response = await this.rootStore.agent.getFollows({ 67 + actor: this.rootStore.me.did, 68 + cursor, 69 + limit: 100, 70 + }) 71 + res.data.follows = res.data.follows.filter( 72 + profile => 73 + !moderateProfile(profile, this.rootStore.preferences.moderationOpts) 74 + .account.filter, 75 + ) 76 + this.hydrateMany(res.data.follows) 77 + if (!res.data.cursor) { 78 + break 79 + } 80 + cursor = res.data.cursor 81 + } 82 + 83 + this.lastSync = Date.now() 41 84 } 42 85 43 86 getFollowState(did: string): FollowState { 44 - if (typeof this.followDidToRecordMap[did] === 'undefined') { 87 + if (typeof this.byDid[did] === 'undefined') { 45 88 return FollowState.Unknown 46 89 } 47 - if (typeof this.followDidToRecordMap[did] === 'string') { 90 + if (typeof this.byDid[did].followRecordUri === 'string') { 48 91 return FollowState.Following 49 92 } 50 93 return FollowState.NotFollowing ··· 53 96 async fetchFollowState(did: string): Promise<FollowState> { 54 97 // TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf 55 98 const res = await this.rootStore.agent.getProfile({actor: did}) 56 - if (res.data.viewer?.following) { 57 - this.addFollow(did, res.data.viewer.following) 58 - } else { 59 - this.removeFollow(did) 60 - } 99 + this.hydrate(did, res.data) 61 100 return this.getFollowState(did) 62 101 } 63 102 64 103 getFollowUri(did: string): string { 65 - const v = this.followDidToRecordMap[did] 104 + const v = this.byDid[did] 66 105 if (typeof v === 'string') { 67 106 return v 68 107 } 69 108 throw new Error('Not a followed user') 70 109 } 71 110 72 - addFollow(did: string, recordUri: string) { 73 - this.followDidToRecordMap[did] = recordUri 111 + addFollow(did: string, info: FollowInfo) { 112 + this.byDid[did] = info 74 113 } 75 114 76 115 removeFollow(did: string) { 77 - this.followDidToRecordMap[did] = false 116 + if (this.byDid[did]) { 117 + this.byDid[did].followRecordUri = undefined 118 + } 78 119 } 79 120 80 - /** 81 - * Use this to incrementally update the cache as views provide information 82 - */ 83 - hydrate(did: string, recordUri: string | undefined) { 84 - if (recordUri) { 85 - this.followDidToRecordMap[did] = recordUri 86 - } else { 87 - this.followDidToRecordMap[did] = false 121 + hydrate(did: string, profile: Profile) { 122 + this.byDid[did] = { 123 + did, 124 + followRecordUri: profile.viewer?.following, 125 + handle: profile.handle, 126 + displayName: profile.displayName, 127 + avatar: profile.avatar, 88 128 } 89 129 } 90 130 91 - /** 92 - * Use this to incrementally update the cache as views provide information 93 - */ 94 - hydrateProfiles(profiles: Profile[]) { 131 + hydrateMany(profiles: Profile[]) { 95 132 for (const profile of profiles) { 96 - if (profile.viewer) { 97 - this.hydrate(profile.did, profile.viewer.following) 98 - } 133 + this.hydrate(profile.did, profile) 99 134 } 100 135 } 101 136 }
+2 -2
src/state/models/content/profile.ts
··· 137 137 runInAction(() => { 138 138 this.followersCount++ 139 139 this.viewer.following = res.uri 140 - this.rootStore.me.follows.addFollow(this.did, res.uri) 140 + this.rootStore.me.follows.hydrate(this.did, this) 141 141 }) 142 142 track('Profile:Follow', { 143 143 username: this.handle, ··· 290 290 this.labels = res.data.labels 291 291 if (res.data.viewer) { 292 292 Object.assign(this.viewer, res.data.viewer) 293 - this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following) 294 293 } 294 + this.rootStore.me.follows.hydrate(this.did, res.data) 295 295 } 296 296 297 297 async _createRichText() {
+5 -31
src/state/models/discovery/foafs.ts
··· 1 - import { 2 - AppBskyActorDefs, 3 - AppBskyGraphGetFollows as GetFollows, 4 - moderateProfile, 5 - } from '@atproto/api' 1 + import {AppBskyActorDefs} from '@atproto/api' 6 2 import {makeAutoObservable, runInAction} from 'mobx' 7 3 import sampleSize from 'lodash.samplesize' 8 4 import {bundleAsync} from 'lib/async/bundle' ··· 43 39 try { 44 40 this.isLoading = true 45 41 46 - // fetch & hydrate up to 1000 follows 47 - { 48 - let cursor 49 - for (let i = 0; i < 10; i++) { 50 - const res: GetFollows.Response = 51 - await this.rootStore.agent.getFollows({ 52 - actor: this.rootStore.me.did, 53 - cursor, 54 - limit: 100, 55 - }) 56 - res.data.follows = res.data.follows.filter( 57 - profile => 58 - !moderateProfile( 59 - profile, 60 - this.rootStore.preferences.moderationOpts, 61 - ).account.filter, 62 - ) 63 - this.rootStore.me.follows.hydrateProfiles(res.data.follows) 64 - if (!res.data.cursor) { 65 - break 66 - } 67 - cursor = res.data.cursor 68 - } 69 - } 42 + // fetch some of the user's follows 43 + await this.rootStore.me.follows.syncIfNeeded() 70 44 71 45 // grab 10 of the users followed by the user 72 46 runInAction(() => { 73 47 this.sources = sampleSize( 74 - Object.keys(this.rootStore.me.follows.followDidToRecordMap), 48 + Object.keys(this.rootStore.me.follows.byDid), 75 49 10, 76 50 ) 77 51 }) ··· 100 74 for (let i = 0; i < results.length; i++) { 101 75 const res = results[i] 102 76 if (res.status === 'fulfilled') { 103 - this.rootStore.me.follows.hydrateProfiles(res.value.data.follows) 77 + this.rootStore.me.follows.hydrateMany(res.value.data.follows) 104 78 } 105 79 const profile = profiles.data.profiles[i] 106 80 const source = this.sources[i]
+2 -2
src/state/models/discovery/suggested-actors.ts
··· 76 76 !moderateProfile(actor, this.rootStore.preferences.moderationOpts) 77 77 .account.filter, 78 78 ) 79 - this.rootStore.me.follows.hydrateProfiles(actors) 79 + this.rootStore.me.follows.hydrateMany(actors) 80 80 81 81 runInAction(() => { 82 82 if (replace) { ··· 118 118 actor: actor, 119 119 }) 120 120 const {suggestions: moreSuggestions} = res.data 121 - this.rootStore.me.follows.hydrateProfiles(moreSuggestions) 121 + this.rootStore.me.follows.hydrateMany(moreSuggestions) 122 122 // dedupe 123 123 const toInsert = moreSuggestions.filter( 124 124 s => !this.suggestions.find(s2 => s2.did === s.did),
+76 -39
src/state/models/discovery/user-autocomplete.ts
··· 4 4 import {RootStoreModel} from '../root-store' 5 5 import {isInvalidHandle} from 'lib/strings/handles' 6 6 7 + type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic 8 + 7 9 export class UserAutocompleteModel { 8 10 // state 9 11 isLoading = false ··· 12 14 lock = new AwaitLock() 13 15 14 16 // data 15 - follows: AppBskyActorDefs.ProfileViewBasic[] = [] 16 - searchRes: AppBskyActorDefs.ProfileViewBasic[] = [] 17 17 knownHandles: Set<string> = new Set() 18 + _suggestions: ProfileViewBasic[] = [] 18 19 19 20 constructor(public rootStore: RootStoreModel) { 20 21 makeAutoObservable( ··· 27 28 ) 28 29 } 29 30 30 - get suggestions() { 31 + get follows(): ProfileViewBasic[] { 32 + return Object.values(this.rootStore.me.follows.byDid).map(item => ({ 33 + did: item.did, 34 + handle: item.handle, 35 + displayName: item.displayName, 36 + avatar: item.avatar, 37 + })) 38 + } 39 + 40 + get suggestions(): ProfileViewBasic[] { 31 41 if (!this.isActive) { 32 42 return [] 33 43 } 34 - if (this.prefix) { 35 - return this.searchRes.map(user => ({ 36 - handle: user.handle, 37 - displayName: user.displayName, 38 - avatar: user.avatar, 39 - })) 40 - } 41 - return this.follows.map(follow => ({ 42 - handle: follow.handle, 43 - displayName: follow.displayName, 44 - avatar: follow.avatar, 45 - })) 44 + return this._suggestions 46 45 } 47 46 48 47 // public api 49 48 // = 50 49 51 50 async setup() { 52 - await this._getFollows() 51 + await this.rootStore.me.follows.syncIfNeeded() 52 + runInAction(() => { 53 + for (const did in this.rootStore.me.follows.byDid) { 54 + const info = this.rootStore.me.follows.byDid[did] 55 + if (!isInvalidHandle(info.handle)) { 56 + this.knownHandles.add(info.handle) 57 + } 58 + } 59 + }) 53 60 } 54 61 55 62 setActive(v: boolean) { ··· 57 64 } 58 65 59 66 async setPrefix(prefix: string) { 60 - const origPrefix = prefix.trim() 67 + const origPrefix = prefix.trim().toLocaleLowerCase() 61 68 this.prefix = origPrefix 62 69 await this.lock.acquireAsync() 63 70 try { ··· 65 72 if (this.prefix !== origPrefix) { 66 73 return // another prefix was set before we got our chance 67 74 } 68 - await this._search() 75 + 76 + // reset to follow results 77 + this._computeSuggestions([]) 78 + 79 + // ask backend 80 + const res = await this.rootStore.agent.searchActorsTypeahead({ 81 + term: this.prefix, 82 + limit: 8, 83 + }) 84 + this._computeSuggestions(res.data.actors) 85 + 86 + // update known handles 87 + runInAction(() => { 88 + for (const u of res.data.actors) { 89 + this.knownHandles.add(u.handle) 90 + } 91 + }) 69 92 } else { 70 - this.searchRes = [] 93 + runInAction(() => { 94 + this._computeSuggestions([]) 95 + }) 71 96 } 72 97 } finally { 73 98 this.lock.release() ··· 77 102 // internal 78 103 // = 79 104 80 - async _getFollows() { 81 - const res = await this.rootStore.agent.getFollows({ 82 - actor: this.rootStore.me.did || '', 83 - }) 84 - runInAction(() => { 85 - this.follows = res.data.follows.filter(f => !isInvalidHandle(f.handle)) 86 - for (const f of this.follows) { 87 - this.knownHandles.add(f.handle) 105 + _computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) { 106 + if (this.prefix) { 107 + const items: ProfileViewBasic[] = [] 108 + for (const item of this.follows) { 109 + if (prefixMatch(this.prefix, item)) { 110 + items.push(item) 111 + } 112 + if (items.length >= 8) { 113 + break 114 + } 115 + } 116 + for (const item of searchRes) { 117 + if (!items.find(item2 => item2.handle === item.handle)) { 118 + items.push({ 119 + did: item.did, 120 + handle: item.handle, 121 + displayName: item.displayName, 122 + avatar: item.avatar, 123 + }) 124 + } 88 125 } 89 - }) 126 + this._suggestions = items 127 + } else { 128 + this._suggestions = this.follows 129 + } 90 130 } 131 + } 91 132 92 - async _search() { 93 - const res = await this.rootStore.agent.searchActorsTypeahead({ 94 - term: this.prefix, 95 - limit: 8, 96 - }) 97 - runInAction(() => { 98 - this.searchRes = res.data.actors 99 - for (const u of this.searchRes) { 100 - this.knownHandles.add(u.handle) 101 - } 102 - }) 133 + function prefixMatch(prefix: string, info: ProfileViewBasic): boolean { 134 + if (info.handle.includes(prefix)) { 135 + return true 103 136 } 137 + if (info.displayName?.toLocaleLowerCase().includes(prefix)) { 138 + return true 139 + } 140 + return false 104 141 }
+1 -1
src/state/models/feeds/posts.ts
··· 316 316 this.emptyFetches = 0 317 317 } 318 318 319 - this.rootStore.me.follows.hydrateProfiles( 319 + this.rootStore.me.follows.hydrateMany( 320 320 res.feed.map(item => item.post.author), 321 321 ) 322 322 for (const item of res.feed) {
+1 -1
src/state/models/invited-users.ts
··· 61 61 profile => !profile.viewer?.following, 62 62 ) 63 63 }) 64 - this.rootStore.me.follows.hydrateProfiles(this.profiles) 64 + this.rootStore.me.follows.hydrateMany(this.profiles) 65 65 } catch (e) { 66 66 this.rootStore.log.error( 67 67 'Failed to fetch profiles for invited users',
+1 -1
src/state/models/lists/likes.ts
··· 126 126 _appendAll(res: GetLikes.Response) { 127 127 this.loadMoreCursor = res.data.cursor 128 128 this.hasMore = !!this.loadMoreCursor 129 - this.rootStore.me.follows.hydrateProfiles( 129 + this.rootStore.me.follows.hydrateMany( 130 130 res.data.likes.map(like => like.actor), 131 131 ) 132 132 this.likes = this.likes.concat(res.data.likes)
+1 -1
src/state/models/lists/reposted-by.ts
··· 130 130 this.loadMoreCursor = res.data.cursor 131 131 this.hasMore = !!this.loadMoreCursor 132 132 this.repostedBy = this.repostedBy.concat(res.data.repostedBy) 133 - this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy) 133 + this.rootStore.me.follows.hydrateMany(res.data.repostedBy) 134 134 } 135 135 }
+1 -1
src/state/models/lists/user-followers.ts
··· 115 115 this.loadMoreCursor = res.data.cursor 116 116 this.hasMore = !!this.loadMoreCursor 117 117 this.followers = this.followers.concat(res.data.followers) 118 - this.rootStore.me.follows.hydrateProfiles(res.data.followers) 118 + this.rootStore.me.follows.hydrateMany(res.data.followers) 119 119 } 120 120 }
+1 -1
src/state/models/lists/user-follows.ts
··· 115 115 this.loadMoreCursor = res.data.cursor 116 116 this.hasMore = !!this.loadMoreCursor 117 117 this.follows = this.follows.concat(res.data.follows) 118 - this.rootStore.me.follows.hydrateProfiles(res.data.follows) 118 + this.rootStore.me.follows.hydrateMany(res.data.follows) 119 119 } 120 120 }
+1 -1
src/state/models/ui/search.ts
··· 59 59 } while (profilesSearch.length) 60 60 } 61 61 62 - this.rootStore.me.follows.hydrateProfiles(profiles) 62 + this.rootStore.me.follows.hydrateMany(profiles) 63 63 64 64 runInAction(() => { 65 65 this.profiles = profiles
+1 -1
src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
··· 89 89 </View> 90 90 91 91 <FollowButton 92 - did={profile.did} 92 + profile={profile} 93 93 labelStyle={styles.followButton} 94 94 onToggleFollow={async isFollow => { 95 95 if (isFollow) {
+1 -1
src/view/com/notifications/InvitedUsers.tsx
··· 75 75 <FollowButton 76 76 unfollowedType="primary" 77 77 followedType="primary-light" 78 - did={profile.did} 78 + profile={profile} 79 79 /> 80 80 <Button 81 81 testID="dismissBtn"
+5 -4
src/view/com/profile/FollowButton.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, TextStyle, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 + import {AppBskyActorDefs} from '@atproto/api' 4 5 import {Button, ButtonType} from '../util/forms/Button' 5 6 import * as Toast from '../util/Toast' 6 7 import {FollowState} from 'state/models/cache/my-follows' 7 - import {useFollowDid} from 'lib/hooks/useFollowDid' 8 + import {useFollowProfile} from 'lib/hooks/useFollowProfile' 8 9 9 10 export const FollowButton = observer(function FollowButtonImpl({ 10 11 unfollowedType = 'inverted', 11 12 followedType = 'default', 12 - did, 13 + profile, 13 14 onToggleFollow, 14 15 labelStyle, 15 16 }: { 16 17 unfollowedType?: ButtonType 17 18 followedType?: ButtonType 18 - did: string 19 + profile: AppBskyActorDefs.ProfileViewBasic 19 20 onToggleFollow?: (v: boolean) => void 20 21 labelStyle?: StyleProp<TextStyle> 21 22 }) { 22 - const {state, following, toggle} = useFollowDid({did}) 23 + const {state, following, toggle} = useFollowProfile(profile) 23 24 24 25 const onPress = React.useCallback(async () => { 25 26 try {
+1 -1
src/view/com/profile/ProfileCard.tsx
··· 200 200 noBorder={noBorder} 201 201 followers={followers} 202 202 renderButton={ 203 - isMe ? undefined : () => <FollowButton did={profile.did} /> 203 + isMe ? undefined : () => <FollowButton profile={profile} /> 204 204 } 205 205 /> 206 206 )
+3 -3
src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
··· 19 19 import {usePalette} from 'lib/hooks/usePalette' 20 20 import {Text} from 'view/com/util/text/Text' 21 21 import {UserAvatar} from 'view/com/util/UserAvatar' 22 - import {useFollowDid} from 'lib/hooks/useFollowDid' 22 + import {useFollowProfile} from 'lib/hooks/useFollowProfile' 23 23 import {Button} from 'view/com/util/forms/Button' 24 24 import {sanitizeDisplayName} from 'lib/strings/display-names' 25 25 import {sanitizeHandle} from 'lib/strings/handles' ··· 83 83 return [] 84 84 } 85 85 86 - store.me.follows.hydrateProfiles(suggestions) 86 + store.me.follows.hydrateMany(suggestions) 87 87 88 88 return suggestions 89 89 } catch (e) { ··· 218 218 const {track} = useAnalytics() 219 219 const pal = usePalette('default') 220 220 const store = useStores() 221 - const {following, toggle} = useFollowDid({did: profile.did}) 221 + const {following, toggle} = useFollowProfile(profile) 222 222 const moderation = moderateProfile(profile, store.preferences.moderationOpts) 223 223 224 224 const onPress = React.useCallback(async () => {
+6 -6
src/view/screens/SearchMobile.tsx
··· 148 148 style={pal.view} 149 149 onScroll={onMainScroll} 150 150 scrollEventThrottle={100}> 151 - {query && autocompleteView.searchRes.length ? ( 151 + {query && autocompleteView.suggestions.length ? ( 152 152 <> 153 - {autocompleteView.searchRes.map((profile, index) => ( 153 + {autocompleteView.suggestions.map((suggestion, index) => ( 154 154 <ProfileCard 155 - key={profile.did} 156 - testID={`searchAutoCompleteResult-${profile.handle}`} 157 - profile={profile} 155 + key={suggestion.did} 156 + testID={`searchAutoCompleteResult-${suggestion.handle}`} 157 + profile={suggestion} 158 158 noBorder={index === 0} 159 159 /> 160 160 ))} 161 161 </> 162 - ) : query && !autocompleteView.searchRes.length ? ( 162 + ) : query && !autocompleteView.suggestions.length ? ( 163 163 <View> 164 164 <Text style={[pal.textLight, styles.searchPrompt]}> 165 165 No results found for {autocompleteView.prefix}
+2 -2
src/view/shell/desktop/Search.tsx
··· 90 90 91 91 {query !== '' && ( 92 92 <View style={[pal.view, pal.borderDark, styles.resultsContainer]}> 93 - {autocompleteView.searchRes.length ? ( 93 + {autocompleteView.suggestions.length ? ( 94 94 <> 95 - {autocompleteView.searchRes.map((item, i) => ( 95 + {autocompleteView.suggestions.map((item, i) => ( 96 96 <ProfileCard key={item.did} profile={item} noBorder={i === 0} /> 97 97 ))} 98 98 </>