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
This commit is contained in:
Paul Frazee 2023-10-05 16:44:05 -07:00 committed by GitHub
parent 19f8389fc7
commit bd7db8af26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 197 additions and 142 deletions

View file

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {FollowState} from 'state/models/cache/my-follows' import {FollowState} from 'state/models/cache/my-follows'
export function useFollowDid({did}: {did: string}) { export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
const store = useStores() const store = useStores()
const state = store.me.follows.getFollowState(did) const state = store.me.follows.getFollowState(profile.did)
return { return {
state, state,
@ -13,8 +13,10 @@ export function useFollowDid({did}: {did: string}) {
toggle: React.useCallback(async () => { toggle: React.useCallback(async () => {
if (state === FollowState.Following) { if (state === FollowState.Following) {
try { try {
await store.agent.deleteFollow(store.me.follows.getFollowUri(did)) await store.agent.deleteFollow(
store.me.follows.removeFollow(did) store.me.follows.getFollowUri(profile.did),
)
store.me.follows.removeFollow(profile.did)
return { return {
state: FollowState.NotFollowing, state: FollowState.NotFollowing,
following: false, following: false,
@ -25,8 +27,14 @@ export function useFollowDid({did}: {did: string}) {
} }
} else if (state === FollowState.NotFollowing) { } else if (state === FollowState.NotFollowing) {
try { try {
const res = await store.agent.follow(did) const res = await store.agent.follow(profile.did)
store.me.follows.addFollow(did, res.uri) store.me.follows.addFollow(profile.did, {
followRecordUri: res.uri,
did: profile.did,
handle: profile.handle,
displayName: profile.displayName,
avatar: profile.avatar,
})
return { return {
state: FollowState.Following, state: FollowState.Following,
following: true, following: true,
@ -41,6 +49,6 @@ export function useFollowDid({did}: {did: string}) {
state: FollowState.Unknown, state: FollowState.Unknown,
following: false, following: false,
} }
}, [store, did, state]), }, [store, profile, state]),
} }
} }

View file

@ -1,7 +1,14 @@
import {makeAutoObservable} from 'mobx' import {makeAutoObservable} from 'mobx'
import {AppBskyActorDefs} from '@atproto/api' import {
AppBskyActorDefs,
AppBskyGraphGetFollows as GetFollows,
moderateProfile,
} from '@atproto/api'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
const MAX_SYNC_PAGES = 10
const SYNC_TTL = 60e3 * 10 // 10 minutes
type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
export enum FollowState { export enum FollowState {
@ -10,6 +17,14 @@ export enum FollowState {
Unknown, Unknown,
} }
export interface FollowInfo {
did: string
followRecordUri: string | undefined
handle: string
displayName: string | undefined
avatar: string | undefined
}
/** /**
* This model is used to maintain a synced local cache of the user's * This model is used to maintain a synced local cache of the user's
* follows. It should be periodically refreshed and updated any time * follows. It should be periodically refreshed and updated any time
@ -17,9 +32,8 @@ export enum FollowState {
*/ */
export class MyFollowsCache { export class MyFollowsCache {
// data // data
followDidToRecordMap: Record<string, string | boolean> = {} byDid: Record<string, FollowInfo> = {}
lastSync = 0 lastSync = 0
myDid?: string
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
makeAutoObservable( makeAutoObservable(
@ -35,16 +49,45 @@ export class MyFollowsCache {
// = // =
clear() { clear() {
this.followDidToRecordMap = {} this.byDid = {}
this.lastSync = 0 }
this.myDid = undefined
/**
* Syncs a subset of the user's follows
* for performance reasons, caps out at 1000 follows
*/
async syncIfNeeded() {
if (this.lastSync > Date.now() - SYNC_TTL) {
return
}
let cursor
for (let i = 0; i < MAX_SYNC_PAGES; i++) {
const res: GetFollows.Response = await this.rootStore.agent.getFollows({
actor: this.rootStore.me.did,
cursor,
limit: 100,
})
res.data.follows = res.data.follows.filter(
profile =>
!moderateProfile(profile, this.rootStore.preferences.moderationOpts)
.account.filter,
)
this.hydrateMany(res.data.follows)
if (!res.data.cursor) {
break
}
cursor = res.data.cursor
}
this.lastSync = Date.now()
} }
getFollowState(did: string): FollowState { getFollowState(did: string): FollowState {
if (typeof this.followDidToRecordMap[did] === 'undefined') { if (typeof this.byDid[did] === 'undefined') {
return FollowState.Unknown return FollowState.Unknown
} }
if (typeof this.followDidToRecordMap[did] === 'string') { if (typeof this.byDid[did].followRecordUri === 'string') {
return FollowState.Following return FollowState.Following
} }
return FollowState.NotFollowing return FollowState.NotFollowing
@ -53,49 +96,41 @@ export class MyFollowsCache {
async fetchFollowState(did: string): Promise<FollowState> { async fetchFollowState(did: string): Promise<FollowState> {
// TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf // TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf
const res = await this.rootStore.agent.getProfile({actor: did}) const res = await this.rootStore.agent.getProfile({actor: did})
if (res.data.viewer?.following) { this.hydrate(did, res.data)
this.addFollow(did, res.data.viewer.following)
} else {
this.removeFollow(did)
}
return this.getFollowState(did) return this.getFollowState(did)
} }
getFollowUri(did: string): string { getFollowUri(did: string): string {
const v = this.followDidToRecordMap[did] const v = this.byDid[did]
if (typeof v === 'string') { if (typeof v === 'string') {
return v return v
} }
throw new Error('Not a followed user') throw new Error('Not a followed user')
} }
addFollow(did: string, recordUri: string) { addFollow(did: string, info: FollowInfo) {
this.followDidToRecordMap[did] = recordUri this.byDid[did] = info
} }
removeFollow(did: string) { removeFollow(did: string) {
this.followDidToRecordMap[did] = false if (this.byDid[did]) {
} this.byDid[did].followRecordUri = undefined
/**
* Use this to incrementally update the cache as views provide information
*/
hydrate(did: string, recordUri: string | undefined) {
if (recordUri) {
this.followDidToRecordMap[did] = recordUri
} else {
this.followDidToRecordMap[did] = false
} }
} }
/** hydrate(did: string, profile: Profile) {
* Use this to incrementally update the cache as views provide information this.byDid[did] = {
*/ did,
hydrateProfiles(profiles: Profile[]) { followRecordUri: profile.viewer?.following,
handle: profile.handle,
displayName: profile.displayName,
avatar: profile.avatar,
}
}
hydrateMany(profiles: Profile[]) {
for (const profile of profiles) { for (const profile of profiles) {
if (profile.viewer) { this.hydrate(profile.did, profile)
this.hydrate(profile.did, profile.viewer.following)
}
} }
} }
} }

View file

@ -137,7 +137,7 @@ export class ProfileModel {
runInAction(() => { runInAction(() => {
this.followersCount++ this.followersCount++
this.viewer.following = res.uri this.viewer.following = res.uri
this.rootStore.me.follows.addFollow(this.did, res.uri) this.rootStore.me.follows.hydrate(this.did, this)
}) })
track('Profile:Follow', { track('Profile:Follow', {
username: this.handle, username: this.handle,
@ -290,8 +290,8 @@ export class ProfileModel {
this.labels = res.data.labels this.labels = res.data.labels
if (res.data.viewer) { if (res.data.viewer) {
Object.assign(this.viewer, res.data.viewer) Object.assign(this.viewer, res.data.viewer)
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
} }
this.rootStore.me.follows.hydrate(this.did, res.data)
} }
async _createRichText() { async _createRichText() {

View file

@ -1,8 +1,4 @@
import { import {AppBskyActorDefs} from '@atproto/api'
AppBskyActorDefs,
AppBskyGraphGetFollows as GetFollows,
moderateProfile,
} from '@atproto/api'
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import sampleSize from 'lodash.samplesize' import sampleSize from 'lodash.samplesize'
import {bundleAsync} from 'lib/async/bundle' import {bundleAsync} from 'lib/async/bundle'
@ -43,35 +39,13 @@ export class FoafsModel {
try { try {
this.isLoading = true this.isLoading = true
// fetch & hydrate up to 1000 follows // fetch some of the user's follows
{ await this.rootStore.me.follows.syncIfNeeded()
let cursor
for (let i = 0; i < 10; i++) {
const res: GetFollows.Response =
await this.rootStore.agent.getFollows({
actor: this.rootStore.me.did,
cursor,
limit: 100,
})
res.data.follows = res.data.follows.filter(
profile =>
!moderateProfile(
profile,
this.rootStore.preferences.moderationOpts,
).account.filter,
)
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
if (!res.data.cursor) {
break
}
cursor = res.data.cursor
}
}
// grab 10 of the users followed by the user // grab 10 of the users followed by the user
runInAction(() => { runInAction(() => {
this.sources = sampleSize( this.sources = sampleSize(
Object.keys(this.rootStore.me.follows.followDidToRecordMap), Object.keys(this.rootStore.me.follows.byDid),
10, 10,
) )
}) })
@ -100,7 +74,7 @@ export class FoafsModel {
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const res = results[i] const res = results[i]
if (res.status === 'fulfilled') { if (res.status === 'fulfilled') {
this.rootStore.me.follows.hydrateProfiles(res.value.data.follows) this.rootStore.me.follows.hydrateMany(res.value.data.follows)
} }
const profile = profiles.data.profiles[i] const profile = profiles.data.profiles[i]
const source = this.sources[i] const source = this.sources[i]

View file

@ -76,7 +76,7 @@ export class SuggestedActorsModel {
!moderateProfile(actor, this.rootStore.preferences.moderationOpts) !moderateProfile(actor, this.rootStore.preferences.moderationOpts)
.account.filter, .account.filter,
) )
this.rootStore.me.follows.hydrateProfiles(actors) this.rootStore.me.follows.hydrateMany(actors)
runInAction(() => { runInAction(() => {
if (replace) { if (replace) {
@ -118,7 +118,7 @@ export class SuggestedActorsModel {
actor: actor, actor: actor,
}) })
const {suggestions: moreSuggestions} = res.data const {suggestions: moreSuggestions} = res.data
this.rootStore.me.follows.hydrateProfiles(moreSuggestions) this.rootStore.me.follows.hydrateMany(moreSuggestions)
// dedupe // dedupe
const toInsert = moreSuggestions.filter( const toInsert = moreSuggestions.filter(
s => !this.suggestions.find(s2 => s2.did === s.did), s => !this.suggestions.find(s2 => s2.did === s.did),

View file

@ -4,6 +4,8 @@ import AwaitLock from 'await-lock'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
import {isInvalidHandle} from 'lib/strings/handles' import {isInvalidHandle} from 'lib/strings/handles'
type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic
export class UserAutocompleteModel { export class UserAutocompleteModel {
// state // state
isLoading = false isLoading = false
@ -12,9 +14,8 @@ export class UserAutocompleteModel {
lock = new AwaitLock() lock = new AwaitLock()
// data // data
follows: AppBskyActorDefs.ProfileViewBasic[] = []
searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
knownHandles: Set<string> = new Set() knownHandles: Set<string> = new Set()
_suggestions: ProfileViewBasic[] = []
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
makeAutoObservable( makeAutoObservable(
@ -27,29 +28,35 @@ export class UserAutocompleteModel {
) )
} }
get suggestions() { get follows(): ProfileViewBasic[] {
return Object.values(this.rootStore.me.follows.byDid).map(item => ({
did: item.did,
handle: item.handle,
displayName: item.displayName,
avatar: item.avatar,
}))
}
get suggestions(): ProfileViewBasic[] {
if (!this.isActive) { if (!this.isActive) {
return [] return []
} }
if (this.prefix) { return this._suggestions
return this.searchRes.map(user => ({
handle: user.handle,
displayName: user.displayName,
avatar: user.avatar,
}))
}
return this.follows.map(follow => ({
handle: follow.handle,
displayName: follow.displayName,
avatar: follow.avatar,
}))
} }
// public api // public api
// = // =
async setup() { async setup() {
await this._getFollows() await this.rootStore.me.follows.syncIfNeeded()
runInAction(() => {
for (const did in this.rootStore.me.follows.byDid) {
const info = this.rootStore.me.follows.byDid[did]
if (!isInvalidHandle(info.handle)) {
this.knownHandles.add(info.handle)
}
}
})
} }
setActive(v: boolean) { setActive(v: boolean) {
@ -57,7 +64,7 @@ export class UserAutocompleteModel {
} }
async setPrefix(prefix: string) { async setPrefix(prefix: string) {
const origPrefix = prefix.trim() const origPrefix = prefix.trim().toLocaleLowerCase()
this.prefix = origPrefix this.prefix = origPrefix
await this.lock.acquireAsync() await this.lock.acquireAsync()
try { try {
@ -65,9 +72,27 @@ export class UserAutocompleteModel {
if (this.prefix !== origPrefix) { if (this.prefix !== origPrefix) {
return // another prefix was set before we got our chance return // another prefix was set before we got our chance
} }
await this._search()
// reset to follow results
this._computeSuggestions([])
// ask backend
const res = await this.rootStore.agent.searchActorsTypeahead({
term: this.prefix,
limit: 8,
})
this._computeSuggestions(res.data.actors)
// update known handles
runInAction(() => {
for (const u of res.data.actors) {
this.knownHandles.add(u.handle)
}
})
} else { } else {
this.searchRes = [] runInAction(() => {
this._computeSuggestions([])
})
} }
} finally { } finally {
this.lock.release() this.lock.release()
@ -77,28 +102,40 @@ export class UserAutocompleteModel {
// internal // internal
// = // =
async _getFollows() { _computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) {
const res = await this.rootStore.agent.getFollows({ if (this.prefix) {
actor: this.rootStore.me.did || '', const items: ProfileViewBasic[] = []
}) for (const item of this.follows) {
runInAction(() => { if (prefixMatch(this.prefix, item)) {
this.follows = res.data.follows.filter(f => !isInvalidHandle(f.handle)) items.push(item)
for (const f of this.follows) { }
this.knownHandles.add(f.handle) if (items.length >= 8) {
break
}
} }
}) for (const item of searchRes) {
} if (!items.find(item2 => item2.handle === item.handle)) {
items.push({
async _search() { did: item.did,
const res = await this.rootStore.agent.searchActorsTypeahead({ handle: item.handle,
term: this.prefix, displayName: item.displayName,
limit: 8, avatar: item.avatar,
}) })
runInAction(() => { }
this.searchRes = res.data.actors
for (const u of this.searchRes) {
this.knownHandles.add(u.handle)
} }
}) this._suggestions = items
} else {
this._suggestions = this.follows
}
} }
} }
function prefixMatch(prefix: string, info: ProfileViewBasic): boolean {
if (info.handle.includes(prefix)) {
return true
}
if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
return true
}
return false
}

View file

@ -316,7 +316,7 @@ export class PostsFeedModel {
this.emptyFetches = 0 this.emptyFetches = 0
} }
this.rootStore.me.follows.hydrateProfiles( this.rootStore.me.follows.hydrateMany(
res.feed.map(item => item.post.author), res.feed.map(item => item.post.author),
) )
for (const item of res.feed) { for (const item of res.feed) {

View file

@ -61,7 +61,7 @@ export class InvitedUsers {
profile => !profile.viewer?.following, profile => !profile.viewer?.following,
) )
}) })
this.rootStore.me.follows.hydrateProfiles(this.profiles) this.rootStore.me.follows.hydrateMany(this.profiles)
} catch (e) { } catch (e) {
this.rootStore.log.error( this.rootStore.log.error(
'Failed to fetch profiles for invited users', 'Failed to fetch profiles for invited users',

View file

@ -126,7 +126,7 @@ export class LikesModel {
_appendAll(res: GetLikes.Response) { _appendAll(res: GetLikes.Response) {
this.loadMoreCursor = res.data.cursor this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor this.hasMore = !!this.loadMoreCursor
this.rootStore.me.follows.hydrateProfiles( this.rootStore.me.follows.hydrateMany(
res.data.likes.map(like => like.actor), res.data.likes.map(like => like.actor),
) )
this.likes = this.likes.concat(res.data.likes) this.likes = this.likes.concat(res.data.likes)

View file

@ -130,6 +130,6 @@ export class RepostedByModel {
this.loadMoreCursor = res.data.cursor this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor this.hasMore = !!this.loadMoreCursor
this.repostedBy = this.repostedBy.concat(res.data.repostedBy) this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy) this.rootStore.me.follows.hydrateMany(res.data.repostedBy)
} }
} }

View file

@ -115,6 +115,6 @@ export class UserFollowersModel {
this.loadMoreCursor = res.data.cursor this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor this.hasMore = !!this.loadMoreCursor
this.followers = this.followers.concat(res.data.followers) this.followers = this.followers.concat(res.data.followers)
this.rootStore.me.follows.hydrateProfiles(res.data.followers) this.rootStore.me.follows.hydrateMany(res.data.followers)
} }
} }

View file

@ -115,6 +115,6 @@ export class UserFollowsModel {
this.loadMoreCursor = res.data.cursor this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor this.hasMore = !!this.loadMoreCursor
this.follows = this.follows.concat(res.data.follows) this.follows = this.follows.concat(res.data.follows)
this.rootStore.me.follows.hydrateProfiles(res.data.follows) this.rootStore.me.follows.hydrateMany(res.data.follows)
} }
} }

View file

@ -59,7 +59,7 @@ export class SearchUIModel {
} while (profilesSearch.length) } while (profilesSearch.length)
} }
this.rootStore.me.follows.hydrateProfiles(profiles) this.rootStore.me.follows.hydrateMany(profiles)
runInAction(() => { runInAction(() => {
this.profiles = profiles this.profiles = profiles

View file

@ -89,7 +89,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
</View> </View>
<FollowButton <FollowButton
did={profile.did} profile={profile}
labelStyle={styles.followButton} labelStyle={styles.followButton}
onToggleFollow={async isFollow => { onToggleFollow={async isFollow => {
if (isFollow) { if (isFollow) {

View file

@ -75,7 +75,7 @@ function InvitedUser({
<FollowButton <FollowButton
unfollowedType="primary" unfollowedType="primary"
followedType="primary-light" followedType="primary-light"
did={profile.did} profile={profile}
/> />
<Button <Button
testID="dismissBtn" testID="dismissBtn"

View file

@ -1,25 +1,26 @@
import React from 'react' import React from 'react'
import {StyleProp, TextStyle, View} from 'react-native' import {StyleProp, TextStyle, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {AppBskyActorDefs} from '@atproto/api'
import {Button, ButtonType} from '../util/forms/Button' import {Button, ButtonType} from '../util/forms/Button'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {FollowState} from 'state/models/cache/my-follows' import {FollowState} from 'state/models/cache/my-follows'
import {useFollowDid} from 'lib/hooks/useFollowDid' import {useFollowProfile} from 'lib/hooks/useFollowProfile'
export const FollowButton = observer(function FollowButtonImpl({ export const FollowButton = observer(function FollowButtonImpl({
unfollowedType = 'inverted', unfollowedType = 'inverted',
followedType = 'default', followedType = 'default',
did, profile,
onToggleFollow, onToggleFollow,
labelStyle, labelStyle,
}: { }: {
unfollowedType?: ButtonType unfollowedType?: ButtonType
followedType?: ButtonType followedType?: ButtonType
did: string profile: AppBskyActorDefs.ProfileViewBasic
onToggleFollow?: (v: boolean) => void onToggleFollow?: (v: boolean) => void
labelStyle?: StyleProp<TextStyle> labelStyle?: StyleProp<TextStyle>
}) { }) {
const {state, following, toggle} = useFollowDid({did}) const {state, following, toggle} = useFollowProfile(profile)
const onPress = React.useCallback(async () => { const onPress = React.useCallback(async () => {
try { try {

View file

@ -200,7 +200,7 @@ export const ProfileCardWithFollowBtn = observer(
noBorder={noBorder} noBorder={noBorder}
followers={followers} followers={followers}
renderButton={ renderButton={
isMe ? undefined : () => <FollowButton did={profile.did} /> isMe ? undefined : () => <FollowButton profile={profile} />
} }
/> />
) )

View file

@ -19,7 +19,7 @@ import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {useFollowDid} from 'lib/hooks/useFollowDid' import {useFollowProfile} from 'lib/hooks/useFollowProfile'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
@ -83,7 +83,7 @@ export function ProfileHeaderSuggestedFollows({
return [] return []
} }
store.me.follows.hydrateProfiles(suggestions) store.me.follows.hydrateMany(suggestions)
return suggestions return suggestions
} catch (e) { } catch (e) {
@ -218,7 +218,7 @@ const SuggestedFollow = observer(function SuggestedFollowImpl({
const {track} = useAnalytics() const {track} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {following, toggle} = useFollowDid({did: profile.did}) const {following, toggle} = useFollowProfile(profile)
const moderation = moderateProfile(profile, store.preferences.moderationOpts) const moderation = moderateProfile(profile, store.preferences.moderationOpts)
const onPress = React.useCallback(async () => { const onPress = React.useCallback(async () => {

View file

@ -148,18 +148,18 @@ export const SearchScreen = withAuthRequired(
style={pal.view} style={pal.view}
onScroll={onMainScroll} onScroll={onMainScroll}
scrollEventThrottle={100}> scrollEventThrottle={100}>
{query && autocompleteView.searchRes.length ? ( {query && autocompleteView.suggestions.length ? (
<> <>
{autocompleteView.searchRes.map((profile, index) => ( {autocompleteView.suggestions.map((suggestion, index) => (
<ProfileCard <ProfileCard
key={profile.did} key={suggestion.did}
testID={`searchAutoCompleteResult-${profile.handle}`} testID={`searchAutoCompleteResult-${suggestion.handle}`}
profile={profile} profile={suggestion}
noBorder={index === 0} noBorder={index === 0}
/> />
))} ))}
</> </>
) : query && !autocompleteView.searchRes.length ? ( ) : query && !autocompleteView.suggestions.length ? (
<View> <View>
<Text style={[pal.textLight, styles.searchPrompt]}> <Text style={[pal.textLight, styles.searchPrompt]}>
No results found for {autocompleteView.prefix} No results found for {autocompleteView.prefix}

View file

@ -90,9 +90,9 @@ export const DesktopSearch = observer(function DesktopSearch() {
{query !== '' && ( {query !== '' && (
<View style={[pal.view, pal.borderDark, styles.resultsContainer]}> <View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
{autocompleteView.searchRes.length ? ( {autocompleteView.suggestions.length ? (
<> <>
{autocompleteView.searchRes.map((item, i) => ( {autocompleteView.suggestions.map((item, i) => (
<ProfileCard key={item.did} profile={item} noBorder={i === 0} /> <ProfileCard key={item.did} profile={item} noBorder={i === 0} />
))} ))}
</> </>