Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: search ups by name #10

Merged
merged 9 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/api/buildUniversalProfileUrl.ts
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 6 additions & 0 deletions lib/api/isUniversalProfileEnabled.ts
Original file line number Diff line number Diff line change
@@ -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');
};
4 changes: 4 additions & 0 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ export const RESOURCES = {
api_v2_key: {
path: '/api/v2/key',
},
universal_profile: {
path: '',
},

// API V1
csv_export_txs: {
Expand Down Expand Up @@ -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<SearchResultItem> :
Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo :
Q extends 'token_counters' ? TokenCounters :
Expand Down
35 changes: 35 additions & 0 deletions lib/api/useUniversalProfileApiFetch.ts
Original file line number Diff line number Diff line change
@@ -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<R extends ResourceName> {
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
}

export default function useUniversalProfileApiFetch() {
return React.useCallback(async(queryParams: string,
) => {
try {
const { hits } = await algoliaIndex.search(queryParams);
return hits.map<SearchResultAddressOrContractOrUniversalProfile>((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;
}
}, []);
}
21 changes: 21 additions & 0 deletions lib/api/useUniversalProfileQuery.ts
Original file line number Diff line number Diff line change
@@ -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<R extends ResourceName, E = unknown>(
resource: R,
{ queryOptions, pathParams, queryParams }: Params<R, E> = {},
) {
const upFetch = useUniversalProfileApiFetch();
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => {
return await upFetch(queryParams?.q as string) as Promise<ResourcePayload<R>>;
},
...queryOptions,
});
}
13 changes: 10 additions & 3 deletions mocks/search/index.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -47,15 +54,15 @@ export const block2: SearchResultBlock = {
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2',
};

export const address1: SearchResultAddressOrContract = {
export const address1: SearchResultAddressOrContractOrUniversalProfile = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null,
type: 'address' as const,
is_smart_contract_verified: false,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};

export const contract1: SearchResultAddressOrContract = {
export const contract1: SearchResultAddressOrContractOrUniversalProfile = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Unknown contract in this network',
type: 'contract' as const,
Expand Down
2 changes: 2 additions & 0 deletions nextjs/csp/policies/universalprofile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export function universalProfile(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'api.universalprofile.cloud',
'*.algolianet.com',
'*.algolia.net',
],
};
}
11 changes: 8 additions & 3 deletions types/api/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SearchResultItem>;
Expand Down
16 changes: 15 additions & 1 deletion types/api/universalProfile.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 2 additions & 0 deletions ui/address/contract/ContractSourceCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}/> :
null;

Expand Down
8 changes: 2 additions & 6 deletions ui/shared/HashStringShorten.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 ]);
Expand Down
13 changes: 4 additions & 9 deletions ui/shared/HashStringShortenDynamic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
19 changes: 8 additions & 11 deletions ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UPResponse>([ 'universalProfile', { address: address } ]);
const query = queryClient.getQueryData<UniversalProfileProxyResponse>([ 'universalProfile', { address: address } ]);
if (query !== undefined) {
return query;
}
Expand All @@ -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;
}
Expand All @@ -48,7 +45,7 @@ export const getUniversalProfile = async(address: string, queryClient: QueryClie
};

export const IdenticonUniversalProfile: React.FC<Props> = ({ address, fallbackIcon }) => {
const [ up, setUp ] = useState({} as UPResponse);
const [ up, setUp ] = useState({} as UniversalProfileProxyResponse);
const queryClient = useQueryClient();
useEffect(() => {
(async() => {
Expand All @@ -59,7 +56,7 @@ export const IdenticonUniversalProfile: React.FC<Props> = ({ address, fallbackIc
return;
}
})();
}, [ address, up, setUp, queryClient ]);
}, [ address, setUp, queryClient ]);

if (up === undefined || up.LSP3Profile === undefined) {
return fallbackIcon;
Expand Down
7 changes: 6 additions & 1 deletion ui/shared/search/utils.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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<Category, { itemTitle: string; itemTitleShort: string }> = {
Expand All @@ -31,6 +32,7 @@ export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleSh
public_tag: { itemTitle: 'Public tag', itemTitleShort: 'Tag' },
transaction: { itemTitle: 'Transaction', itemTitleShort: 'Txn' },
block: { itemTitle: 'Block', itemTitleShort: 'Block' },
universal_profile: { itemTitle: 'Universal Profile', itemTitleShort: 'UP' },
};

export function getItemCategory(item: SearchResultItem | SearchResultAppItem): Category | undefined {
Expand All @@ -57,5 +59,8 @@ export function getItemCategory(item: SearchResultItem | SearchResultAppItem): C
case 'app': {
return 'app';
}
case 'universal_profile': {
return 'universal_profile';
}
}
}
5 changes: 3 additions & 2 deletions ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { scroller, Element } from 'react-scroll';

import type { SearchResultItem } from 'types/api/search';

import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
import TextAd from 'ui/shared/ad/TextAd';
Expand All @@ -18,7 +17,7 @@ import SearchBarSuggestApp from './SearchBarSuggestApp';
import SearchBarSuggestItem from './SearchBarSuggestItem';

interface Props {
query: UseQueryResult<Array<SearchResultItem>, ResourceError<unknown>>;
query: UseQueryResult<Array<SearchResultItem>, Error>;
searchTerm: string;
onItemClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
containerId: string;
Expand Down Expand Up @@ -142,6 +141,8 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
{ cat.title }
</Text>
{ cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>,
) }
{ cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) =>
Expand Down
Loading
Loading