diff --git a/.env.example b/.env.example index 84e0dad688..34a9c98f4c 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,5 @@ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx -NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx \ No newline at end of file +NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx +NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY=xxx \ No newline at end of file diff --git a/configs/app/ui/views/address.ts b/configs/app/ui/views/address.ts new file mode 100644 index 0000000000..ee6089a274 --- /dev/null +++ b/configs/app/ui/views/address.ts @@ -0,0 +1,16 @@ +import type { IdenticonType } from 'types/views/address'; +import { IDENTICON_TYPES } from 'types/views/address'; + +import { getEnvValue } from 'configs/app/utils'; + +const identiconType: IdenticonType = (() => { + const value = getEnvValue(process.env.NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE); + + return IDENTICON_TYPES.find((type) => value === type) || 'jazzicon'; +})(); + +const config = Object.freeze({ + identiconType: identiconType, +}); + +export default config; diff --git a/configs/app/ui/views/index.ts b/configs/app/ui/views/index.ts index 8f7135cbbe..9933720d91 100644 --- a/configs/app/ui/views/index.ts +++ b/configs/app/ui/views/index.ts @@ -1 +1,2 @@ export { default as block } from './block'; +export { default as address } from './address'; diff --git a/configs/envs/.env.main.L2 b/configs/envs/.env.main.L2 index 7c96caa9f9..000ab742fc 100644 --- a/configs/envs/.env.main.L2 +++ b/configs/envs/.env.main.L2 @@ -31,6 +31,8 @@ NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg ## footer ## misc +## views +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar # app features NEXT_PUBLIC_APP_INSTANCE=local diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 856ba853a9..db477a77d1 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -10,6 +10,7 @@ import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; import type { ChainIndicatorId } from '../../../types/homepage'; import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; +import { IDENTICON_TYPES } from '../../../types/views/address'; import { BLOCK_FIELDS_IDS } from '../../../types/views/block'; import type { BlockFieldId } from '../../../types/views/block'; @@ -294,6 +295,7 @@ const schema = yup .transform(getEnvValue) .json() .of(yup.string().oneOf(BLOCK_FIELDS_IDS)), + NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: yup.string().oneOf(IDENTICON_TYPES), // e. misc NEXT_PUBLIC_NETWORK_EXPLORERS: yup @@ -329,6 +331,7 @@ const schema = yup NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), + NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY: yup.string(), }) .concat(accountSchema) .concat(adsBannerSchema) diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index 3a7540f653..3c21b8b3d8 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -130,3 +130,5 @@ frontend: _default: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY NEXT_PUBLIC_WEB3_WALLETS: _default: "['token_pocket','coinbase','metamask']" + NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: + _default: gradient_avatar diff --git a/docs/ENVS.md b/docs/ENVS.md index 55f37c186e..87c5051b62 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -163,6 +163,14 @@ By default, the app has generic favicon. You can override this behavior by provi   +#### Address views + +| Variable | Type | Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | 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) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | + +  + ### Misc | Variable | Type| Description | Compulsoriness | Default value | Example value | diff --git a/package.json b/package.json index 84fd0b434a..c916bfc822 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "d3": "^7.6.1", "dayjs": "^1.11.5", "dom-to-image": "^2.6.0", + "ethereum-blockies-base64": "^1.0.2", "framer-motion": "^6.5.1", + "gradient-avatar": "^1.0.2", "graphiql": "^2.2.0", "graphql": "^16.6.0", "graphql-ws": "^5.11.3", diff --git a/types/views/address.ts b/types/views/address.ts new file mode 100644 index 0000000000..2d22583182 --- /dev/null +++ b/types/views/address.ts @@ -0,0 +1,10 @@ +import type { ArrayElement } from 'types/utils'; + +export const IDENTICON_TYPES = [ + 'github', + 'jazzicon', + 'gradient_avatar', + 'blockie', +] as const; + +export type IdenticonType = ArrayElement; diff --git a/ui/address/AddressTxs.pw.tsx b/ui/address/AddressTxs.pw.tsx index c29594d875..b226debc32 100644 --- a/ui/address/AddressTxs.pw.tsx +++ b/ui/address/AddressTxs.pw.tsx @@ -27,7 +27,13 @@ base.describe('base view', () => { base.beforeEach(async({ page, mount }) => { await page.route(API_URL, (route) => route.fulfill({ status: 200, - body: JSON.stringify({ items: [ txMock.base, txMock.base ], next_page_params: { block: 1 } }), + body: JSON.stringify({ items: [ + txMock.base, + { + ...txMock.base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194', + }, + ], next_page_params: { block: 1 } }), })); component = await mount( diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png index 01e856e18b..a987d3de8f 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png index 123832e8e4..6297cffaec 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png index 325b32ecd4..ce212ad1b2 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png index 35bf7295ba..798c467bb2 100644 Binary files a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png and b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png index efbc67ee55..dad00e1c1c 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/shared/IdenticonGithub.tsx b/ui/shared/IdenticonGithub.tsx new file mode 100644 index 0000000000..430740247c --- /dev/null +++ b/ui/shared/IdenticonGithub.tsx @@ -0,0 +1,43 @@ +import { useColorModeValue, useToken, Box, chakra, Skeleton } from '@chakra-ui/react'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +const Identicon = dynamic<{ bg: string; string: string; size: number }>( + async() => { + const lib = await import('react-identicons'); + return typeof lib === 'object' && 'default' in lib ? lib.default : lib; + }, + { + loading: () => , + ssr: false, + }, +); + +interface Props { + className?: string; + size: number; + seed: string; +} + +const IdenticonGithub = ({ size, seed }: Props) => { + const bgColor = useToken('colors', useColorModeValue('gray.100', 'white')); + + return ( + + + + ); +}; + +export default React.memo(chakra(IdenticonGithub)); diff --git a/ui/shared/UserAvatar.tsx b/ui/shared/UserAvatar.tsx index 2b73219d60..0f2c2d2685 100644 --- a/ui/shared/UserAvatar.tsx +++ b/ui/shared/UserAvatar.tsx @@ -1,35 +1,10 @@ -import { useColorModeValue, useToken, SkeletonCircle, Image, Box } from '@chakra-ui/react'; +import { SkeletonCircle, Image } from '@chakra-ui/react'; import React from 'react'; -import Identicon from 'react-identicons'; import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; - -const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon; - -// for those who haven't got profile -// or if we cannot download the profile picture for some reasons -const FallbackImage = ({ size, id }: { size: number; id: string }) => { - const bgColor = useToken('colors', useColorModeValue('gray.100', 'white')); - - return ( - - - - - - ); -}; +import IdenticonGithub from 'ui/shared/IdenticonGithub'; interface Props { size: number; @@ -56,13 +31,10 @@ const UserAvatar = ({ size }: Props) => { flexShrink={ 0 } src={ data?.avatar } alt={ `Profile picture of ${ data?.name || data?.nickname || '' }` } - w={ sizeString } - minW={ sizeString } - h={ sizeString } - minH={ sizeString } + boxSize={ `${ size }px` } borderRadius="full" overflow="hidden" - fallback={ isImageLoadError || !data?.avatar ? : undefined } + fallback={ isImageLoadError || !data?.avatar ? : undefined } onError={ handleImageLoadError } /> ); diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 129c580f75..3077610628 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -2,7 +2,6 @@ import type { As } from '@chakra-ui/react'; import { Flex, Skeleton, Tooltip, chakra } from '@chakra-ui/react'; import _omit from 'lodash/omit'; import React from 'react'; -import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'; import type { AddressParam } from 'types/api/addressParams'; @@ -14,6 +13,7 @@ import iconContract from 'icons/contract.svg'; import * as EntityBase from 'ui/shared/entities/base/components'; import { getIconProps } from '../base/utils'; +import AddressIdenticon from './AddressIdenticon'; type LinkProps = EntityBase.LinkBaseProps & Pick; @@ -88,8 +88,11 @@ const Icon = (props: IconProps) => { return ( - - + + ); diff --git a/ui/shared/entities/address/AddressIdenticon.tsx b/ui/shared/entities/address/AddressIdenticon.tsx new file mode 100644 index 0000000000..5a19e6c6be --- /dev/null +++ b/ui/shared/entities/address/AddressIdenticon.tsx @@ -0,0 +1,78 @@ +import { Box, Image } from '@chakra-ui/react'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import config from 'configs/app'; +import IdenticonGithub from 'ui/shared/IdenticonGithub'; + +interface IconProps { + hash: string; + size: number; +} + +const Icon = dynamic( + async() => { + switch (config.UI.views.address.identiconType) { + case 'github': { + // eslint-disable-next-line react/display-name + return (props: IconProps) => ; + } + + case 'blockie': { + const makeBlockie = (await import('ethereum-blockies-base64')).default; + + // eslint-disable-next-line react/display-name + return (props: IconProps) => { + const data = makeBlockie(props.hash); + return ( + { + ); + }; + } + + case 'jazzicon': { + const Jazzicon = await import('react-jazzicon'); + + // eslint-disable-next-line react/display-name + return (props: IconProps) => { + return ( + + ); + }; + } + + case 'gradient_avatar': { + const GradientAvatar = (await import('gradient-avatar')).default; + + // eslint-disable-next-line react/display-name + return (props: IconProps) => { + const svg = GradientAvatar(props.hash, props.size); + return
; + }; + } + + default: { + return () => null; + } + } + }, { + ssr: false, + }); + +type Props = IconProps; + +const AddressIdenticon = ({ size, hash }: Props) => { + return ( + + + + ); +}; + +export default React.memo(AddressIdenticon); diff --git a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx index 30f8e633a2..aa375bfa01 100644 --- a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx +++ b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx @@ -44,8 +44,8 @@ const ProfileMenuDesktop = () => {