diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index dcbf1b4457..9afe9581bc 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -499,6 +499,9 @@ const schema = yup NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), NEXT_PUBLIC_UNIVERSAL_PROFILES_API_URL: yup.string(), + NEXT_PUBLIC_ALGOLIA_APP_ID: yup.string(), + NEXT_PUBLIC_ALGOLIA_API_KEY: yup.string(), + NEXT_PUBLIC_ALGOLIA_INDEX_NAME: yup.string(), // Misc NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(), diff --git a/docs/ENVS.md b/docs/ENVS.md index 4fe3a4440d..af06e56d05 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -192,11 +192,14 @@ Settings for meta tags and OG tags #### Address views -| Variable | Type | Description | Compulsoriness | Default value | Example value | -|---------------------------------------------|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|------------------------------------| -| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie" \| "universal_profile\|[identicon]"` | Type of identicon displayed next to addresses. In case of universal_profile you can provide a second identicon type that will be displayed when no universal profile is found | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar), [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) and [LUKSO Universal Profile](https://universalprofile.cloud/) | - | `jazzicon` | `gradient_avatar` | -| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | -| NEXT_PUBLIC_UNIVERSAL_PROFILES_API_URL | `string` | LUKSO UNIVERSAL PROFILE API URL used for getting various universal profile data | - | - | `https://api.universalprofile.com` | +| Variable | Type | Description | Compulsoriness | Default value | Example value | +|------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|------------------------------------| +| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie" \| "universal_profile\|[identicon]"` | Type of identicon displayed next to addresses. In case of universal_profile you can provide a second identicon type that will be displayed when no universal profile is found | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar), [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) and [LUKSO Universal Profile](https://universalprofile.cloud/) | - | `jazzicon` | `gradient_avatar` | +| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | +| NEXT_PUBLIC_UNIVERSAL_PROFILES_API_URL | `string` | LUKSO UNIVERSAL PROFILE API URL used for getting various universal profile data | - | - | `https://api.universalprofile.com` | +| NEXT_PUBLIC_ALGOLIA_APP_ID | `string` | Algolia App's ID | - | - | `ABCD1234` | +| NEXT_PUBLIC_ALGOLIA_API_KEY | `string` | Algolia API key used for authenticating requests | - | - | `abcd1234defg5678` | +| NEXT_PUBLIC_ALGOLIA_INDEX_NAME | `string` | Index name from where data should be requested | - | - | `prod_mainnet_data` | ##### Address views list | Id | Description | diff --git a/lib/api/buildUniversalProfileUrl.ts b/lib/api/buildUniversalProfileUrl.ts new file mode 100644 index 0000000000..690da58e06 --- /dev/null +++ b/lib/api/buildUniversalProfileUrl.ts @@ -0,0 +1,11 @@ +import algoliasearch from 'algoliasearch'; + +import { getEnvValue } from '../../configs/app/utils'; + +const algolia = { + appId: getEnvValue('NEXT_PUBLIC_ALGOLIA_APP_ID') || '', + apiKey: getEnvValue('NEXT_PUBLIC_ALGOLIA_API_KEY') || '', + index: getEnvValue('NEXT_PUBLIC_ALGOLIA_INDEX_NAME') || '', +}; + +export const algoliaIndex = algoliasearch(algolia.appId, algolia.apiKey).initIndex(algolia.index); diff --git a/lib/api/isUniversalProfileEnabled.ts b/lib/api/isUniversalProfileEnabled.ts new file mode 100644 index 0000000000..553a0ac2e0 --- /dev/null +++ b/lib/api/isUniversalProfileEnabled.ts @@ -0,0 +1,6 @@ +import { getEnvValue } from '../../configs/app/utils'; + +export const isUniversalProfileEnabled = () => { + const env = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE'); + return env === undefined ? false : env.includes('universal_profile'); +}; diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 795d97b0e1..5843dc3be7 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -518,6 +518,9 @@ export const RESOURCES = { api_v2_key: { path: '/api/v2/key', }, + universal_profile: { + path: '', + }, // API V1 csv_export_txs: { @@ -635,6 +638,7 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_withdrawals' ? AddressWithdrawalsResponse : +Q extends 'universal_profile' ? Array : Q extends 'token' ? TokenInfo : Q extends 'token_verified_info' ? TokenVerifiedInfo : Q extends 'token_counters' ? TokenCounters : diff --git a/lib/api/useUniversalProfileApiFetch.ts b/lib/api/useUniversalProfileApiFetch.ts new file mode 100644 index 0000000000..e4a8041fee --- /dev/null +++ b/lib/api/useUniversalProfileApiFetch.ts @@ -0,0 +1,35 @@ +import React from 'react'; + +import type { SearchResultAddressOrContractOrUniversalProfile } from '../../types/api/search'; +import type { UniversalProfileProxyResponse } from '../../types/api/universalProfile'; + +import type { Params as FetchParams } from 'lib/hooks/useFetch'; + +import { algoliaIndex } from './buildUniversalProfileUrl'; +import type { ResourceName, ResourcePathParams } from './resources'; + +export interface Params { + pathParams?: ResourcePathParams; + queryParams?: Record | number | undefined>; + fetchParams?: Pick; +} + +export default function useUniversalProfileApiFetch() { + return React.useCallback(async(queryParams: string, + ) => { + try { + const { hits } = await algoliaIndex.search(queryParams); + return hits.map((hit) => { + const hitAsUp = hit as unknown as UniversalProfileProxyResponse; + return { + type: 'universal_profile', + name: hitAsUp.hasProfileName ? hitAsUp.LSP3Profile.name : null, + address: hit.objectID, + is_smart_contract_verified: false, + }; + }); + } catch (error) { + return error; + } + }, []); +} diff --git a/lib/api/useUniversalProfileQuery.ts b/lib/api/useUniversalProfileQuery.ts new file mode 100644 index 0000000000..93d3d3da4d --- /dev/null +++ b/lib/api/useUniversalProfileQuery.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import type { ResourceError, ResourceName, ResourcePayload } from './resources'; +import type { Params } from './useApiQuery'; +import { getResourceKey } from './useApiQuery'; +import useUniversalProfileApiFetch from './useUniversalProfileApiFetch'; + +export default function useUniversalProfileQuery( + resource: R, + { queryOptions, pathParams, queryParams }: Params = {}, +) { + const upFetch = useUniversalProfileApiFetch(); + return useQuery, ResourceError, ResourcePayload>({ + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: getResourceKey(resource, { pathParams, queryParams }), + queryFn: async() => { + return await upFetch(queryParams?.q as string) as Promise>; + }, + ...queryOptions, + }); +} diff --git a/mocks/search/index.ts b/mocks/search/index.ts index 0fbdf9e946..9f8907fa71 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -1,4 +1,11 @@ -import type { SearchResultToken, SearchResultBlock, SearchResultAddressOrContract, SearchResultTx, SearchResultLabel, SearchResult } from 'types/api/search'; +import type { + SearchResultToken, + SearchResultBlock, + SearchResultAddressOrContractOrUniversalProfile, + SearchResultTx, + SearchResultLabel, + SearchResult, +} from 'types/api/search'; export const token1: SearchResultToken = { address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B', @@ -47,7 +54,7 @@ export const block2: SearchResultBlock = { url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', }; -export const address1: SearchResultAddressOrContract = { +export const address1: SearchResultAddressOrContractOrUniversalProfile = { address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', name: null, type: 'address' as const, @@ -55,7 +62,7 @@ export const address1: SearchResultAddressOrContract = { url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', }; -export const contract1: SearchResultAddressOrContract = { +export const contract1: SearchResultAddressOrContractOrUniversalProfile = { address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', name: 'Unknown contract in this network', type: 'contract' as const, diff --git a/nextjs/csp/policies/universalprofile.ts b/nextjs/csp/policies/universalprofile.ts index 1ab96498e8..886889d327 100644 --- a/nextjs/csp/policies/universalprofile.ts +++ b/nextjs/csp/policies/universalprofile.ts @@ -4,6 +4,8 @@ export function universalProfile(): CspDev.DirectiveDescriptor { return { 'connect-src': [ 'api.universalprofile.cloud', + '*.algolianet.com', + '*.algolia.net', ], }; } diff --git a/types/api/search.ts b/types/api/search.ts index 3d2ad1d2c5..46467c3e39 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -17,8 +17,8 @@ export interface SearchResultToken { is_smart_contract_verified: boolean; } -export interface SearchResultAddressOrContract { - type: 'address' | 'contract'; +export interface SearchResultAddressOrContractOrUniversalProfile { + type: 'address' | 'contract' | 'universal_profile'; name: string | null; address: string; is_smart_contract_verified: boolean; @@ -49,7 +49,12 @@ export interface SearchResultTx { url?: string; // not used by the frontend, we build the url ourselves } -export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel; +export type SearchResultItem = + SearchResultToken | + SearchResultAddressOrContractOrUniversalProfile | + SearchResultBlock | + SearchResultTx | + SearchResultLabel export interface SearchResult { items: Array; diff --git a/types/api/universalProfile.ts b/types/api/universalProfile.ts index 5173462685..9e5419567d 100644 --- a/types/api/universalProfile.ts +++ b/types/api/universalProfile.ts @@ -1,4 +1,18 @@ -export type UPResponse = { +export type UniversalProfileProxyResponse = { + type: string; + hasProfileName: boolean; + hasProfileImage: boolean; + LSP3Profile: { + name: string; + profileImage: { + [key: number]: { + url: string; + }; + }; + }; +} + +export type UniversalProfileAlgoliaResponse = { type: string; hasProfileName: boolean; hasProfileImage: boolean; diff --git a/ui/address/contract/ContractSourceCode.tsx b/ui/address/contract/ContractSourceCode.tsx index 627969c638..1130b47a2b 100644 --- a/ui/address/contract/ContractSourceCode.tsx +++ b/ui/address/contract/ContractSourceCode.tsx @@ -113,6 +113,8 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { ) : null; const copyToClipboard = activeContractData?.length === 1 ? + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore : null; diff --git a/ui/shared/HashStringShorten.tsx b/ui/shared/HashStringShorten.tsx index 2ec0727c8a..b87631affb 100644 --- a/ui/shared/HashStringShorten.tsx +++ b/ui/shared/HashStringShorten.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import shortenString from 'lib/shortenString'; -import { getEnvValue } from '../../configs/app/utils'; +import { isUniversalProfileEnabled } from '../../lib/api/isUniversalProfileEnabled'; import shortenUniversalProfile from '../../lib/shortenUniversalProfile'; interface Props { @@ -16,11 +16,7 @@ interface Props { const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span' }: Props) => { const [ shortenedString, setShortenedString ] = useState(shortenString(hash)); useEffect(() => { - const identiconType = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE'); - if (identiconType === undefined) { - return undefined; - } - if (identiconType.includes('universal_profile') && hash.includes(' (')) { + if (isUniversalProfileEnabled() && hash.includes(' (')) { setShortenedString(shortenUniversalProfile(hash)); } }, [ hash ]); diff --git a/ui/shared/HashStringShortenDynamic.tsx b/ui/shared/HashStringShortenDynamic.tsx index f916290b39..146891a5c6 100644 --- a/ui/shared/HashStringShortenDynamic.tsx +++ b/ui/shared/HashStringShortenDynamic.tsx @@ -17,7 +17,7 @@ import useFontFaceObserver from 'use-font-face-observer'; import { BODY_TYPEFACE, HEADING_TYPEFACE } from 'theme/foundations/typography'; -import { getEnvValue } from '../../configs/app/utils'; +import { isUniversalProfileEnabled } from '../../lib/api/isUniversalProfileEnabled'; const TAIL_LENGTH = 4; const HEAD_MIN_LENGTH = 4; @@ -69,21 +69,16 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled, } // if we get #, this means that we got a valid universal profile in format of @name#0x1234 - we can split this data and return username component. - const identiconType = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE'); - if (identiconType === undefined) { - return; - } - if (identiconType.includes('universal_profile') && hash.includes(' (')) { + if (isUniversalProfileEnabled() && hash.includes(' (')) { const upParts = hash.split(' ('); const hashHead = '#' + upParts[1].slice(2, 6); // change (0x1234...5678) -> #1234 const name = upParts[0]; const slicedName = name.slice(0, rightI - 3); const displayed = rightI - 3 > name.length ? name + hashHead : slicedName + '...' + hashHead; setDisplayedString(displayed); - - return; + } else { + setDisplayedString(hash.slice(0, rightI - 1) + '...' + tail); } - setDisplayedString(hash.slice(0, rightI - 1) + '...' + tail); } else { setDisplayedString(hash); } diff --git a/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx b/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx index 0625be1d54..bbe268b0d7 100644 --- a/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx +++ b/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx @@ -3,28 +3,25 @@ import type { QueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query'; import React, { useEffect, useState } from 'react'; -import type { UPResponse } from '../../../../types/api/universalProfile'; +import type { UniversalProfileProxyResponse } from '../../../../types/api/universalProfile'; import { getEnvValue } from '../../../../configs/app/utils'; +import { isUniversalProfileEnabled } from '../../../../lib/api/isUniversalProfileEnabled'; interface Props { address: string; fallbackIcon: JSX.Element; } -export const formattedLuksoName = (hash: string, name: string) => { +export const formattedLuksoName = (hash: string, name: string | null) => { return `@${ name } (${ hash })`; }; export const getUniversalProfile = async(address: string, queryClient: QueryClient) => { - const identiconType = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE'); - if (identiconType === undefined) { + if (!isUniversalProfileEnabled()) { return undefined; } - if (!identiconType.includes('universal_profile')) { - return undefined; - } - const query = queryClient.getQueryData([ 'universalProfile', { address: address } ]); + const query = queryClient.getQueryData([ 'universalProfile', { address: address } ]); if (query !== undefined) { return query; } @@ -39,7 +36,7 @@ export const getUniversalProfile = async(address: string, queryClient: QueryClie try { const resp = await fetch(url); const json = await resp.json(); - return json as UPResponse; + return json as UniversalProfileProxyResponse; } catch (err) { return undefined; } @@ -48,7 +45,7 @@ export const getUniversalProfile = async(address: string, queryClient: QueryClie }; export const IdenticonUniversalProfile: React.FC = ({ address, fallbackIcon }) => { - const [ up, setUp ] = useState({} as UPResponse); + const [ up, setUp ] = useState({} as UniversalProfileProxyResponse); const queryClient = useQueryClient(); useEffect(() => { (async() => { @@ -59,7 +56,7 @@ export const IdenticonUniversalProfile: React.FC = ({ address, fallbackIc return; } })(); - }, [ address, up, setUp, queryClient ]); + }, [ address, setUp, queryClient ]); if (up === undefined || up.LSP3Profile === undefined) { return fallbackIcon; diff --git a/ui/shared/search/utils.ts b/ui/shared/search/utils.ts index dd900e60c3..9d83e4cb03 100644 --- a/ui/shared/search/utils.ts +++ b/ui/shared/search/utils.ts @@ -1,7 +1,7 @@ import type { SearchResultItem } from 'types/api/search'; import type { MarketplaceAppOverview } from 'types/client/marketplace'; -export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block'; +export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block' | 'universal_profile'; export type Category = ApiCategory | 'app'; export type ItemsCategoriesMap = @@ -21,6 +21,7 @@ export const searchCategories: Array<{id: Category; title: string }> = [ { id: 'public_tag', title: 'Public tags' }, { id: 'transaction', title: 'Transactions' }, { id: 'block', title: 'Blocks' }, + { id: 'universal_profile', title: 'Universal Profiles' }, ]; export const searchItemTitles: Record = { @@ -31,6 +32,7 @@ export const searchItemTitles: Record, ResourceError>; + query: UseQueryResult, Error>; searchTerm: string; onItemClick: (event: React.MouseEvent) => void; containerId: string; @@ -142,6 +141,8 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props { cat.title } { cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) => + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore , ) } { cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) => diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx index e0b34e6a04..36627d12a6 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -1,45 +1,30 @@ import { Box, Text, Flex } from '@chakra-ui/react'; -import { useQueryClient } from '@tanstack/react-query'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; -import type { SearchResultAddressOrContract } from 'types/api/search'; +import type { SearchResultAddressOrContractOrUniversalProfile } from 'types/api/search'; import highlightText from 'lib/highlightText'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; -import { formattedLuksoName, getUniversalProfile } from '../../../shared/entities/address/IdenticonUniversalProfileQuery'; +import { formattedLuksoName } from '../../../shared/entities/address/IdenticonUniversalProfileQuery'; interface Props { - data: SearchResultAddressOrContract; + data: SearchResultAddressOrContractOrUniversalProfile; isMobile: boolean | undefined; searchTerm: string; } const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { - const queryClient = useQueryClient(); - const [ type, setType ] = useState(data.type); - const [ displayedName, setDisplayedName ] = useState(data.address); - - useEffect(() => { // this causes a sort of loading state where the address suddenly switches to up name - needs fix? - (async() => { - const upData = await getUniversalProfile(data.address, queryClient); - if (upData === undefined) { - return; - } - - if (upData.LSP3Profile !== undefined) { - setType('contract'); // when the type is contract the icon will know that it needs to get UP profile picture - if (upData.hasProfileImage) { - setDisplayedName(formattedLuksoName(data.address, upData.LSP3Profile.name)); - } - } - })(); - }, [ data, queryClient, setType, setDisplayedName ]); - const icon = ( ); const name = data.name && ( @@ -49,11 +34,12 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { whiteSpace="nowrap" textOverflow="ellipsis" > - + ); - const address = ; + const dynamicTitle = data.type === 'universal_profile' ? formattedLuksoName(data.address, data.name) : data.address; + const address = ; if (isMobile) { return ( diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx index 530980cdc2..107d63a416 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx @@ -29,6 +29,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => } case 'contract': case 'address': + case 'universal_profile': case 'label': { return route({ pathname: '/address/[hash]', query: { hash: data.address } }); } @@ -47,6 +48,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => return ; } case 'contract': + case 'universal_profile': case 'address': { return ; } diff --git a/ui/snippets/searchBar/useQuickSearchQuery.tsx b/ui/snippets/searchBar/useQuickSearchQuery.tsx index e39953b9ff..6c4ea806c2 100644 --- a/ui/snippets/searchBar/useQuickSearchQuery.tsx +++ b/ui/snippets/searchBar/useQuickSearchQuery.tsx @@ -1,9 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React from 'react'; +import type { SearchResultItem } from '../../../types/api/search'; + import useApiQuery from 'lib/api/useApiQuery'; import useDebounce from 'lib/hooks/useDebounce'; +import useUniversalProfileQuery from '../../../lib/api/useUniversalProfileQuery'; + export default function useQuickSearchQuery() { const router = useRouter(); @@ -12,7 +17,7 @@ export default function useQuickSearchQuery() { const debouncedSearchTerm = useDebounce(searchTerm, 300); const pathname = router.pathname; - const query = useApiQuery('quick_search', { + const quickSearchQuery = useApiQuery('quick_search', { queryParams: { q: debouncedSearchTerm }, queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 }, }); @@ -24,6 +29,21 @@ export default function useQuickSearchQuery() { queryOptions: { enabled: Boolean(debouncedSearchTerm) }, }); + const upQuery = useUniversalProfileQuery('universal_profile', { + queryParams: { q: debouncedSearchTerm }, + queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 }, + }); + + const query = useQuery({ + queryKey: [ 'merged_query', quickSearchQuery, upQuery ], + queryFn: () => { + const q1 = quickSearchQuery.data as Array; + const q2 = upQuery.data as Array; + + return [ ...q1, ...q2 ]; + }, + }); + return React.useMemo(() => ({ searchTerm, debouncedSearchTerm, @@ -31,5 +51,5 @@ export default function useQuickSearchQuery() { query, redirectCheckQuery, pathname, - }), [ debouncedSearchTerm, pathname, query, redirectCheckQuery, searchTerm ]); + }), [ debouncedSearchTerm, pathname, redirectCheckQuery, searchTerm, query ]); } diff --git a/yarn.lock b/yarn.lock index f0108d4301..b0afbd05db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5522,7 +5522,7 @@ ajv@^6.10.0, ajv@^6.12.4: algoliasearch@^4.20.0: version "4.20.0" - resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.20.0.tgz" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.20.0.tgz#700c2cb66e14f8a288460036c7b2a554d0d93cf4" integrity sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g== dependencies: "@algolia/cache-browser-local-storage" "4.20.0"