diff --git a/.gitignore b/.gitignore index 9166d3eeca..85fdc04a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ yalc.lock /public/sw.js.map /public/workbox-*.js /public/workbox-*.js.map +/public/fallback* \ No newline at end of file diff --git a/src/components/balances/AssetsTable/index.test.tsx b/src/components/balances/AssetsTable/index.test.tsx index 759db503d8..d22f7c1e9a 100644 --- a/src/components/balances/AssetsTable/index.test.tsx +++ b/src/components/balances/AssetsTable/index.test.tsx @@ -109,6 +109,7 @@ describe('AssetsTable', () => { onChainSigning: false, }, transactionExecution: true, + addressEmojis: false, }, }, }) @@ -215,6 +216,7 @@ describe('AssetsTable', () => { onChainSigning: false, }, transactionExecution: true, + addressEmojis: false, }, }, }) @@ -317,6 +319,7 @@ describe('AssetsTable', () => { onChainSigning: false, }, transactionExecution: true, + addressEmojis: false, }, }, }) @@ -416,6 +419,7 @@ describe('AssetsTable', () => { onChainSigning: false, }, transactionExecution: true, + addressEmojis: false, }, }, }) diff --git a/src/components/balances/HiddenTokenButton/index.test.tsx b/src/components/balances/HiddenTokenButton/index.test.tsx index 2df2e63fb0..b2a907e5be 100644 --- a/src/components/balances/HiddenTokenButton/index.test.tsx +++ b/src/components/balances/HiddenTokenButton/index.test.tsx @@ -87,6 +87,7 @@ describe('HiddenTokenToggle', () => { onChainSigning: false, }, transactionExecution: true, + addressEmojis: false, }, }, }) diff --git a/src/components/batch/BatchSidebar/styles.module.css b/src/components/batch/BatchSidebar/styles.module.css index ef9e77a584..1f8ba735f9 100644 --- a/src/components/batch/BatchSidebar/styles.module.css +++ b/src/components/batch/BatchSidebar/styles.module.css @@ -21,6 +21,10 @@ margin: var(--space-3) 0; } +.txs { + width: 100%; +} + .txs ul { padding: 0 var(--space-3) var(--space-3); display: flex; diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index f9b7c98317..5909748a2e 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -13,6 +13,7 @@ import ChainSwitcher from '../ChainSwitcher' import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import WalletInfo, { UNKNOWN_CHAIN_NAME } from '../WalletInfo' +import AddressEmoji from '../EthHashInfo/AddressEmoji' const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) @@ -78,7 +79,10 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { sx={{ marginTop: 1 }} > - +
+ + +
{addressBook[wallet.address] || wallet.ens} diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index d988b29eec..b4e8867b7a 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -54,6 +54,10 @@ gap: var(--space-2); } +.identicon { + position: relative; +} + @media (max-width: 599.95px) { .buttonContainer button { font-size: 12px; diff --git a/src/components/common/EthHashInfo/AddressEmoji.tsx b/src/components/common/EthHashInfo/AddressEmoji.tsx new file mode 100644 index 0000000000..831b9ab17e --- /dev/null +++ b/src/components/common/EthHashInfo/AddressEmoji.tsx @@ -0,0 +1,57 @@ +import { type ReactElement, memo } from 'react' +import { useAppSelector } from '@/store' +import { selectSettings } from '@/store/settingsSlice' +import css from './styles.module.css' + +// Define the Unicode ranges for animal, fruit, and vegetable emojis +const unicodeRanges = [ + [0x1f400, 0x1f43f], + [0x1f980, 0x1f994], + [0x1f345, 0x1f35f], + [0x1f950, 0x1f96b], +] + +// Calculate the total number of emojis +let totalEmojis = 0 +for (let range of unicodeRanges) { + totalEmojis += range[1] - range[0] + 1 +} + +function ethereumAddressToEmoji(address: string): string { + // Convert the Ethereum address from hexadecimal to decimal + const decimal = BigInt(address.slice(0, 6)) + + // Calculate the index by taking the modulo of the decimal by the number of emojis + let index = Number(decimal % BigInt(totalEmojis)) + + // Find the corresponding emoji + for (let range of unicodeRanges) { + if (index < range[1] - range[0] + 1) { + return String.fromCodePoint(range[0] + index) + } else { + index -= range[1] - range[0] + 1 + } + } + + return '' +} + +type EmojiProps = { + address: string + size?: number +} + +export const Emoji = memo(function Emoji({ address, size = 40 }: EmojiProps): ReactElement { + return ( +
+ {ethereumAddressToEmoji(address)} +
+ ) +}) + +const AddressEmoji = (props: EmojiProps): ReactElement | null => { + const { addressEmojis } = useAppSelector(selectSettings) + return addressEmojis ? : null +} + +export default AddressEmoji diff --git a/src/components/common/EthHashInfo/index.test.tsx b/src/components/common/EthHashInfo/index.test.tsx index 17bfd5bc80..3364cc06a9 100644 --- a/src/components/common/EthHashInfo/index.test.tsx +++ b/src/components/common/EthHashInfo/index.test.tsx @@ -173,7 +173,7 @@ describe('EthHashInfo', () => { expect(container.querySelector('.icon')).toHaveAttribute( 'style', - `background-image: url(${makeBlockie(MOCK_SAFE_ADDRESS)}); width: 40px; height: 40px;`, + `background-image: url(${makeBlockie(MOCK_SAFE_ADDRESS)}); width: 44px; height: 44px;`, ) }) diff --git a/src/components/common/EthHashInfo/index.tsx b/src/components/common/EthHashInfo/index.tsx index 5361dd1baf..dcfd6ce100 100644 --- a/src/components/common/EthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/index.tsx @@ -5,13 +5,14 @@ import useChainId from '@/hooks/useChainId' import { useAppSelector } from '@/store' import { selectSettings } from '@/store/settingsSlice' import { selectChainById } from '@/store/chainsSlice' - -import { getBlockExplorerLink } from '../../../utils/chains' - +import { getBlockExplorerLink } from '@/utils/chains' import type { EthHashInfoProps } from '@safe-global/safe-react-components' +import css from './styles.module.css' +import { Emoji } from './AddressEmoji' const PrefixedEthHashInfo = ({ showName = true, + avatarSize = 44, ...props }: EthHashInfoProps & { showName?: boolean }): ReactElement => { const settings = useAppSelector(selectSettings) @@ -20,18 +21,24 @@ const PrefixedEthHashInfo = ({ const addressBook = useAddressBook() const link = chain ? getBlockExplorerLink(chain, props.address) : undefined const name = showName ? props.name || addressBook[props.address] : undefined + const showEmoji = settings.addressEmojis && props.showAvatar !== false && !props.customAvatar && avatarSize >= 20 return ( - - {props.children} - +
+ + {props.children} + + {showEmoji && } +
) } diff --git a/src/components/common/EthHashInfo/styles.module.css b/src/components/common/EthHashInfo/styles.module.css index a64151a6eb..8ebcf511e3 100644 --- a/src/components/common/EthHashInfo/styles.module.css +++ b/src/components/common/EthHashInfo/styles.module.css @@ -1,41 +1,19 @@ .container { - display: flex; - align-items: center; - gap: 0.5em; - line-height: 1.4; -} - -.avatar { - flex-shrink: 0; + position: relative; } -.resizeAvatar, -.resizeAvatar * { - width: 2.3em !important; - height: 2.3em !important; -} - -.addressRow { +.emojiWrapper { + text-align: center; display: flex; - align-items: center; - gap: 0.25em; - white-space: nowrap; -} - -.nameRow { - overflow: hidden; -} - -.mobileAddress { - display: none; -} - -@media (max-width: 599.95px) { - .mobileAddress { - display: inline-block; - } - - .desktopAddress { - display: none; - } + flex-direction: column; + justify-content: center; + position: absolute; + top: 0; + left: 0; + border-radius: 100%; + border: 2px solid var(--color-secondary-light); + color: #000; + background-color: rgba(255, 255, 255, 0.5); + text-shadow: -1px 0 0 var(--color-secondary-light), 0 -1px 0 var(--color-secondary-light), + 1px 0 0 var(--color-secondary-light), 0 1px 0 var(--color-secondary-light); } diff --git a/src/components/common/SafeIcon/index.tsx b/src/components/common/SafeIcon/index.tsx index 4ecfdddbbb..8e565e8e36 100644 --- a/src/components/common/SafeIcon/index.tsx +++ b/src/components/common/SafeIcon/index.tsx @@ -3,6 +3,7 @@ import { Box } from '@mui/material' import css from './styles.module.css' import Identicon, { type IdenticonProps } from '../Identicon' +import AddressEmoji from '../EthHashInfo/AddressEmoji' interface ThresholdProps { threshold: number | string @@ -24,6 +25,7 @@ const SafeIcon = ({ address, threshold, owners, size }: SafeIconProps): ReactEle
{threshold && owners ? : null} +
) diff --git a/src/components/common/SafeIcon/styles.module.css b/src/components/common/SafeIcon/styles.module.css index d2efb8456d..c29277c89b 100644 --- a/src/components/common/SafeIcon/styles.module.css +++ b/src/components/common/SafeIcon/styles.module.css @@ -6,7 +6,7 @@ position: absolute; top: -6px; right: -6px; - z-index: 1; + z-index: 2; border-radius: 100%; font-size: 12px; min-width: 24px; diff --git a/src/components/settings/EmojiPreview/index.tsx b/src/components/settings/EmojiPreview/index.tsx new file mode 100644 index 0000000000..f01cfda5a8 --- /dev/null +++ b/src/components/settings/EmojiPreview/index.tsx @@ -0,0 +1,23 @@ +import { Alert, Box, Chip, SvgIcon, Typography } from '@mui/material' +import { ZERO_ADDRESS } from '@safe-global/safe-core-sdk/dist/src/utils/constants' +import InfoIcon from '@/public/images/notifications/info.svg' +import SafeIcon from '@/components/common/SafeIcon' + +const EmojiPreview = () => ( + <> + + + }> + + + Enable emojis for all Ethereum addresses and your Safe Accounts. + + + + {ZERO_ADDRESS} + + + +) + +export default EmojiPreview diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 0020248304..ea1597373a 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -34,7 +34,7 @@ const TransferTxInfo = ({ txInfo, txStatus }: TransferTxInfoProps) => { - + diff --git a/src/components/tx/SignOrExecuteForm/BatchButton.tsx b/src/components/tx/SignOrExecuteForm/BatchButton.tsx index 2c867e20c5..70fc4fb8b1 100644 --- a/src/components/tx/SignOrExecuteForm/BatchButton.tsx +++ b/src/components/tx/SignOrExecuteForm/BatchButton.tsx @@ -17,7 +17,7 @@ const BatchButton = ({ - diff --git a/src/pages/settings/appearance.tsx b/src/pages/settings/appearance.tsx index 17013c63af..6b5bfab4d1 100644 --- a/src/pages/settings/appearance.tsx +++ b/src/pages/settings/appearance.tsx @@ -4,11 +4,18 @@ import type { NextPage } from 'next' import Head from 'next/head' import { useAppDispatch, useAppSelector } from '@/store' -import { selectSettings, setCopyShortName, setDarkMode, setShowShortName } from '@/store/settingsSlice' +import { + selectSettings, + setCopyShortName, + setDarkMode, + setShowShortName, + setAddressEmojis, +} from '@/store/settingsSlice' import SettingsHeader from '@/components/settings/SettingsHeader' import { trackEvent, SETTINGS_EVENTS } from '@/services/analytics' import { useDarkMode } from '@/hooks/useDarkMode' import ExternalLink from '@/components/common/ExternalLink' +import EmojiPreview from '@/components/settings/EmojiPreview' const Appearance: NextPage = () => { const dispatch = useAppDispatch() @@ -16,11 +23,12 @@ const Appearance: NextPage = () => { const isDarkMode = useDarkMode() const handleToggle = ( - action: typeof setCopyShortName | typeof setDarkMode | typeof setShowShortName, + action: typeof setCopyShortName | typeof setDarkMode | typeof setShowShortName | typeof setAddressEmojis, event: | typeof SETTINGS_EVENTS.APPEARANCE.PREPEND_PREFIXES | typeof SETTINGS_EVENTS.APPEARANCE.COPY_PREFIXES - | typeof SETTINGS_EVENTS.APPEARANCE.DARK_MODE, + | typeof SETTINGS_EVENTS.APPEARANCE.DARK_MODE + | typeof SETTINGS_EVENTS.APPEARANCE.ADDRESS_EMOJIS, ) => { return (_: ChangeEvent, checked: boolean) => { dispatch(action(checked)) @@ -41,7 +49,7 @@ const Appearance: NextPage = () => {
- + @@ -97,6 +105,27 @@ const Appearance: NextPage = () => { /> + + + + + Experimental + + + + + + } + label="Address emoji" + /> + + +
diff --git a/src/services/analytics/events/settings.ts b/src/services/analytics/events/settings.ts index 485faea317..e55d417281 100644 --- a/src/services/analytics/events/settings.ts +++ b/src/services/analytics/events/settings.ts @@ -48,6 +48,10 @@ export const SETTINGS_EVENTS = { action: 'Dark mode', category: SETTINGS_CATEGORY, }, + ADDRESS_EMOJIS: { + action: 'Toggle address emojis', + category: SETTINGS_CATEGORY, + }, }, MODULES: { REMOVE_MODULE: { diff --git a/src/store/settingsSlice.ts b/src/store/settingsSlice.ts index 5735ef64ad..691e8d96f3 100644 --- a/src/store/settingsSlice.ts +++ b/src/store/settingsSlice.ts @@ -42,6 +42,7 @@ export type SettingsState = { onChainSigning: boolean } transactionExecution: boolean + addressEmojis: boolean } export const initialState: SettingsState = { @@ -68,6 +69,7 @@ export const initialState: SettingsState = { onChainSigning: false, }, transactionExecution: true, + addressEmojis: false, } export const settingsSlice = createSlice({ @@ -92,6 +94,9 @@ export const settingsSlice = createSlice({ setDarkMode: (state, { payload }: PayloadAction) => { state.theme.darkMode = payload }, + setAddressEmojis: (state, { payload }: PayloadAction) => { + state.addressEmojis = payload + }, setHiddenTokensForChain: (state, { payload }: PayloadAction<{ chainId: string; assets: string[] }>) => { const { chainId, assets } = payload state.hiddenTokens[chainId] = assets @@ -127,6 +132,7 @@ export const { setCopyShortName, setQrShortName, setDarkMode, + setAddressEmojis, setHiddenTokensForChain, setTokenList, setRpc,