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

configurable address icon style #1199

Merged
merged 5 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY=xxx
16 changes: 16 additions & 0 deletions configs/app/ui/views/address.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions configs/app/ui/views/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as block } from './block';
export { default as address } from './address';
2 changes: 2 additions & 0 deletions configs/envs/.env.main.L2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -294,6 +295,7 @@ const schema = yup
.transform(getEnvValue)
.json()
.of(yup.string<BlockFieldId>().oneOf(BLOCK_FIELDS_IDS)),
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: yup.string().oneOf(IDENTICON_TYPES),

// e. misc
NEXT_PUBLIC_NETWORK_EXPLORERS: yup
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions deploy/values/review/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ By default, the app has generic favicon. You can override this behavior by provi

&nbsp;

#### 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` |

&nbsp;

### Misc

| Variable | Type| Description | Compulsoriness | Default value | Example value |
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions types/views/address.ts
Original file line number Diff line number Diff line change
@@ -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<typeof IDENTICON_TYPES>;
8 changes: 7 additions & 1 deletion ui/address/AddressTxs.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions ui/shared/IdenticonGithub.tsx
Original file line number Diff line number Diff line change
@@ -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;
tom2drum marked this conversation as resolved.
Show resolved Hide resolved
},
{
loading: () => <Skeleton w="100%" h="100%"/>,
ssr: false,
},
);

interface Props {
className?: string;
size: number;
seed: string;
}

const IdenticonGithub = ({ size, seed }: Props) => {
const bgColor = useToken('colors', useColorModeValue('gray.100', 'white'));

return (
<Box
boxSize={ `${ size * 2 }px` }
transformOrigin="left top"
transform="scale(0.5)"
borderRadius="full"
overflow="hidden"
>
<Identicon
bg={ bgColor }
string={ seed }
// the displayed size is doubled for retina displays and then scaled down
size={ size * 2 }
/>
</Box>
);
};

export default React.memo(chakra(IdenticonGithub));
36 changes: 4 additions & 32 deletions ui/shared/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box
flexShrink={ 0 }
maxWidth={ `${ size }px` }
maxHeight={ `${ size }px` }
>
<Box boxSize={ `${ size * 2 }px` } transformOrigin="left top" transform="scale(0.5)" borderRadius="full" overflow="hidden">
<IdenticonComponent
bg={ bgColor }
string={ id }
// the displayed size is doubled for retina displays and then scaled down
size={ size * 2 }
/>
</Box>
</Box>
);
};
import IdenticonGithub from 'ui/shared/IdenticonGithub';

interface Props {
size: number;
Expand All @@ -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 ? <FallbackImage size={ size } id={ data?.email || 'randomness' }/> : undefined }
fallback={ isImageLoadError || !data?.avatar ? <IdenticonGithub size={ size } seed={ data?.email || 'randomness' } flexShrink={ 0 }/> : undefined }
onError={ handleImageLoadError }
/>
);
Expand Down
9 changes: 6 additions & 3 deletions ui/shared/entities/address/AddressEntity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<EntityProps, 'address'>;

Expand Down Expand Up @@ -88,8 +88,11 @@ const Icon = (props: IconProps) => {

return (
<Tooltip label={ props.address.implementation_name }>
<Flex { ...styles }>
<Jazzicon diameter={ props.iconSize === 'lg' ? 30 : 20 } seed={ jsNumberForAddress(props.address.hash) }/>
<Flex marginRight={ styles.marginRight }>
<AddressIdenticon
size={ props.iconSize === 'lg' ? 30 : 20 }
hash={ props.address.hash }
/>
</Flex>
</Tooltip>
);
Expand Down
78 changes: 78 additions & 0 deletions ui/shared/entities/address/AddressIdenticon.tsx
Original file line number Diff line number Diff line change
@@ -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) => <IdenticonGithub size={ props.size } seed={ props.hash }/>;
}

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 (
<Image
src={ data }
alt={ `Identicon for ${ props.hash }}` }
/>
);
};
}

case 'jazzicon': {
const Jazzicon = await import('react-jazzicon');

// eslint-disable-next-line react/display-name
return (props: IconProps) => {
return (
<Jazzicon.default
diameter={ props.size }
seed={ Jazzicon.jsNumberForAddress(props.hash) }
/>
);
};
}

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 <div dangerouslySetInnerHTML={{ __html: svg }}/>;
};
}

default: {
return () => null;
}
}
}, {
ssr: false,
});

type Props = IconProps;

const AddressIdenticon = ({ size, hash }: Props) => {
return (
<Box boxSize={ `${ size }px` } borderRadius="full" overflow="hidden">
<Icon size={ size } hash={ hash }/>
</Box>
);
};

export default React.memo(AddressIdenticon);
4 changes: 2 additions & 2 deletions ui/snippets/profileMenu/ProfileMenuDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ const ProfileMenuDesktop = () => {
<PopoverTrigger>
<Button
variant="unstyled"
display="inline-flex"
height="auto"
display="block"
boxSize="50px"
flexShrink={ 0 }
{ ...buttonProps }
>
Expand Down
4 changes: 3 additions & 1 deletion ui/snippets/profileMenu/ProfileMenuMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ const ProfileMenuMobile = () => {
<Box padding={ 2 } onClick={ hasMenu ? onOpen : undefined }>
<Button
variant="unstyled"
height="auto"
display="block"
boxSize="24px"
flexShrink={ 0 }
{ ...buttonProps }
>
<UserAvatar size={ 24 }/>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading