Rework search suggestions for performance (#492)

zio/stable
Paul Frazee 2023-04-18 18:29:54 -05:00 committed by GitHub
parent 1ab8f31517
commit 75fd653be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 312 additions and 448 deletions

View File

@ -60,7 +60,7 @@
"await-lock": "^2.2.2", "await-lock": "^2.2.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"expo": "~48.0.10", "expo": "~48.0.11",
"expo-build-properties": "~0.5.1", "expo-build-properties": "~0.5.1",
"expo-camera": "~13.2.1", "expo-camera": "~13.2.1",
"expo-dev-client": "~2.1.1", "expo-dev-client": "~2.1.1",

View File

@ -57,15 +57,19 @@ export class FoafsModel {
} }
// grab 10 of the users followed by the user // grab 10 of the users followed by the user
this.sources = sampleSize( runInAction(() => {
Object.keys(this.rootStore.me.follows.followDidToRecordMap), this.sources = sampleSize(
10, Object.keys(this.rootStore.me.follows.followDidToRecordMap),
) 10,
)
})
if (this.sources.length === 0) { if (this.sources.length === 0) {
return return
} }
this.foafs.clear() runInAction(() => {
this.popular.length = 0 this.foafs.clear()
this.popular.length = 0
})
// fetch their profiles // fetch their profiles
const profiles = await this.rootStore.agent.getProfiles({ const profiles = await this.rootStore.agent.getProfiles({

View File

@ -1,68 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
export const SuggestedFollows = ({
title,
suggestions,
}: {
title: string
suggestions: (
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileView
| RefWithInfoAndFollowers
)[]
}) => {
const pal = usePalette('default')
return (
<View style={[styles.container, pal.view, pal.border]}>
<Text type="title" style={[styles.heading, pal.text]}>
{title}
</Text>
{suggestions.map(item => (
<View key={item.did} style={[styles.card, pal.view, pal.border]}>
<ProfileCardWithFollowBtn
key={item.did}
did={item.did}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
noBg
noBorder
description={
item.description
? (item as AppBskyActorDefs.ProfileView).description
: ''
}
followers={
item.followers
? (item.followers as AppBskyActorDefs.ProfileView[])
: undefined
}
/>
</View>
))}
</View>
)
}
const styles = StyleSheet.create({
container: {
borderBottomWidth: 1,
},
heading: {
fontWeight: 'bold',
paddingHorizontal: 12,
paddingBottom: 8,
},
card: {
borderTopWidth: 1,
},
})

View File

@ -1,65 +1,255 @@
import React from 'react' import React, {forwardRef, ForwardedRef} from 'react'
import {StyleSheet, View} from 'react-native' import {RefreshControl, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {AppBskyActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {FoafsModel} from 'state/models/discovery/foafs' import {FoafsModel} from 'state/models/discovery/foafs'
import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors' import {
import {SuggestedFollows} from 'view/com/discover/SuggestedFollows' SuggestedActorsModel,
SuggestedActor,
} from 'state/models/discovery/suggested-actors'
import {Text} from '../util/text/Text'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
import {usePalette} from 'lib/hooks/usePalette'
interface Heading {
_reactKey: string
type: 'heading'
title: string
}
interface RefWrapper {
_reactKey: string
type: 'ref'
ref: RefWithInfoAndFollowers
}
interface SuggestWrapper {
_reactKey: string
type: 'suggested'
suggested: SuggestedActor
}
interface ProfileView {
_reactKey: string
type: 'profile-view'
view: AppBskyActorDefs.ProfileViewBasic
}
type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
export const Suggestions = observer( export const Suggestions = observer(
({ forwardRef(
foafs, (
suggestedActors, {
}: { foafs,
foafs: FoafsModel suggestedActors,
suggestedActors: SuggestedActorsModel }: {
}) => { foafs: FoafsModel
if (foafs.isLoading || suggestedActors.isLoading) { suggestedActors: SuggestedActorsModel
return <ProfileCardFeedLoadingPlaceholder /> },
} flatListRef: ForwardedRef<FlatList>,
return ( ) => {
<> const pal = usePalette('default')
{foafs.popular.length > 0 && ( const [refreshing, setRefreshing] = React.useState(false)
<View style={styles.suggestions}> const data = React.useMemo(() => {
<SuggestedFollows let items: Item[] = []
title="In your network"
suggestions={foafs.popular} if (foafs.popular.length > 0) {
/> items = items
</View> .concat([
)} {
{suggestedActors.hasContent && ( _reactKey: '__popular_heading__',
<View style={styles.suggestions}> type: 'heading',
<SuggestedFollows title: 'In your network',
title="Suggested follows" },
suggestions={suggestedActors.suggestions} ])
/> .concat(
</View> foafs.popular.map(ref => ({
)} _reactKey: `popular-${ref.did}`,
{foafs.sources.map((source, i) => { type: 'ref',
ref,
})),
)
}
if (suggestedActors.hasContent) {
items = items
.concat([
{
_reactKey: '__suggested_heading__',
type: 'heading',
title: 'Suggested follows',
},
])
.concat(
suggestedActors.suggestions.map(suggested => ({
_reactKey: `suggested-${suggested.did}`,
type: 'suggested',
suggested,
})),
)
}
for (const source of foafs.sources) {
const item = foafs.foafs.get(source) const item = foafs.foafs.get(source)
if (!item || item.follows.length === 0) { if (!item || item.follows.length === 0) {
return <View key={`sf-${item?.did || i}`} /> return
} }
return ( items = items
<View key={`sf-${item.did}`} style={styles.suggestions}> .concat([
<SuggestedFollows {
title={`Followed by ${sanitizeDisplayName( _reactKey: `__${item.did}_heading__`,
type: 'heading',
title: `Followed by ${sanitizeDisplayName(
item.displayName || item.handle, item.displayName || item.handle,
)}`} )}`,
suggestions={item.follows.slice(0, 10)} },
/> ])
</View> .concat(
) item.follows.slice(0, 10).map(view => ({
})} _reactKey: `${item.did}-${view.did}`,
</> type: 'profile-view',
) view,
}, })),
)
}
return items
}, [
foafs.popular,
suggestedActors.hasContent,
suggestedActors.suggestions,
foafs.sources,
foafs.foafs,
])
const onRefresh = React.useCallback(async () => {
setRefreshing(true)
try {
await foafs.fetch()
} finally {
setRefreshing(false)
}
}, [foafs, setRefreshing])
const renderItem = React.useCallback(
({item}: {item: Item}) => {
if (item.type === 'heading') {
return (
<Text type="title" style={[styles.heading, pal.text]}>
{item.title}
</Text>
)
}
if (item.type === 'ref') {
return (
<View style={[styles.card, pal.view, pal.border]}>
<ProfileCardWithFollowBtn
key={item.ref.did}
did={item.ref.did}
handle={item.ref.handle}
displayName={item.ref.displayName}
avatar={item.ref.avatar}
labels={item.ref.labels}
noBg
noBorder
description={
item.ref.description
? (item.ref as AppBskyActorDefs.ProfileView).description
: ''
}
followers={
item.ref.followers
? (item.ref.followers as AppBskyActorDefs.ProfileView[])
: undefined
}
/>
</View>
)
}
if (item.type === 'profile-view') {
return (
<View style={[styles.card, pal.view, pal.border]}>
<ProfileCardWithFollowBtn
key={item.view.did}
did={item.view.did}
handle={item.view.handle}
displayName={item.view.displayName}
avatar={item.view.avatar}
labels={item.view.labels}
noBg
noBorder
description={
item.view.description
? (item.view as AppBskyActorDefs.ProfileView).description
: ''
}
/>
</View>
)
}
if (item.type === 'suggested') {
return (
<View style={[styles.card, pal.view, pal.border]}>
<ProfileCardWithFollowBtn
key={item.suggested.did}
did={item.suggested.did}
handle={item.suggested.handle}
displayName={item.suggested.displayName}
avatar={item.suggested.avatar}
labels={item.suggested.labels}
noBg
noBorder
description={
item.suggested.description
? (item.suggested as AppBskyActorDefs.ProfileView)
.description
: ''
}
/>
</View>
)
}
return null
},
[pal],
)
if (foafs.isLoading || suggestedActors.isLoading) {
return (
<CenteredView>
<ProfileCardFeedLoadingPlaceholder />
</CenteredView>
)
}
return (
<FlatList
ref={flatListRef}
data={data}
keyExtractor={item => item._reactKey}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
renderItem={renderItem}
initialNumToRender={15}
/>
)
},
),
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({
suggestions: { heading: {
marginTop: 10, fontWeight: 'bold',
marginBottom: 20, paddingHorizontal: 12,
paddingBottom: 8,
paddingTop: 16,
},
card: {
borderTopWidth: 1,
}, },
}) })

View File

@ -1,196 +1 @@
import React from 'react' export * from './SearchMobile'
import {
Keyboard,
RefreshControl,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ScrollView} from 'view/com/util/Views'
import {
NativeStackScreenProps,
SearchTabNavigatorParams,
} from 'lib/routes/types'
import {observer} from 'mobx-react-lite'
import {Text} from 'view/com/util/text/Text'
import {useStores} from 'state/index'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {SearchUIModel} from 'state/models/ui/search'
import {FoafsModel} from 'state/models/discovery/foafs'
import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
import {HeaderWithInput} from 'view/com/search/HeaderWithInput'
import {Suggestions} from 'view/com/search/Suggestions'
import {SearchResults} from 'view/com/search/SearchResults'
import {s} from 'lib/styles'
import {ProfileCard} from 'view/com/profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = withAuthRequired(
observer<Props>(({}: Props) => {
const pal = usePalette('default')
const store = useStores()
const scrollElRef = React.useRef<ScrollView>(null)
const onMainScroll = useOnMainScroll(store)
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const [query, setQuery] = React.useState<string>('')
const autocompleteView = React.useMemo<UserAutocompleteModel>(
() => new UserAutocompleteModel(store),
[store],
)
const foafs = React.useMemo<FoafsModel>(
() => new FoafsModel(store),
[store],
)
const suggestedActors = React.useMemo<SuggestedActorsModel>(
() => new SuggestedActorsModel(store),
[store],
)
const [searchUIModel, setSearchUIModel] = React.useState<
SearchUIModel | undefined
>()
const [refreshing, setRefreshing] = React.useState(false)
const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0})
}
useFocusEffect(
React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
store.shell.setMinimalShellMode(false)
autocompleteView.setup()
if (!foafs.hasData) {
foafs.fetch()
}
if (!suggestedActors.hasLoaded) {
suggestedActors.loadMore(true)
}
return cleanup
}, [store, autocompleteView, foafs, suggestedActors]),
)
const onChangeQuery = React.useCallback(
(text: string) => {
setQuery(text)
if (text.length > 0) {
autocompleteView.setActive(true)
autocompleteView.setPrefix(text)
} else {
autocompleteView.setActive(false)
}
},
[setQuery, autocompleteView],
)
const onPressClearQuery = React.useCallback(() => {
setQuery('')
}, [setQuery])
const onPressCancelSearch = React.useCallback(() => {
setQuery('')
autocompleteView.setActive(false)
setSearchUIModel(undefined)
store.shell.setIsDrawerSwipeDisabled(false)
}, [setQuery, autocompleteView, store])
const onSubmitQuery = React.useCallback(() => {
const model = new SearchUIModel(store)
model.fetch(query)
setSearchUIModel(model)
store.shell.setIsDrawerSwipeDisabled(true)
}, [query, setSearchUIModel, store])
const onRefresh = React.useCallback(async () => {
setRefreshing(true)
try {
await foafs.fetch()
} finally {
setRefreshing(false)
}
}, [foafs, setRefreshing])
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={[pal.view, styles.container]}>
<HeaderWithInput
isInputFocused={isInputFocused}
query={query}
setIsInputFocused={setIsInputFocused}
onChangeQuery={onChangeQuery}
onPressClearQuery={onPressClearQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
/>
{searchUIModel ? (
<SearchResults model={searchUIModel} />
) : (
<ScrollView
ref={scrollElRef}
testID="searchScrollView"
style={pal.view}
onScroll={onMainScroll}
scrollEventThrottle={100}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}>
{query && autocompleteView.searchRes.length ? (
<>
{autocompleteView.searchRes.map(item => (
<ProfileCard
key={item.did}
testID={`searchAutoCompleteResult-${item.handle}`}
handle={item.handle}
displayName={item.displayName}
labels={item.labels}
avatar={item.avatar}
/>
))}
</>
) : query && !autocompleteView.searchRes.length ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
No results found for {autocompleteView.prefix}
</Text>
</View>
) : isInputFocused ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
Search for users on the network
</Text>
</View>
) : (
<Suggestions foafs={foafs} suggestedActors={suggestedActors} />
)}
<View style={s.footerSpacer} />
</ScrollView>
)}
</View>
</TouchableWithoutFeedback>
)
}),
)
const styles = StyleSheet.create({
container: {
flex: 1,
},
searchPrompt: {
textAlign: 'center',
paddingTop: 10,
},
})

View File

@ -1,10 +1,8 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native'
import {SearchUIModel} from 'state/models/ui/search' import {SearchUIModel} from 'state/models/ui/search'
import {FoafsModel} from 'state/models/discovery/foafs' import {FoafsModel} from 'state/models/discovery/foafs'
import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors' import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ScrollView} from 'view/com/util/Views'
import {Suggestions} from 'view/com/search/Suggestions' import {Suggestions} from 'view/com/search/Suggestions'
import {SearchResults} from 'view/com/search/SearchResults' import {SearchResults} from 'view/com/search/SearchResults'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
@ -13,15 +11,12 @@ import {
SearchTabNavigatorParams, SearchTabNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import * as Mobile from './SearchMobile' import * as Mobile from './SearchMobile'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = withAuthRequired( export const SearchScreen = withAuthRequired(
observer(({navigation, route}: Props) => { observer(({navigation, route}: Props) => {
const pal = usePalette('default')
const store = useStores() const store = useStores()
const params = route.params || {} const params = route.params || {}
const foafs = React.useMemo<FoafsModel>( const foafs = React.useMemo<FoafsModel>(
@ -59,58 +54,6 @@ export const SearchScreen = withAuthRequired(
return <Mobile.SearchScreen navigation={navigation} route={route} /> return <Mobile.SearchScreen navigation={navigation} route={route} />
} }
return ( return <Suggestions foafs={foafs} suggestedActors={suggestedActors} />
<ScrollView
testID="searchScrollView"
style={[pal.view, styles.container]}
scrollEventThrottle={100}>
<Suggestions foafs={foafs} suggestedActors={suggestedActors} />
<View style={s.footerSpacer} />
</ScrollView>
)
}), }),
) )
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 4,
marginBottom: 14,
},
headerMenuBtn: {
width: 40,
height: 30,
marginLeft: 6,
},
headerSearchContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 30,
paddingHorizontal: 12,
paddingVertical: 8,
},
headerSearchIcon: {
marginRight: 6,
alignSelf: 'center',
},
headerSearchInput: {
flex: 1,
fontSize: 17,
},
headerCancelBtn: {
width: 60,
paddingLeft: 10,
},
searchPrompt: {
textAlign: 'center',
paddingTop: 10,
},
})

View File

@ -1,14 +1,13 @@
import React from 'react' import React from 'react'
import { import {
Keyboard, Keyboard,
RefreshControl,
StyleSheet, StyleSheet,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
} from 'react-native' } from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ScrollView} from 'view/com/util/Views' import {FlatList, ScrollView} from 'view/com/util/Views'
import { import {
NativeStackScreenProps, NativeStackScreenProps,
SearchTabNavigatorParams, SearchTabNavigatorParams,
@ -33,7 +32,8 @@ export const SearchScreen = withAuthRequired(
observer<Props>(({}: Props) => { observer<Props>(({}: Props) => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const scrollElRef = React.useRef<ScrollView>(null) const scrollViewRef = React.useRef<ScrollView>(null)
const flatListRef = React.useRef<FlatList>(null)
const onMainScroll = useOnMainScroll(store) const onMainScroll = useOnMainScroll(store)
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const [query, setQuery] = React.useState<string>('') const [query, setQuery] = React.useState<string>('')
@ -52,31 +52,6 @@ export const SearchScreen = withAuthRequired(
const [searchUIModel, setSearchUIModel] = React.useState< const [searchUIModel, setSearchUIModel] = React.useState<
SearchUIModel | undefined SearchUIModel | undefined
>() >()
const [refreshing, setRefreshing] = React.useState(false)
const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0})
}
useFocusEffect(
React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
store.shell.setMinimalShellMode(false)
autocompleteView.setup()
if (!foafs.hasData) {
foafs.fetch()
}
if (!suggestedActors.hasLoaded) {
suggestedActors.loadMore(true)
}
return cleanup
}, [store, autocompleteView, foafs, suggestedActors]),
)
const onChangeQuery = React.useCallback( const onChangeQuery = React.useCallback(
(text: string) => { (text: string) => {
@ -109,14 +84,31 @@ export const SearchScreen = withAuthRequired(
store.shell.setIsDrawerSwipeDisabled(true) store.shell.setIsDrawerSwipeDisabled(true)
}, [query, setSearchUIModel, store]) }, [query, setSearchUIModel, store])
const onRefresh = React.useCallback(async () => { const onSoftReset = React.useCallback(() => {
setRefreshing(true) scrollViewRef.current?.scrollTo({x: 0, y: 0})
try { flatListRef.current?.scrollToOffset({offset: 0})
await foafs.fetch() onPressCancelSearch()
} finally { }, [scrollViewRef, flatListRef, onPressCancelSearch])
setRefreshing(false)
} useFocusEffect(
}, [foafs, setRefreshing]) React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
store.shell.setMinimalShellMode(false)
autocompleteView.setup()
if (!foafs.hasData) {
foafs.fetch()
}
if (!suggestedActors.hasLoaded) {
suggestedActors.loadMore(true)
}
return cleanup
}, [store, autocompleteView, foafs, suggestedActors, onSoftReset]),
)
return ( return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
@ -132,21 +124,19 @@ export const SearchScreen = withAuthRequired(
/> />
{searchUIModel ? ( {searchUIModel ? (
<SearchResults model={searchUIModel} /> <SearchResults model={searchUIModel} />
) : !isInputFocused && !query ? (
<Suggestions
ref={flatListRef}
foafs={foafs}
suggestedActors={suggestedActors}
/>
) : ( ) : (
<ScrollView <ScrollView
ref={scrollElRef} ref={scrollViewRef}
testID="searchScrollView" testID="searchScrollView"
style={pal.view} style={pal.view}
onScroll={onMainScroll} onScroll={onMainScroll}
scrollEventThrottle={100} scrollEventThrottle={100}>
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}>
{query && autocompleteView.searchRes.length ? ( {query && autocompleteView.searchRes.length ? (
<> <>
{autocompleteView.searchRes.map(item => ( {autocompleteView.searchRes.map(item => (
@ -155,6 +145,7 @@ export const SearchScreen = withAuthRequired(
testID={`searchAutoCompleteResult-${item.handle}`} testID={`searchAutoCompleteResult-${item.handle}`}
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
labels={item.labels}
avatar={item.avatar} avatar={item.avatar}
/> />
))} ))}
@ -171,9 +162,7 @@ export const SearchScreen = withAuthRequired(
Search for users on the network Search for users on the network
</Text> </Text>
</View> </View>
) : ( ) : null}
<Suggestions foafs={foafs} suggestedActors={suggestedActors} />
)}
<View style={s.footerSpacer} /> <View style={s.footerSpacer} />
</ScrollView> </ScrollView>
)} )}

View File

@ -1535,16 +1535,16 @@
mv "~2" mv "~2"
safe-json-stringify "~1" safe-json-stringify "~1"
"@expo/cli@0.6.2": "@expo/cli@0.7.0":
version "0.6.2" version "0.7.0"
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.6.2.tgz#1090c9d23f49d9603c4c85fa85b878b2848da322" resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.7.0.tgz#2a16873ced05c1f3b7f3990d7b410e9853600f45"
integrity sha512-uhmrXNemXTbCTKP/ycyJHOU/KLGdFwVCrWNBzz1VkwnmL8yJV5F3C18a83ybFFnUNfkGHeH5LtID7CSNbbTWKg== integrity sha512-9gjr3pRgwWzUDW/P7B4tA0QevKb+hCrvTmVc3Ce5w7CjdM3zNoBcro8vwviRHqkiB1IifG7zQh0PPStSbK+FRQ==
dependencies: dependencies:
"@babel/runtime" "^7.20.0" "@babel/runtime" "^7.20.0"
"@expo/code-signing-certificates" "0.0.5" "@expo/code-signing-certificates" "0.0.5"
"@expo/config" "~8.0.0" "@expo/config" "~8.0.0"
"@expo/config-plugins" "~6.0.0" "@expo/config-plugins" "~6.0.0"
"@expo/dev-server" "0.2.3" "@expo/dev-server" "0.3.0"
"@expo/devcert" "^1.0.0" "@expo/devcert" "^1.0.0"
"@expo/json-file" "^8.2.37" "@expo/json-file" "^8.2.37"
"@expo/metro-config" "~0.7.0" "@expo/metro-config" "~0.7.0"
@ -1600,6 +1600,7 @@
text-table "^0.2.0" text-table "^0.2.0"
url-join "4.0.0" url-join "4.0.0"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
ws "^8.12.1"
"@expo/code-signing-certificates@0.0.5": "@expo/code-signing-certificates@0.0.5":
version "0.0.5" version "0.0.5"
@ -1709,10 +1710,10 @@
xcode "^3.0.0" xcode "^3.0.0"
xml-js "^1.6.11" xml-js "^1.6.11"
"@expo/dev-server@0.2.3": "@expo/dev-server@0.3.0":
version "0.2.3" version "0.3.0"
resolved "https://registry.yarnpkg.com/@expo/dev-server/-/dev-server-0.2.3.tgz#736317cc1340b28dc49da8a45b85040306048e24" resolved "https://registry.yarnpkg.com/@expo/dev-server/-/dev-server-0.3.0.tgz#c575c88b0ec28f127f328a80ea6a3a4c6f785800"
integrity sha512-9+6QGRdymj3dmTp1vUpROvWJ+Ezz6Qp9xHafAcaRHzw322pUCOiRKxTYqDqYYZ/72shrHPGQ2CiIXTnV1vM2tA== integrity sha512-2A6/8uZADSKAtzyR6YqhCBUFxb5DFmjxmFn0EHMqnPnsh13ZSiKEjrZPrRkM6Li2EHLYqHK2rmweJ7O/7q9pPQ==
dependencies: dependencies:
"@expo/bunyan" "4.0.0" "@expo/bunyan" "4.0.0"
"@expo/metro-config" "~0.7.0" "@expo/metro-config" "~0.7.0"
@ -8390,10 +8391,10 @@ expo-media-library@~15.2.3:
resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-15.2.3.tgz#188f3c77f58b354f0ea6250f6756ac1e1a226291" resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-15.2.3.tgz#188f3c77f58b354f0ea6250f6756ac1e1a226291"
integrity sha512-Oz8b8Xsvfj7YcutUBtI84NUIqSnt7iCM5HZ5DyKoWKKiDK/+aUuj3RXNQELG8jUw6pQPgEwgbZ1+J8SdH/y9jw== integrity sha512-Oz8b8Xsvfj7YcutUBtI84NUIqSnt7iCM5HZ5DyKoWKKiDK/+aUuj3RXNQELG8jUw6pQPgEwgbZ1+J8SdH/y9jw==
expo-modules-autolinking@1.1.2: expo-modules-autolinking@1.2.0:
version "1.1.2" version "1.2.0"
resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.1.2.tgz#a81c65c63bd281922410c6d8c3ad6255b6305246" resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.2.0.tgz#3ead115510a43fe196fc0498586b6133bd573209"
integrity sha512-oOlkAccVnHwwR5ccvF/F/x4Omj9HWzSimMUlIVz0SVGdNBEqTPyn0L/d4uIufhyQbEWvrarqL8o5Yz11wEI0SQ== integrity sha512-QOPh/iXykNDCAzUual1imSrn2aDakzCGUp2QmxVREr0llajXygroUWlT9sQXh1zKzbNp+a+i/xK375ZeBFiNJA==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
commander "^7.2.0" commander "^7.2.0"
@ -8467,13 +8468,13 @@ expo-updates@~0.16.4:
fbemitter "^3.0.0" fbemitter "^3.0.0"
resolve-from "^5.0.0" resolve-from "^5.0.0"
expo@~48.0.10: expo@~48.0.11:
version "48.0.10" version "48.0.11"
resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.10.tgz#c1218f6a0ca9ca8209d6f833dc6911743870dfaf" resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.11.tgz#afd43c7a5ddce3d02a3f27263c95f8d01e1fb84d"
integrity sha512-8YXG6um3ld36nu/ONEC0NNkMatdj4k/HwR7OUd3dKUt3PJSkZHsCeLXIu8za7WSWpgPAU/xAj35noPFEFnjO1w== integrity sha512-KX1RCHhdhdT4DjCeRqYJpZXhdCTuqxHHdNIRoFkmCgkUARYlZbB+Y1U8/KMz8fBAlFoEq99cF/KyRr87VAxRCw==
dependencies: dependencies:
"@babel/runtime" "^7.20.0" "@babel/runtime" "^7.20.0"
"@expo/cli" "0.6.2" "@expo/cli" "0.7.0"
"@expo/config" "8.0.2" "@expo/config" "8.0.2"
"@expo/config-plugins" "6.0.1" "@expo/config-plugins" "6.0.1"
"@expo/vector-icons" "^13.0.0" "@expo/vector-icons" "^13.0.0"
@ -8485,7 +8486,7 @@ expo@~48.0.10:
expo-file-system "~15.2.2" expo-file-system "~15.2.2"
expo-font "~11.1.1" expo-font "~11.1.1"
expo-keep-awake "~12.0.1" expo-keep-awake "~12.0.1"
expo-modules-autolinking "1.1.2" expo-modules-autolinking "1.2.0"
expo-modules-core "1.2.6" expo-modules-core "1.2.6"
fbemitter "^3.0.0" fbemitter "^3.0.0"
getenv "^1.0.0" getenv "^1.0.0"
@ -17884,7 +17885,7 @@ ws@^7, ws@^7.0.0, ws@^7.4.6, ws@^7.5.1:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^8.11.0, ws@^8.13.0: ws@^8.11.0, ws@^8.12.1, ws@^8.13.0:
version "8.13.0" version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==