Rework search suggestions for performance (#492)
parent
1ab8f31517
commit
75fd653be3
|
@ -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",
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -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==
|
||||||
|
|
Loading…
Reference in New Issue