diff --git a/.changelog/1246.feature.md b/.changelog/1246.feature.md new file mode 100644 index 000000000..fba6defd8 --- /dev/null +++ b/.changelog/1246.feature.md @@ -0,0 +1 @@ +Initial support for displaying account names diff --git a/package.json b/package.json index 70136bc92..495bcd393 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "axios": "1.6.8", "bignumber.js": "9.1.2", "bip39": "^3.1.0", + "chance": "^1.1.11", "date-fns": "3.6.0", "i18next": "23.11.2", "react": "18.2.0", @@ -101,6 +102,7 @@ "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.2", "@testing-library/user-event": "14.5.2", + "@types/chance": "^1.1.6", "@types/jest": "^29.5.12", "@types/node": "20.12.7", "@types/node-fetch": "2.6.11", diff --git a/src/app/components/Account/AccountLink.tsx b/src/app/components/Account/AccountLink.tsx index ddffd1e84..a81e3b65a 100644 --- a/src/app/components/Account/AccountLink.tsx +++ b/src/app/components/Account/AccountLink.tsx @@ -1,30 +1,141 @@ -import { FC } from 'react' +import { FC, ReactNode } from 'react' import { Link as RouterLink } from 'react-router-dom' import { useScreenSize } from '../../hooks/useScreensize' import Link from '@mui/material/Link' -import { TrimLinkLabel } from '../TrimLinkLabel' import { RouteUtils } from '../../utils/route-utils' +import InfoIcon from '@mui/icons-material/Info' import Typography from '@mui/material/Typography' import { SearchScope } from '../../../types/searchScope' +import { useAccountName } from '../../hooks/useAccountName' +import { trimLongString } from '../../utils/trimLongString' +import { MaybeWithTooltip } from '../AdaptiveTrimmer/MaybeWithTooltip' +import Box from '@mui/material/Box' +import { HighlightedText } from '../HighlightedText' +import { AdaptiveHighlightedText } from '../HighlightedText/AdaptiveHighlightedText' +import { AdaptiveTrimmer } from '../AdaptiveTrimmer/AdaptiveTrimmer' + +const WithTypographyAndLink: FC<{ + to: string + mobile?: boolean + children: ReactNode +}> = ({ children, to, mobile }) => { + return ( + + + {children} + + + ) +} interface Props { scope: SearchScope address: string + + /** + * Should we always trim the text to a short line? + */ alwaysTrim?: boolean + + /** + * What part of the name should be highlighted (if any) + */ + highlightedPartOfName?: string | undefined + + /** + * Any extra tooltips to display + * + * (Besides the content necessary because of potential shortening) + */ + extraTooltip?: ReactNode } -export const AccountLink: FC = ({ scope, address, alwaysTrim }) => { +export const AccountLink: FC = ({ + scope, + address, + alwaysTrim, + highlightedPartOfName, + extraTooltip, +}) => { const { isTablet } = useScreenSize() + const { name: accountName } = useAccountName(scope, address) const to = RouteUtils.getAccountRoute(scope, address) + + const tooltipPostfix = extraTooltip ? ( + <> + + {extraTooltip} + + ) : undefined + + // Are we in a table? + if (alwaysTrim) { + // In a table, we only ever want one short line + + return ( + + + {accountName} + {address} + {tooltipPostfix} + + ) : ( + address + ) + } + > + {accountName ? trimLongString(accountName, 12, 0) : trimLongString(address, 6, 6)} + + + ) + } + + if (!isTablet) { + // Details in desktop mode. + // We want one long line, with name and address. + + return ( + + + {accountName ? ( + + ({address}) + + ) : ( + address + )} + + + ) + } + + // We need to show the data in details mode on mobile. + // We want two lines, one for name (if available), one for address + // Both line adaptively shortened to fill available space return ( - - {alwaysTrim || isTablet ? ( - - ) : ( - - {address} - - )} - + + <> + + + + ) } diff --git a/src/app/components/Account/ContractCreatorInfo.tsx b/src/app/components/Account/ContractCreatorInfo.tsx index a944c8362..fed858109 100644 --- a/src/app/components/Account/ContractCreatorInfo.tsx +++ b/src/app/components/Account/ContractCreatorInfo.tsx @@ -14,7 +14,11 @@ import Box from '@mui/material/Box' import Skeleton from '@mui/material/Skeleton' import { useScreenSize } from '../../hooks/useScreensize' -const TxSender: FC<{ scope: SearchScope; txHash: string }> = ({ scope, txHash }) => { +const TxSender: FC<{ scope: SearchScope; txHash: string; alwaysTrim?: boolean }> = ({ + scope, + txHash, + alwaysTrim, +}) => { const { t } = useTranslation() if (scope.layer === Layer.consensus) { throw AppErrors.UnsupportedLayer @@ -31,7 +35,7 @@ const TxSender: FC<{ scope: SearchScope; txHash: string }> = ({ scope, txHash }) }} /> ) : senderAddress ? ( - + ) : ( t('common.missing') ) @@ -41,7 +45,8 @@ export const ContractCreatorInfo: FC<{ scope: SearchScope isLoading?: boolean creationTxHash: string | undefined -}> = ({ scope, isLoading, creationTxHash }) => { + alwaysTrim?: boolean +}> = ({ scope, isLoading, creationTxHash, alwaysTrim }) => { const { t } = useTranslation() const { isMobile } = useScreenSize() @@ -59,9 +64,9 @@ export const ContractCreatorInfo: FC<{ minWidth: '25%', }} > - + {t('contract.createdAt')} - + ) } @@ -69,7 +74,8 @@ export const ContractCreatorInfo: FC<{ export const DelayedContractCreatorInfo: FC<{ scope: SearchScope contractOasisAddress: string | undefined -}> = ({ scope, contractOasisAddress }) => { + alwaysTrim?: boolean +}> = ({ scope, contractOasisAddress, alwaysTrim }) => { const accountQuery = useGetRuntimeAccountsAddress( scope.network, scope.layer as Runtime, @@ -82,6 +88,11 @@ export const DelayedContractCreatorInfo: FC<{ const creationTxHash = contract?.eth_creation_tx || contract?.creation_tx return ( - + ) } diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx index 538ac8e0c..bc275873e 100644 --- a/src/app/components/Account/index.tsx +++ b/src/app/components/Account/index.tsx @@ -30,9 +30,17 @@ type AccountProps = { isLoading: boolean tokenPrices: AllTokenPrices showLayer?: boolean + highlightedPartOfName: string | undefined } -export const Account: FC = ({ account, token, isLoading, tokenPrices, showLayer }) => { +export const Account: FC = ({ + account, + token, + isLoading, + tokenPrices, + showLayer, + highlightedPartOfName, +}) => { const { t } = useTranslation() const { isMobile } = useScreenSize() const address = account ? account.address_eth ?? account.address : undefined @@ -67,7 +75,7 @@ export const Account: FC = ({ account, token, isLoading, tokenPric
- +
@@ -100,6 +108,7 @@ export const Account: FC = ({ account, token, isLoading, tokenPric diff --git a/src/app/components/AccountList/index.tsx b/src/app/components/AccountList/index.tsx index 2a6450348..1635190d9 100644 --- a/src/app/components/AccountList/index.tsx +++ b/src/app/components/AccountList/index.tsx @@ -39,7 +39,7 @@ export const AccountList: FC = ({ isLoading, limit, pagination key: 'size', }, { - content: , + content: , key: 'address', }, ...(verbose diff --git a/src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx b/src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx new file mode 100644 index 000000000..d020a7fc2 --- /dev/null +++ b/src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx @@ -0,0 +1,151 @@ +import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react' +import Box from '@mui/material/Box' +import InfoIcon from '@mui/icons-material/Info' +import { MaybeWithTooltip } from './MaybeWithTooltip' + +type AdaptiveDynamicTrimmerProps = { + getFullContent: () => { + content: ReactNode + length: number + } + getShortenedContent: (wantedLength: number) => ReactNode + extraTooltip: ReactNode +} + +/** + * Display content, potentially shortened as needed. + * + * This component will do automatic detection of available space, + * and determine the best way to display content accordingly. + * + * The difference compared to AdaptiveTrimmer is that this component + * expects a function to provide a shortened version of the components. + */ +export const AdaptiveDynamicTrimmer: FC = ({ + getFullContent, + getShortenedContent, + extraTooltip, +}) => { + // Initial setup + const textRef = useRef(null) + const { content: fullContent, length: fullLength } = getFullContent() + + // Data about the currently rendered version + const [currentContent, setCurrentContent] = useState() + const [currentLength, setCurrentLength] = useState(0) + + // Known good - this fits + const [largestKnownGood, setLargestKnownGood] = useState(0) + + // Known bad - this doesn't fit + const [smallestKnownBad, setSmallestKnownBad] = useState(fullLength + 1) + + // Are we exploring our possibilities now? + const [inDiscovery, setInDiscovery] = useState(false) + + const attemptContent = useCallback((content: ReactNode, length: number) => { + setCurrentContent(content) + setCurrentLength(length) + }, []) + + const attemptShortenedContent = useCallback( + (wantedLength: number) => attemptContent(getShortenedContent(wantedLength), wantedLength), + [attemptContent, getShortenedContent], + ) + + const initDiscovery = useCallback(() => { + setLargestKnownGood(0) + setSmallestKnownBad(fullLength + 1) + attemptContent(fullContent, fullLength) + setInDiscovery(true) + }, [fullContent, fullLength, attemptContent]) + + useEffect(() => { + initDiscovery() + const handleResize = () => { + initDiscovery() + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [initDiscovery]) + + useEffect(() => { + if (inDiscovery) { + if (!textRef.current) { + return + } + const isOverflow = textRef.current.scrollWidth > textRef.current.clientWidth + // log('Overflow?', isOverflow) + + if (isOverflow) { + // This is too much + + // Update known bad length + const newSmallestKnownBad = Math.min(currentLength, smallestKnownBad) + setSmallestKnownBad(newSmallestKnownBad) + + // We should try something smaller + attemptShortenedContent(Math.floor((largestKnownGood + newSmallestKnownBad) / 2)) + } else { + // This is OK + + // Update known good length + const newLargestKnownGood = Math.max(currentLength, largestKnownGood) + setLargestKnownGood(currentLength) + + if (currentLength === fullLength) { + // The whole thing fits, so we are good. + setInDiscovery(false) + } else { + if (currentLength + 1 === smallestKnownBad) { + // This the best we can do, for now + setInDiscovery(false) + } else { + // So far, so good, but we should try something longer + attemptShortenedContent(Math.floor((newLargestKnownGood + smallestKnownBad) / 2)) + } + } + } + } + }, [ + attemptShortenedContent, + currentLength, + fullContent, + fullLength, + inDiscovery, + initDiscovery, + largestKnownGood, + smallestKnownBad, + ]) + + const title = + currentLength !== fullLength ? ( + + {fullContent} + {extraTooltip && ( + + + {extraTooltip} + + )} + + ) : ( + extraTooltip + ) + + return ( + + + {currentContent} + + + ) +} diff --git a/src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx b/src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx new file mode 100644 index 000000000..878252654 --- /dev/null +++ b/src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx @@ -0,0 +1,140 @@ +import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react' +import Box from '@mui/material/Box' +import InfoIcon from '@mui/icons-material/Info' +import { trimLongString } from '../../utils/trimLongString' +import { MaybeWithTooltip } from './MaybeWithTooltip' + +type AdaptiveTrimmerProps = { + text: string | undefined + strategy: 'middle' | 'end' + extraTooltip?: ReactNode +} + +/** + * Display content, potentially shortened as needed. + * + * This component will do automatic detection of available space, + * and determine the best way to display content accordingly. + */ +export const AdaptiveTrimmer: FC = ({ text = '', strategy = 'end', extraTooltip }) => { + // Initial setup + const fullLength = text.length + const textRef = useRef(null) + + // Data about the currently rendered version + const [currentContent, setCurrentContent] = useState('') + const [currentLength, setCurrentLength] = useState(0) + + // Known good - this fits + const [largestKnownGood, setLargestKnownGood] = useState(0) + + // Known bad - this doesn't fit + const [smallestKnownBad, setSmallestKnownBad] = useState(fullLength + 1) + + // Are we exploring our possibilities now? + const [inDiscovery, setInDiscovery] = useState(false) + + const attemptContent = useCallback((content: string, length: number) => { + setCurrentContent(content) + setCurrentLength(length) + }, []) + + const attemptShortenedContent = useCallback( + (length: number) => { + const content = + strategy === 'middle' + ? trimLongString(text, Math.floor(length / 2) - 1, Math.floor(length / 2) - 1)! + : trimLongString(text, length, 0)! + + attemptContent(content, length) + }, + [strategy, text, attemptContent], + ) + + const initDiscovery = useCallback(() => { + setLargestKnownGood(0) + setSmallestKnownBad(fullLength + 1) + attemptContent(text, fullLength) + setInDiscovery(true) + }, [text, fullLength, attemptContent]) + + useEffect(() => { + initDiscovery() + const handleResize = () => { + initDiscovery() + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [initDiscovery]) + + useEffect(() => { + if (inDiscovery) { + if (!textRef.current) { + return + } + const isOverflow = textRef.current.scrollWidth > textRef.current.clientWidth + + if (isOverflow) { + // This is too much + + // Update known bad length + const newSmallestKnownBad = Math.min(currentLength, smallestKnownBad) + setSmallestKnownBad(newSmallestKnownBad) + + // We should try something smaller + attemptShortenedContent(Math.floor((largestKnownGood + newSmallestKnownBad) / 2)) + } else { + // This is OK + + // Update known good length + const newLargestKnownGood = Math.max(currentLength, largestKnownGood) + setLargestKnownGood(currentLength) + + if (currentLength === fullLength) { + // The whole thing fits, so we are good. + setInDiscovery(false) + } else { + if (currentLength + 1 === smallestKnownBad) { + // This the best we can do, for now + setInDiscovery(false) + } else { + // So far, so good, but we should try something longer + attemptShortenedContent(Math.floor((newLargestKnownGood + smallestKnownBad) / 2)) + } + } + } + } + }, [inDiscovery, currentLength, largestKnownGood, smallestKnownBad, attemptShortenedContent, fullLength]) + + if (!text) return null + + const title = + currentLength !== fullLength ? ( + + {text} + {extraTooltip && ( + + + {extraTooltip} + + )} + + ) : ( + extraTooltip + ) + + return ( + + + {currentContent} + + + ) +} diff --git a/src/app/components/AdaptiveTrimmer/MaybeWithTooltip.tsx b/src/app/components/AdaptiveTrimmer/MaybeWithTooltip.tsx new file mode 100644 index 000000000..4edfe0c26 --- /dev/null +++ b/src/app/components/AdaptiveTrimmer/MaybeWithTooltip.tsx @@ -0,0 +1,65 @@ +import { FC, ReactNode } from 'react' +import Tooltip from '@mui/material/Tooltip' +import Box from '@mui/material/Box' +import { SxProps } from '@mui/material/styles' + +type MaybeWithTooltipProps = { + /** + * Do we want to show the tooltip? + * + * Default is true + */ + tooltipWanted?: boolean + + /** + * What should be the content of the tooltip? + * + * Undefined means no tooltip + */ + title?: ReactNode + + /** + * Any extra styles to apply to the span + */ + spanSx?: SxProps + + /** + * The content to show + */ + children: ReactNode +} + +/** + * A component to display some content with or without a tooltip + */ +export const MaybeWithTooltip: FC = ({ + tooltipWanted = true, + title, + children, + spanSx, +}) => + tooltipWanted && !!title ? ( + + {title} + + } + > + + {children} + + + ) : ( + + {children} + + ) diff --git a/src/app/components/Blocks/BlockLink.tsx b/src/app/components/Blocks/BlockLink.tsx index f9673b7c8..53a55cf7c 100644 --- a/src/app/components/Blocks/BlockLink.tsx +++ b/src/app/components/Blocks/BlockLink.tsx @@ -7,6 +7,7 @@ import { RouteUtils } from '../../utils/route-utils' import { TrimLinkLabel } from '../TrimLinkLabel' import { SearchScope } from '../../../types/searchScope' import { useScreenSize } from '../../hooks/useScreensize' +import { AdaptiveTrimmer } from '../AdaptiveTrimmer/AdaptiveTrimmer' export const BlockLink: FC<{ scope: SearchScope; height: number }> = ({ scope, height }) => ( @@ -23,15 +24,34 @@ export const BlockHashLink: FC<{ alwaysTrim?: boolean }> = ({ scope, hash, height, alwaysTrim }) => { const { isTablet } = useScreenSize() - return ( - - {isTablet || alwaysTrim ? ( - - ) : ( - + const to = RouteUtils.getBlockRoute(scope, height) + + if (alwaysTrim) { + // Table view + return ( + + + + ) + } + + if (!isTablet) { + // Desktop view + return ( + + {hash} - )} + + ) + } + + // Mobile view + return ( + + + + ) } diff --git a/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx b/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx new file mode 100644 index 000000000..0676bfc26 --- /dev/null +++ b/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx @@ -0,0 +1,66 @@ +import { FC, ReactNode } from 'react' +import InfoIcon from '@mui/icons-material/Info' +import { HighlightedText, HighlightOptions } from './index' +import { AdaptiveDynamicTrimmer } from '../AdaptiveTrimmer/AdaptiveDynamicTrimmer' +import { HighlightedTrimmedText } from './HighlightedTrimmedText' + +type AdaptiveHighlightedTextProps = { + /** + * The text to display + */ + text: string | undefined + + /** + * The pattern to search for (and highlight) + */ + pattern: string | undefined + + /** + * Options for highlighting (case sensitivity, styling, etc.) + * + * (This is optional, sensible defaults are provided.) + */ + options?: HighlightOptions + + /** + * Extra content to put into the tooltip + */ + extraTooltip?: ReactNode +} + +/** + * Display a text with a part highlighted, potentially trimmed to an adaptive length around the highlight + */ +export const AdaptiveHighlightedText: FC = ({ + text, + pattern, + options, + extraTooltip, +}) => { + const fullContent = + + return text ? ( + ({ + content: fullContent, + length: text.length, + })} + getShortenedContent={wantedLength => ( + + )} + extraTooltip={ + extraTooltip ? ( + <> + + {extraTooltip} + + ) : undefined + } + /> + ) : undefined +} diff --git a/src/app/components/HighlightedText/HighlightedTrimmedText.tsx b/src/app/components/HighlightedText/HighlightedTrimmedText.tsx new file mode 100644 index 000000000..ff7c816b9 --- /dev/null +++ b/src/app/components/HighlightedText/HighlightedTrimmedText.tsx @@ -0,0 +1,43 @@ +import { FC } from 'react' + +import { HighlightedText, HighlightOptions } from './index' +import { cutAroundMatch } from './text-cutting' + +type HighlightedTrimmedTextProps = { + /** + * The text to display + */ + text: string | undefined + + /** + * The pattern to search for (and highlight) + */ + pattern: string | undefined + + /** + * Options for highlighting (case sensitivity, styling, etc.) + * + * (This is optional, sensible defaults are provided.) + */ + options?: HighlightOptions + + /** + * What should be the length of the fragment delivered, which + * has the pattern inside it? + */ + fragmentLength: number +} + +/** + * Display a text with a part highlighted, potentially trimmed to shorter length around the highlight + */ +export const HighlightedTrimmedText: FC = props => { + const { text, pattern, fragmentLength, options } = props + return ( + + ) +} diff --git a/src/app/components/HighlightedText/text-cutting.ts b/src/app/components/HighlightedText/text-cutting.ts new file mode 100644 index 000000000..cb7555fa6 --- /dev/null +++ b/src/app/components/HighlightedText/text-cutting.ts @@ -0,0 +1,74 @@ +import { findTextMatch, NormalizerOptions } from './text-matching' + +export interface CutAroundOptions extends NormalizerOptions { + /** + * What should be the length of the fragment delivered, which + * has the pattern inside it? + * + * The default value is 80. + */ + fragmentLength?: number +} + +/** + * Return a part of the corpus that contains the match to the pattern, if any + * + * If the corpus is undefined or empty, undefined is returned. + * + * If either the pattern is undefined or empty, or there is no match, + * an adequately sized part from the beginning of the corpus is returned. + * + * If there is a match, but the corpus is at most as long as the desired fragment length, + * the whole corpus is returned. + * + * If there is a match, and the corpus is longer than the desired fragment length, + * then a part of a corpus is returned, so that the match is within the returned part, + * around the middle. + */ +export function cutAroundMatch( + corpus: string | undefined, + pattern: string | undefined, + options: CutAroundOptions = {}, +): { + hasMatch: boolean + part: string | undefined +} { + const { fragmentLength = 80, ...matchOptions } = options + + if (!corpus) { + // there is nothing to see here + return { + hasMatch: false, + part: undefined, + } + } + + // do we have a match? + const match = pattern ? findTextMatch(corpus, [pattern], matchOptions) : undefined + + if (corpus.length <= fragmentLength) { + // the whole corpus fits into the max size, no need to cut. + return { + hasMatch: !!match, + part: corpus, + } + } + + // how much extra space do we have? + const buffer = fragmentLength - (pattern || '').length + + const matchStart = match?.startPos ?? 0 + + // We will start before the start of the match, by buffer / 2 chars + const startPos = Math.max(Math.min(matchStart - Math.floor(buffer / 2), corpus.length - fragmentLength), 0) + const endPos = Math.min(startPos + fragmentLength, corpus.length) + + // compile the result + const part = + (startPos ? '…' : '') + corpus.substring(startPos, endPos) + (endPos < corpus.length - 1 ? '…' : '') + + return { + hasMatch: true, + part, + } +} diff --git a/src/app/components/Search/search-utils.ts b/src/app/components/Search/search-utils.ts index bebe55a4a..6cc4fd520 100644 --- a/src/app/components/Search/search-utils.ts +++ b/src/app/components/Search/search-utils.ts @@ -119,6 +119,11 @@ export const validateAndNormalize = { return searchTerm.toLowerCase() } }, + accountNameFragment: (searchTerm: string) => { + if (searchTerm?.length >= textSearchMininumLength) { + return searchTerm.toLowerCase() + } + }, } satisfies { [name: string]: (searchTerm: string) => string | undefined } export function isSearchValid(searchTerm: string) { diff --git a/src/app/components/StyledDescriptionList/index.tsx b/src/app/components/StyledDescriptionList/index.tsx index 2ba5058ac..08865f369 100644 --- a/src/app/components/StyledDescriptionList/index.tsx +++ b/src/app/components/StyledDescriptionList/index.tsx @@ -61,6 +61,8 @@ export const StyledDescriptionList = styled(InlineDescriptionList, { color: COLORS.brandExtraDark, overflowWrap: 'anywhere', alignItems: 'center', + maxWidth: '100%', + overflowX: 'hidden', }, ...(standalone && { '&&': { diff --git a/src/app/components/Tokens/TokenHolders.tsx b/src/app/components/Tokens/TokenHolders.tsx index 77daeae62..8bc52e63b 100644 --- a/src/app/components/Tokens/TokenHolders.tsx +++ b/src/app/components/Tokens/TokenHolders.tsx @@ -53,7 +53,11 @@ export const TokenHolders: FC = ({ { key: 'address', content: ( - + ), }, { diff --git a/src/app/components/Tokens/TokenList.tsx b/src/app/components/Tokens/TokenList.tsx index 676f8aa60..ec2e0bea4 100644 --- a/src/app/components/Tokens/TokenList.tsx +++ b/src/app/components/Tokens/TokenList.tsx @@ -107,7 +107,11 @@ export const TokenList = (props: TokensProps) => { { content: ( - + ), diff --git a/src/app/components/Tokens/TokenTransfers.tsx b/src/app/components/Tokens/TokenTransfers.tsx index 139ccb814..a35c0ec17 100644 --- a/src/app/components/Tokens/TokenTransfers.tsx +++ b/src/app/components/Tokens/TokenTransfers.tsx @@ -116,7 +116,7 @@ export const TokenTransfers: FC = ({ ownAddress, }) => { const { t } = useTranslation() - const { isMobile } = useScreenSize() + const { isTablet } = useScreenSize() const tableColumns: TableColProps[] = [ { key: 'hash', content: t('common.hash') }, { key: 'block', content: t('common.block') }, @@ -139,7 +139,7 @@ export const TokenTransfers: FC = ({ content: ( ), @@ -186,7 +186,7 @@ export const TokenTransfers: FC = ({ {trimLongString(fromAddress)} ) : ( - + )} @@ -210,7 +210,7 @@ export const TokenTransfers: FC = ({ {trimLongString(toAddress)} ) : ( - + ), }, ...(differentTokens diff --git a/src/app/components/Transactions/ConsensusTransactions.tsx b/src/app/components/Transactions/ConsensusTransactions.tsx index 273752c40..4d5329878 100644 --- a/src/app/components/Transactions/ConsensusTransactions.tsx +++ b/src/app/components/Transactions/ConsensusTransactions.tsx @@ -1,11 +1,9 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' -import Typography from '@mui/material/Typography' import { Transaction } from '../../../oasis-nexus/api' import { Table, TableCellAlign, TableColProps } from '../../components/Table' import { RoundedBalance } from '../../components/RoundedBalance' -import { trimLongString } from '../../utils/trimLongString' import { TablePaginationProps } from '../Table/TablePagination' import { BlockLink } from '../Blocks/BlockLink' import { AccountLink } from '../Account/AccountLink' @@ -38,7 +36,6 @@ export const ConsensusTransactions: FC = ({ limit, pagination, transactions, - ownAddress, verbose = true, }) => { const { t } = useTranslation() @@ -66,7 +63,7 @@ export const ConsensusTransactions: FC = ({ key: 'success', }, { - content: , + content: , key: 'hash', }, { @@ -94,19 +91,7 @@ export const ConsensusTransactions: FC = ({ pr: 3, }} > - {!!ownAddress && transaction.sender === ownAddress ? ( - - {trimLongString(transaction.sender)} - - ) : ( - - )} + ), key: 'from', diff --git a/src/app/components/Transactions/RuntimeTransactions.tsx b/src/app/components/Transactions/RuntimeTransactions.tsx index 1c2208ec9..0268e1f2e 100644 --- a/src/app/components/Transactions/RuntimeTransactions.tsx +++ b/src/app/components/Transactions/RuntimeTransactions.tsx @@ -14,8 +14,6 @@ import { TablePaginationProps } from '../Table/TablePagination' import { BlockLink } from '../Blocks/BlockLink' import { AccountLink } from '../Account/AccountLink' import { TransactionLink } from './TransactionLink' -import { trimLongString } from '../../utils/trimLongString' -import Typography from '@mui/material/Typography' import { doesAnyOfTheseLayersSupportEncryptedTransactions } from '../../../types/layers' import { TransactionEncryptionStatus } from '../TransactionEncryptionStatus' import { Age } from '../Age' @@ -58,7 +56,6 @@ export const RuntimeTransactions: FC = ({ limit, pagination, transactions, - ownAddress, verbose = true, }) => { const { t } = useTranslation() @@ -104,11 +101,7 @@ export const RuntimeTransactions: FC = ({ : []), { content: ( - + ), key: 'hash', }, @@ -138,24 +131,11 @@ export const RuntimeTransactions: FC = ({ pr: 3, }} > - {!!ownAddress && - (transaction.sender_0_eth === ownAddress || transaction.sender_0 === ownAddress) ? ( - - {trimLongString(transaction.sender_0_eth || transaction.sender_0)} - - ) : ( - - )} + {targetAddress && ( @@ -167,19 +147,7 @@ export const RuntimeTransactions: FC = ({ }, { content: targetAddress ? ( - !!ownAddress && (transaction.to_eth === ownAddress || transaction.to === ownAddress) ? ( - - {trimLongString(targetAddress)} - - ) : ( - - ) + ) : null, key: 'to', }, diff --git a/src/app/components/Transactions/TransactionLink.tsx b/src/app/components/Transactions/TransactionLink.tsx index 1afcc359e..ca4590002 100644 --- a/src/app/components/Transactions/TransactionLink.tsx +++ b/src/app/components/Transactions/TransactionLink.tsx @@ -1,29 +1,80 @@ -import { FC } from 'react' +import { FC, ReactNode } from 'react' import { Link as RouterLink } from 'react-router-dom' import Link from '@mui/material/Link' import Typography from '@mui/material/Typography' +import InfoIcon from '@mui/icons-material/Info' import { useScreenSize } from '../../hooks/useScreensize' -import { TrimLinkLabel } from '../TrimLinkLabel' import { RouteUtils } from '../../utils/route-utils' import { SearchScope } from '../../../types/searchScope' +import { AdaptiveTrimmer } from '../AdaptiveTrimmer/AdaptiveTrimmer' +import { MaybeWithTooltip } from '../AdaptiveTrimmer/MaybeWithTooltip' +import { trimLongString } from '../../utils/trimLongString' +import Box from '@mui/material/Box' + +const WithTypographyAndLink: FC<{ children: ReactNode; mobile?: boolean; to: string }> = ({ + children, + mobile, + to, +}) => ( + + + {children} + + +) export const TransactionLink: FC<{ alwaysTrim?: boolean scope: SearchScope hash: string -}> = ({ alwaysTrim, hash, scope }) => { - const { isMobile } = useScreenSize() + extraTooltip?: ReactNode +}> = ({ alwaysTrim, hash, scope, extraTooltip }) => { + const { isTablet } = useScreenSize() const to = RouteUtils.getTransactionRoute(scope, hash) + const tooltipPostfix = extraTooltip ? ( + + + {extraTooltip} + + ) : undefined + + if (alwaysTrim) { + // Table mode + return ( + + + {hash} + {tooltipPostfix} + + } + > + {trimLongString(hash, 6, 6)} + + + ) + } + + if (!isTablet) { + // Desktop mode + return ( + + {hash} + + ) + } + // Mobile mode return ( - - {alwaysTrim || isMobile ? ( - - ) : ( - - {hash} - - )} - + + + ) } diff --git a/src/app/data/pontusx-account-names.ts b/src/app/data/pontusx-account-names.ts new file mode 100644 index 000000000..b5474f0c9 --- /dev/null +++ b/src/app/data/pontusx-account-names.ts @@ -0,0 +1,94 @@ +import axios from 'axios' +import { useQuery } from '@tanstack/react-query' +import type { AccountNameInfo } from '../hooks/useAccountName' +import { Layer } from '../../oasis-nexus/api' +import { Network } from '../../types/network' +import { findTextMatch } from '../components/HighlightedText/text-matching' +import * as process from 'process' + +const DATA_SOURCE_URL = 'https://raw.githubusercontent.com/deltaDAO/mvg-portal/main/pontusxAddresses.json' + +type AccountMap = Map +type AccountEntry = { + name: string + address: string +} +type AccountData = { + map: AccountMap + list: AccountEntry[] +} + +const getPontusXAccountNames = () => + new Promise((resolve, reject) => { + axios.get(DATA_SOURCE_URL).then(response => { + if (response.status !== 200) reject("Couldn't load names") + if (!response.data) reject("Couldn't load names") + const map = new Map() + const list: AccountEntry[] = [] + Object.entries(response.data).forEach(([address, name]) => { + map.set(address, name) + const normalizedEntry: AccountEntry = { + name: name as string, + address, + } + list.push(normalizedEntry) + }) + resolve({ + map, + list, + }) + }, reject) + }) + +export const usePontusXAccountNames = (enabled: boolean) => { + return useQuery(['pontusXNames'], getPontusXAccountNames, { + enabled, + staleTime: Infinity, + }) +} + +export const usePontusXAccountName = (address: string, enabled: boolean): AccountNameInfo => { + // When running jest tests, we don't want to load from Pontus-X. + if (process.env.NODE_ENV === 'test') { + return { + name: undefined, + loading: false, + } + } + // This is not a condition that can change while the app is running, so it's OK. + // eslint-disable-next-line react-hooks/rules-of-hooks + const { isLoading, error, data: allNames } = usePontusXAccountNames(enabled) + if (error) { + console.log('Failed to load Pontus-X account names', error) + } + return { + name: allNames?.map.get(address), + loading: isLoading, + } +} + +export const useSearchForPontusXAccountsByName = ( + network: Network, + nameFragment: string, + enabled: boolean, +) => { + const { isLoading, error, data: allNames } = usePontusXAccountNames(enabled) + if (error) { + console.log('Failed to load Pontus-X account names', error) + } + + const textMatcher = + nameFragment && enabled + ? (entry: AccountEntry): boolean => { + return !!findTextMatch(entry.name, [nameFragment]) + } + : () => false + return { + results: (allNames?.list || []).filter(textMatcher).map(entry => ({ + network, + layer: Layer.pontusx, + address: entry.address, + })), + isLoading, + } +} diff --git a/src/app/hooks/useAccountName.ts b/src/app/hooks/useAccountName.ts new file mode 100644 index 000000000..fe7a95ea7 --- /dev/null +++ b/src/app/hooks/useAccountName.ts @@ -0,0 +1,72 @@ +import { SearchScope } from '../../types/searchScope' +import Chance from 'chance' +import { Layer } from '../../oasis-nexus/api' +import { usePontusXAccountName, useSearchForPontusXAccountsByName } from '../data/pontusx-account-names' + +const NO_MATCH = '__no_match__' + +export type AccountNameInfo = { + name: string | undefined + loading: boolean +} + +/** + * Do we want to see some random names? + */ +const DEBUG_MODE = true + +/** + * Look up the name of an account. + */ +const lookupName = (scope: SearchScope, _address: string): string | undefined => { + switch (scope.layer) { + // TODO: look up the data + default: + // If debug mode is on, return mock names in ~50% of the cases, no nome otherwise + return DEBUG_MODE && Math.random() < 0.5 ? new Chance().name() : undefined + } +} + +const nameCache: Map = new Map() + +/** + * Find out the name of an account + * + * This is the entry point that should be used by the application, + * since this function also includes caching. + */ +export const useAccountName = (scope: SearchScope, address: string, dropCache = false): AccountNameInfo => { + const isPontusX = scope.layer === Layer.pontusx + + const pontusXName = usePontusXAccountName(address, isPontusX) + if (isPontusX) return pontusXName + + const key = `${scope.network}.${scope.layer}.${address}` + + if (dropCache) nameCache.delete(key) + const hasMatch = nameCache.has(key) + if (hasMatch) { + const cachedName = nameCache.get(key) + return { + name: cachedName === NO_MATCH ? undefined : cachedName, + loading: false, + } + } + const name = lookupName(scope, address) + nameCache.set(key, name ?? NO_MATCH) + return { + name, + loading: false, + } +} + +export const useSearchForAccountsByName = (scope: SearchScope, nameFragment = '') => { + const isValidPontusXSearch = scope.layer === Layer.pontusx && !!nameFragment + const pontusXResults = useSearchForPontusXAccountsByName(scope.network, nameFragment, isValidPontusXSearch) + return isValidPontusXSearch + ? pontusXResults + : { + isLoading: false, + results: [], + } +} diff --git a/src/app/pages/ConsensusAccountDetailsPage/DeferredConsensusAccountDetails.tsx b/src/app/pages/ConsensusAccountDetailsPage/DeferredConsensusAccountDetails.tsx new file mode 100644 index 000000000..5720b4ecd --- /dev/null +++ b/src/app/pages/ConsensusAccountDetailsPage/DeferredConsensusAccountDetails.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react' +import { AllTokenPrices } from '../../../coin-gecko/api' +import { Network } from '../../../types/network' + +/** + * Load and display details of a RuntimeAccount + */ +export const DeferredConsensusAccountDetails: FC<{ + network: Network + address: string + tokenPrices: AllTokenPrices + highlightedPartOfName: string | undefined + showLayer?: boolean +}> = () => + // { + // network, address, tokenPrices, highlightedPartOfName, showLayer + // }, + { + // TODO: load and display consensus account details when API and component becomes available + return null + } diff --git a/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx b/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx index ef0b1d0e1..beb512b66 100644 --- a/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx @@ -66,7 +66,7 @@ export const InstanceTitleCard: FC = ({ isFetched, isLoa }} > - + diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx index 18c57866d..ca4f98625 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx +++ b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx @@ -12,6 +12,7 @@ type AccountDetailsProps = { account: RuntimeAccount | undefined token: EvmToken | undefined tokenPrices: AllTokenPrices + highlightedPartOfName?: string | undefined } export const AccountDetailsCard: FC = ({ @@ -21,6 +22,7 @@ export const AccountDetailsCard: FC = ({ account, token, tokenPrices, + highlightedPartOfName, }) => { const { t } = useTranslation() return ( @@ -36,6 +38,7 @@ export const AccountDetailsCard: FC = ({ account={account} token={token} tokenPrices={tokenPrices} + highlightedPartOfName={highlightedPartOfName} /> ) diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx index 0c9f70225..4e94e2a4d 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx +++ b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx @@ -12,7 +12,8 @@ export const AccountDetailsView: FC<{ token?: EvmToken tokenPrices: AllTokenPrices showLayer?: boolean -}> = ({ isLoading, isError, account, token, tokenPrices, showLayer }) => { + highlightedPartOfName?: string | undefined +}> = ({ isLoading, isError, account, token, tokenPrices, showLayer, highlightedPartOfName }) => { const { t } = useTranslation() return isError ? ( @@ -23,6 +24,7 @@ export const AccountDetailsView: FC<{ isLoading={isLoading} tokenPrices={tokenPrices} showLayer={showLayer} + highlightedPartOfName={highlightedPartOfName} /> ) } diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx index 6c2d9dd63..5a2deced1 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx @@ -44,7 +44,7 @@ export const AccountNFTCollectionCard: FC = ({ sco isFetched && firstToken && ( - + ) diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx index f412300a2..77c34231d 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx +++ b/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx @@ -30,10 +30,14 @@ type AccountTokensCardProps = RuntimeAccountDetailsContext & { export const accountTokenContainerId = 'tokens' -export const ContractLink: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => { +export const ContractLink: FC<{ scope: SearchScope; address: string; alwaysTrim?: boolean }> = ({ + scope, + address, + alwaysTrim, +}) => { return ( - + ) @@ -79,7 +83,11 @@ export const AccountTokensCard: FC = ({ scope, account, { content: ( - + ), key: 'hash', diff --git a/src/app/pages/RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails.tsx b/src/app/pages/RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails.tsx new file mode 100644 index 000000000..882f742cc --- /dev/null +++ b/src/app/pages/RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' +import { Runtime, useGetRuntimeAccountsAddress } from '../../../oasis-nexus/api' +import { AllTokenPrices } from '../../../coin-gecko/api' +import { AccountDetailsView } from './AccountDetailsView' +import { Network } from '../../../types/network' + +/** + * Load and display details of a RuntimeAccount + */ +export const DeferredRuntimeAccountDetails: FC<{ + network: Network + layer: Runtime + address: string + tokenPrices: AllTokenPrices + highlightedPartOfName: string | undefined + showLayer?: boolean +}> = ({ network, layer, address, tokenPrices, highlightedPartOfName, showLayer }) => { + const { data, isLoading, isError } = useGetRuntimeAccountsAddress(network, layer, address) + return ( + + ) +} diff --git a/src/app/pages/RuntimeAccountDetailsPage/index.tsx b/src/app/pages/RuntimeAccountDetailsPage/index.tsx index b6c50e28d..8d57ecd01 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/index.tsx +++ b/src/app/pages/RuntimeAccountDetailsPage/index.tsx @@ -32,7 +32,7 @@ export const RuntimeAccountDetailsPage: FC = () => { const { t } = useTranslation() const scope = useRequiredScopeParam() - const { address } = useLoaderData() as AddressLoaderData + const { address, searchTerm } = useLoaderData() as AddressLoaderData const { account, isLoading: isAccountLoading, isError } = useAccount(scope, address) const isContract = !!account?.evm_contract const { token, isLoading: isTokenLoading } = useTokenInfo(scope, address, isContract) @@ -62,6 +62,7 @@ export const RuntimeAccountDetailsPage: FC = () => { account={account} token={token} tokenPrices={tokenPrices} + highlightedPartOfName={searchTerm} /> > = ({ label, children }) => { - return ( - - - {label} - - } - > - {children} - - ) -} - export const RuntimeTransactionDetailView: FC<{ isLoading?: boolean transaction: TransactionDetailRuntimeBlock | undefined @@ -214,22 +194,20 @@ export const RuntimeTransactionDetailView: FC<{ <>
{t('common.hash')}
- - - + /> + {hash && }
@@ -270,23 +248,20 @@ export const RuntimeTransactionDetailView: FC<{ <>
{t('common.from')}
- - - + /> {from && }
@@ -296,20 +271,17 @@ export const RuntimeTransactionDetailView: FC<{ <>
{t('common.to')}
- - - + /> {to && }
diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 335174f87..f6984f29a 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -7,6 +7,7 @@ import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage' import { AccountDetailsView } from '../RuntimeAccountDetailsPage/AccountDetailsView' import { AccountResult, + AccountAddressResult, BlockResult, ContractResult, ProposalResult, @@ -21,6 +22,9 @@ import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' import { TokenDetails } from '../../components/Tokens/TokenDetails' import { ProposalDetailView } from '../ProposalDetailsPage' +import { DeferredRuntimeAccountDetails } from '../RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails' +import { Layer } from '../../../oasis-nexus/api' +import { DeferredConsensusAccountDetails } from '../ConsensusAccountDetailsPage/DeferredConsensusAccountDetails' /** * Component for displaying a list of search results @@ -94,6 +98,35 @@ export const SearchResultsList: FC<{ linkLabel={t('search.results.accounts.viewLink')} /> + item.resultType === 'accountAddress', + )} + resultComponent={item => + item.layer === Layer.consensus ? ( + + ) : ( + + ) + } + link={acc => RouteUtils.getAccountRoute(acc, acc.address)} + linkLabel={t('search.results.accounts.viewLink')} + /> + item.resultType === 'contract')} diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts index 395a96cbf..b5450202e 100644 --- a/src/app/pages/SearchResultsPage/hooks.ts +++ b/src/app/pages/SearchResultsPage/hooks.ts @@ -19,6 +19,7 @@ import { import { RouteUtils } from '../../utils/route-utils' import { SearchParams } from '../../components/Search/search-utils' import { SearchScope } from '../../../types/searchScope' +import { useSearchForAccountsByName } from '../../hooks/useAccountName' function isDefined(item: T): item is NonNullable { return item != null @@ -27,7 +28,7 @@ function isDefined(item: T): item is NonNullable { export type ConditionalResults = { isLoading: boolean; results: T[] } type SearchResultItemCore = HasScope & { - resultType: 'block' | 'transaction' | 'account' | 'contract' | 'token' | 'proposal' + resultType: 'block' | 'transaction' | 'account' | 'accountAddress' | 'contract' | 'token' | 'proposal' } export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' } @@ -36,6 +37,10 @@ export type TransactionResult = SearchResultItemCore & RuntimeTransaction & { re export type AccountResult = SearchResultItemCore & RuntimeAccount & { resultType: 'account' } +export type AccountAddressResult = SearchResultItemCore & { address: string } & { + resultType: 'accountAddress' +} + export type ContractResult = SearchResultItemCore & RuntimeAccount & { resultType: 'contract' } export type TokenResult = SearchResultItemCore & EvmToken & { resultType: 'token' } @@ -46,6 +51,7 @@ export type SearchResultItem = | BlockResult | TransactionResult | AccountResult + | AccountAddressResult | ContractResult | TokenResult | ProposalResult @@ -193,6 +199,25 @@ export function useNetworkProposalsConditionally( } } +type AccountAddressInfo = Pick + +export function useNamedAccountConditionally( + currentScope: SearchScope | undefined, + nameFragment: string | undefined, +): ConditionalResults { + const queries = RouteUtils.getVisibleScopes(currentScope).map(scope => + // eslint-disable-next-line react-hooks/rules-of-hooks + useSearchForAccountsByName(scope, nameFragment), + ) + return { + isLoading: queries.some(query => query.isLoading), + results: queries + .map(query => query.results) + .filter(isDefined) + .flat(), + } +} + export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams) => { const queries = { blockHeight: useBlocksByHeightConditionally(currentScope, q.blockHeight), @@ -201,6 +226,7 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams oasisAccount: useRuntimeAccountConditionally(currentScope, q.consensusAccount), // TODO: remove evmBech32Account and use evmAccount when API is ready evmBech32Account: useRuntimeAccountConditionally(currentScope, q.evmBech32Account), + accountsByName: useNamedAccountConditionally(currentScope, q.accountNameFragment), tokens: useRuntimeTokenConditionally(currentScope, q.evmTokenNameFragment), proposals: useNetworkProposalsConditionally(q.networkProposalNameFragment), } @@ -211,6 +237,7 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams ...(queries.oasisAccount.results || []), ...(queries.evmBech32Account.results || []), ].filter(isAccountNonEmpty) + const accountAddresses = queries.accountsByName.results || [] const tokens = queries.tokens.results .map(l => l.evm_tokens) .flat() @@ -228,6 +255,9 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams ...accounts .filter(account => account.evm_contract) .map((account): ContractResult => ({ ...account, resultType: 'contract' })), + ...accountAddresses.map( + (account): AccountAddressResult => ({ ...account, resultType: 'accountAddress' }), + ), ...tokens.map((token): TokenResult => ({ ...token, resultType: 'token' })), ...proposals.map((proposal): ProposalResult => ({ ...proposal, resultType: 'proposal' })), ] diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts index ebcb816b8..e3e704e02 100644 --- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts +++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts @@ -35,6 +35,9 @@ export function useRedirectIfSingleResult( case 'account': redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) break + case 'accountAddress': + redirectTo = `${RouteUtils.getAccountRoute(item, item.address)}?q=${searchTerm}` + break case 'contract': redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address) break diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx index e70dd170d..a2c6979dc 100644 --- a/src/app/pages/TokenDashboardPage/NFTLinks.tsx +++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx @@ -66,8 +66,9 @@ export const NFTInstanceLink: FC = ({ scope, instance }) => { type NFTOwnerLinkProps = { scope: SearchScope owner: string + alwaysTrim?: boolean } -export const NFTOwnerLink: FC = ({ scope, owner }) => { +export const NFTOwnerLink: FC = ({ scope, owner, alwaysTrim }) => { const { t } = useTranslation() return ( @@ -76,7 +77,7 @@ export const NFTOwnerLink: FC = ({ scope, owner }) => { i18nKey="nft.ownerLink" t={t} components={{ - OwnerLink: , + OwnerLink: , }} /> diff --git a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx index 8c023c955..5fd211da1 100644 --- a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx @@ -69,7 +69,11 @@ export const TokenDetailsCard: FC<{ scope: SearchScope; address: string; searchT
{t('contract.creator')}
- +
{t('common.balance')}
diff --git a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx index cfad3b218..fdfb5ab8a 100644 --- a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx @@ -25,7 +25,11 @@ export const TokenTitleCard: FC<{ scope: SearchScope; address: string; searchTer {token && ( <> - + )} diff --git a/yarn.lock b/yarn.lock index e8b05519d..12d280054 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4239,6 +4239,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/chance@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.6.tgz#2fe3de58742629602c3fbab468093b27207f04ad" + integrity sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ== + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -5789,6 +5794,11 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chance@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.11.tgz#78e10e1f9220a5bbc60a83e3f28a5d8558d84d1b" + integrity sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"