Per-user search history (#7588)
* per-user search history * move to mmkv with cool new hook * revert accidental changes This reverts commit 27c89fa645eff0acb7a8fd852203ff1ea3725c69. * restore limits
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
ComAtprotoRepoUploadBlob,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
keepPreviousData,
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
@@ -81,7 +82,13 @@ export function useProfileQuery({
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfilesQuery({handles}: {handles: string[]}) {
|
||||
export function useProfilesQuery({
|
||||
handles,
|
||||
maintainData,
|
||||
}: {
|
||||
handles: string[]
|
||||
maintainData?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
return useQuery({
|
||||
staleTime: STALE.MINUTES.FIVE,
|
||||
@@ -90,6 +97,7 @@ export function useProfilesQuery({handles}: {handles: string[]}) {
|
||||
const res = await agent.getProfiles({actors: handles})
|
||||
return res.data
|
||||
},
|
||||
placeholderData: maintainData ? keepPreviousData : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+69
-3
@@ -1,5 +1,5 @@
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {MMKV} from 'react-native-mmkv'
|
||||
import {Did} from '@atproto/api'
|
||||
|
||||
import {Account, Device} from '#/storage/schema'
|
||||
|
||||
@@ -65,6 +65,72 @@ export class Storage<Scopes extends unknown[], Schema> {
|
||||
removeMany<Key extends keyof Schema>(scopes: [...Scopes], keys: Key[]) {
|
||||
keys.forEach(key => this.remove([...scopes, key]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires a callback when the storage associated with a given key changes
|
||||
*
|
||||
* @returns Listener - call `remove()` to stop listening
|
||||
*/
|
||||
addOnValueChangedListener<Key extends keyof Schema>(
|
||||
scopes: [...Scopes, Key],
|
||||
callback: () => void,
|
||||
) {
|
||||
return this.store.addOnValueChangedListener(key => {
|
||||
if (key === scopes.join(this.sep)) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type StorageSchema<T extends Storage<any, any>> = T extends Storage<
|
||||
any,
|
||||
infer U
|
||||
>
|
||||
? U
|
||||
: never
|
||||
type StorageScopes<T extends Storage<any, any>> = T extends Storage<
|
||||
infer S,
|
||||
any
|
||||
>
|
||||
? S
|
||||
: never
|
||||
|
||||
/**
|
||||
* Hook to use a storage instance. Acts like a useState hook, but persists the
|
||||
* value in storage.
|
||||
*/
|
||||
export function useStorage<
|
||||
Store extends Storage<any, any>,
|
||||
Key extends keyof StorageSchema<Store>,
|
||||
>(
|
||||
storage: Store,
|
||||
scopes: [...StorageScopes<Store>, Key],
|
||||
): [
|
||||
StorageSchema<Store>[Key] | undefined,
|
||||
(data: StorageSchema<Store>[Key]) => void,
|
||||
] {
|
||||
type Schema = StorageSchema<Store>
|
||||
const [value, setValue] = useState<Schema[Key] | undefined>(() =>
|
||||
storage.get(scopes),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = storage.addOnValueChangedListener(scopes, () => {
|
||||
setValue(storage.get(scopes))
|
||||
})
|
||||
return () => sub.remove()
|
||||
}, [storage, scopes])
|
||||
|
||||
const setter = useCallback(
|
||||
(data: Schema[Key]) => {
|
||||
setValue(data)
|
||||
storage.set(scopes, data)
|
||||
},
|
||||
[storage, scopes],
|
||||
)
|
||||
|
||||
return [value, setter] as const
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,10 +143,10 @@ export const device = new Storage<[], Device>({id: 'bsky_device'})
|
||||
/**
|
||||
* Account data that's specific to the account on this device
|
||||
*/
|
||||
export const account = new Storage<[Did], Account>({id: 'bsky_account'})
|
||||
export const account = new Storage<[string], Account>({id: 'bsky_account'})
|
||||
|
||||
if (__DEV__ && typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - dev global
|
||||
window.bsky_storage = {
|
||||
device,
|
||||
account,
|
||||
|
||||
@@ -13,4 +13,5 @@ export type Device = {
|
||||
|
||||
export type Account = {
|
||||
searchTermHistory?: string[]
|
||||
searchAccountHistory?: string[]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {augmentSearchQuery} from '#/lib/strings/helpers'
|
||||
import {languageName} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {useLanguagePrefs} from '#/state/preferences/languages'
|
||||
@@ -45,6 +43,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {useActorSearch} from '#/state/queries/actor-search'
|
||||
import {usePopularFeedsSearch} from '#/state/queries/feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSearchPostsQuery} from '#/state/queries/search-posts'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
@@ -72,6 +71,7 @@ import {SearchInput} from '#/components/forms/SearchInput'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {account, useStorage} from '#/storage'
|
||||
|
||||
function Loader() {
|
||||
return (
|
||||
@@ -604,6 +604,7 @@ export function SearchScreen(
|
||||
const {_} = useLingui()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
// Query terms
|
||||
const queryParam = props.route?.params?.q ?? ''
|
||||
@@ -612,10 +613,55 @@ export function SearchScreen(
|
||||
useActorAutocompleteQuery(searchText, true)
|
||||
|
||||
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
|
||||
const [searchHistory, setSearchHistory] = React.useState<string[]>([])
|
||||
const [selectedProfiles, setSelectedProfiles] = React.useState<
|
||||
AppBskyActorDefs.ProfileViewBasic[]
|
||||
>([])
|
||||
|
||||
const [termHistory = [], setTermHistory] = useStorage(account, [
|
||||
currentAccount?.did ?? 'pwi',
|
||||
'searchTermHistory',
|
||||
] as const)
|
||||
const [accountHistory = [], setAccountHistory] = useStorage(account, [
|
||||
currentAccount?.did ?? 'pwi',
|
||||
'searchAccountHistory',
|
||||
])
|
||||
|
||||
const {data: accountHistoryProfiles} = useProfilesQuery({
|
||||
handles: accountHistory,
|
||||
maintainData: true,
|
||||
})
|
||||
|
||||
const updateSearchHistory = useCallback(
|
||||
async (item: string) => {
|
||||
const newSearchHistory = [
|
||||
item,
|
||||
...termHistory.filter(search => search !== item),
|
||||
].slice(0, 6)
|
||||
setTermHistory(newSearchHistory)
|
||||
},
|
||||
[termHistory, setTermHistory],
|
||||
)
|
||||
|
||||
const updateProfileHistory = useCallback(
|
||||
async (item: AppBskyActorDefs.ProfileViewBasic) => {
|
||||
const newAccountHistory = [
|
||||
item.did,
|
||||
...accountHistory.filter(p => p !== item.did),
|
||||
].slice(0, 5)
|
||||
setAccountHistory(newAccountHistory)
|
||||
},
|
||||
[accountHistory, setAccountHistory],
|
||||
)
|
||||
|
||||
const deleteSearchHistoryItem = useCallback(
|
||||
async (item: string) => {
|
||||
setTermHistory(termHistory.filter(search => search !== item))
|
||||
},
|
||||
[termHistory, setTermHistory],
|
||||
)
|
||||
const deleteProfileHistoryItem = useCallback(
|
||||
async (item: AppBskyActorDefs.ProfileViewBasic) => {
|
||||
setAccountHistory(accountHistory.filter(p => p !== item.did))
|
||||
},
|
||||
[accountHistory, setAccountHistory],
|
||||
)
|
||||
|
||||
const {params, query, queryWithParams} = useQueryManager({
|
||||
initialQuery: queryParam,
|
||||
@@ -635,25 +681,6 @@ export function SearchScreen(
|
||||
}),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const loadSearchHistory = async () => {
|
||||
try {
|
||||
const history = await AsyncStorage.getItem('searchHistory')
|
||||
if (history !== null) {
|
||||
setSearchHistory(JSON.parse(history))
|
||||
}
|
||||
const profiles = await AsyncStorage.getItem('selectedProfiles')
|
||||
if (profiles !== null) {
|
||||
setSelectedProfiles(JSON.parse(profiles))
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to load search history', {message: e})
|
||||
}
|
||||
}
|
||||
|
||||
loadSearchHistory()
|
||||
}, [])
|
||||
|
||||
const onPressMenu = React.useCallback(() => {
|
||||
textInput.current?.blur()
|
||||
setDrawerOpen(true)
|
||||
@@ -670,57 +697,6 @@ export function SearchScreen(
|
||||
setSearchText(text)
|
||||
}, [])
|
||||
|
||||
const updateSearchHistory = React.useCallback(
|
||||
async (newQuery: string) => {
|
||||
newQuery = newQuery.trim()
|
||||
if (newQuery) {
|
||||
let newHistory = [
|
||||
newQuery,
|
||||
...searchHistory.filter(q => q !== newQuery),
|
||||
]
|
||||
|
||||
if (newHistory.length > 5) {
|
||||
newHistory = newHistory.slice(0, 5)
|
||||
}
|
||||
|
||||
setSearchHistory(newHistory)
|
||||
try {
|
||||
await AsyncStorage.setItem(
|
||||
'searchHistory',
|
||||
JSON.stringify(newHistory),
|
||||
)
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to save search history', {message: e})
|
||||
}
|
||||
}
|
||||
},
|
||||
[searchHistory, setSearchHistory],
|
||||
)
|
||||
|
||||
const updateSelectedProfiles = React.useCallback(
|
||||
async (profile: AppBskyActorDefs.ProfileViewBasic) => {
|
||||
let newProfiles = [
|
||||
profile,
|
||||
...selectedProfiles.filter(p => p.did !== profile.did),
|
||||
]
|
||||
|
||||
if (newProfiles.length > 5) {
|
||||
newProfiles = newProfiles.slice(0, 5)
|
||||
}
|
||||
|
||||
setSelectedProfiles(newProfiles)
|
||||
try {
|
||||
await AsyncStorage.setItem(
|
||||
'selectedProfiles',
|
||||
JSON.stringify(newProfiles),
|
||||
)
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to save selected profiles', {message: e})
|
||||
}
|
||||
},
|
||||
[selectedProfiles, setSelectedProfiles],
|
||||
)
|
||||
|
||||
const navigateToItem = React.useCallback(
|
||||
(item: string) => {
|
||||
scrollToTopWeb()
|
||||
@@ -774,10 +750,10 @@ export function SearchScreen(
|
||||
(profile: AppBskyActorDefs.ProfileViewBasic) => {
|
||||
// Slight delay to avoid updating during push nav animation.
|
||||
setTimeout(() => {
|
||||
updateSelectedProfiles(profile)
|
||||
updateProfileHistory(profile)
|
||||
}, 400)
|
||||
},
|
||||
[updateSelectedProfiles],
|
||||
[updateProfileHistory],
|
||||
)
|
||||
|
||||
const onSoftReset = React.useCallback(() => {
|
||||
@@ -798,36 +774,6 @@ export function SearchScreen(
|
||||
}, [onSoftReset, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const handleRemoveHistoryItem = React.useCallback(
|
||||
(itemToRemove: string) => {
|
||||
const updatedHistory = searchHistory.filter(item => item !== itemToRemove)
|
||||
setSearchHistory(updatedHistory)
|
||||
AsyncStorage.setItem(
|
||||
'searchHistory',
|
||||
JSON.stringify(updatedHistory),
|
||||
).catch(e => {
|
||||
logger.error('Failed to update search history', {message: e})
|
||||
})
|
||||
},
|
||||
[searchHistory],
|
||||
)
|
||||
|
||||
const handleRemoveProfile = React.useCallback(
|
||||
(profileToRemove: AppBskyActorDefs.ProfileViewBasic) => {
|
||||
const updatedProfiles = selectedProfiles.filter(
|
||||
profile => profile.did !== profileToRemove.did,
|
||||
)
|
||||
setSelectedProfiles(updatedProfiles)
|
||||
AsyncStorage.setItem(
|
||||
'selectedProfiles',
|
||||
JSON.stringify(updatedProfiles),
|
||||
).catch(e => {
|
||||
logger.error('Failed to update selected profiles', {message: e})
|
||||
})
|
||||
},
|
||||
[selectedProfiles],
|
||||
)
|
||||
|
||||
const onSearchInputFocus = React.useCallback(() => {
|
||||
if (isWeb) {
|
||||
// Prevent a jump on iPad by ensuring that
|
||||
@@ -932,12 +878,12 @@ export function SearchScreen(
|
||||
/>
|
||||
) : (
|
||||
<SearchHistory
|
||||
searchHistory={searchHistory}
|
||||
selectedProfiles={selectedProfiles}
|
||||
searchHistory={termHistory}
|
||||
selectedProfiles={accountHistoryProfiles?.profiles || []}
|
||||
onItemClick={handleHistoryItemClick}
|
||||
onProfileClick={handleProfileClick}
|
||||
onRemoveItemClick={handleRemoveHistoryItem}
|
||||
onRemoveProfileClick={handleRemoveProfile}
|
||||
onRemoveItemClick={deleteSearchHistoryItem}
|
||||
onRemoveProfileClick={deleteProfileHistoryItem}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user