diff --git a/packages/common/src/utils/coinLogo.test.ts b/packages/common/src/utils/coinLogo.test.ts new file mode 100644 index 00000000000..71b95ecd189 --- /dev/null +++ b/packages/common/src/utils/coinLogo.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' + +import { getCoinLogoUrl } from './coinLogo' + +const CID = 'baeaaaiqsecwikb3rxnjr4txcptwvtlpdj6et6bkkz7vo5giyy35xnl5vxxbxk' +const NODE_URL = `https://creatornode.audius.co/content/${CID}` + +describe('getCoinLogoUrl', () => { + it('requests a variant sized for the display at 3x', () => { + expect(getCoinLogoUrl(NODE_URL, 16)).toBe(`${NODE_URL}/150x150.jpg`) + expect(getCoinLogoUrl(NODE_URL, 50)).toBe(`${NODE_URL}/150x150.jpg`) + expect(getCoinLogoUrl(NODE_URL, 64)).toBe(`${NODE_URL}/480x480.jpg`) + expect(getCoinLogoUrl(NODE_URL, 200)).toBe(`${NODE_URL}/1000x1000.jpg`) + }) + + it('tolerates a trailing slash', () => { + expect(getCoinLogoUrl(`${NODE_URL}/`, 24)).toBe(`${NODE_URL}/150x150.jpg`) + }) + + it('leaves URLs it cannot resize unchanged', () => { + const irys = + 'https://gateway.irys.xyz/FRdFgDGxVZC9HsdRkWQ3CKCyi6icxvxT1ENWgnk1By4y' + const asset = 'https://audius.co/assets/TokenAUDIO-B9fxmOIn.png' + const variant = `${NODE_URL}/480x480.jpg` + expect(getCoinLogoUrl(irys, 16)).toBe(irys) + expect(getCoinLogoUrl(asset, 16)).toBe(asset) + expect(getCoinLogoUrl(variant, 16)).toBe(variant) + }) + + it('passes through empty values', () => { + expect(getCoinLogoUrl(undefined, 16)).toBeUndefined() + expect(getCoinLogoUrl(null, 16)).toBeNull() + expect(getCoinLogoUrl('', 16)).toBe('') + }) +}) diff --git a/packages/common/src/utils/coinLogo.ts b/packages/common/src/utils/coinLogo.ts new file mode 100644 index 00000000000..92d6d1f6d03 --- /dev/null +++ b/packages/common/src/utils/coinLogo.ts @@ -0,0 +1,33 @@ +import { SquareSizes } from '~/models/ImageSizes' + +// A bare validator node content URL, e.g. https://creatornode.audius.co/content/. +// Coin logos launched after the Irys -> Audius migration (and existing logos +// once they are re-hosted) are stored this way, and the node resizes any image +// it stores on demand at /content//.jpg. +const CONTENT_NODE_URL_REGEX = /^(https?:\/\/[^/?#]+\/content\/[^/?#]+)\/?$/ + +// Smallest variant that stays sharp at 3x pixel density +const getSquareSizeForDisplay = (displaySize: number) => { + const pixels = displaySize * 3 + if (pixels <= 150) return SquareSizes.SIZE_150_BY_150 + if (pixels <= 480) return SquareSizes.SIZE_480_BY_480 + return SquareSizes.SIZE_1000_BY_1000 +} + +/** + * Returns a resized variant of a coin's `logo_uri` for drawing it at + * `displaySize` CSS pixels. Coin logos are uploaded at up to 1000px and often + * weigh 0.5-2 MB, so badges should never load the original. + * + * Only validator node URLs can be resized. Other URLs (Irys/Arweave, static + * assets) are returned unchanged. + */ +export const getCoinLogoUrl = ( + logoUri: T, + displaySize: number +): T | string => { + if (!logoUri) return logoUri + const match = CONTENT_NODE_URL_REGEX.exec(logoUri) + if (!match) return logoUri + return `${match[1]}/${getSquareSizeForDisplay(displaySize)}.jpg` +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 2ee43601ea8..26ae2d8a184 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -55,6 +55,7 @@ export * from './handle' export * from './comments' export * from './quickSearch' export * from './coinMetrics' +export * from './coinLogo' export * from './convertHexToRGBA' export * from './socialLinks' export * from './videoUtils' diff --git a/packages/mobile/src/components/core/TokenIcon.tsx b/packages/mobile/src/components/core/TokenIcon.tsx index 4a776123d25..9a91a9ce43f 100644 --- a/packages/mobile/src/components/core/TokenIcon.tsx +++ b/packages/mobile/src/components/core/TokenIcon.tsx @@ -1,3 +1,4 @@ +import { getCoinLogoUrl } from '@audius/common/utils' import { Image } from 'react-native' import { HexagonalIcon, useTheme } from '@audius/harmony-native' @@ -19,7 +20,7 @@ export const TokenIcon = ({ logoURI, size = 's' }: TokenIconProps) => { return ( diff --git a/packages/web/src/components/CoinSuccessModal.tsx b/packages/web/src/components/CoinSuccessModal.tsx index 328a60698c7..1d04e721139 100644 --- a/packages/web/src/components/CoinSuccessModal.tsx +++ b/packages/web/src/components/CoinSuccessModal.tsx @@ -5,7 +5,7 @@ import { TrackMetadataForUpload, useCoinSuccessModal } from '@audius/common/store' -import { route } from '@audius/common/utils' +import { getCoinLogoUrl, route } from '@audius/common/utils' import { Artwork, Button, @@ -107,7 +107,7 @@ export const CoinSuccessModal = () => { > { { label: messages.coinGated, startAdornment: ( - + ) } ] diff --git a/packages/web/src/components/hover-card/FanClubHoverCard.tsx b/packages/web/src/components/hover-card/FanClubHoverCard.tsx index e8cb24a5447..7100769f986 100644 --- a/packages/web/src/components/hover-card/FanClubHoverCard.tsx +++ b/packages/web/src/components/hover-card/FanClubHoverCard.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import { useFanClub, useCoinBalance, useUser } from '@audius/common/api' import { ID } from '@audius/common/models' -import { formatCount, route } from '@audius/common/utils' +import { formatCount, getCoinLogoUrl, route } from '@audius/common/utils' import { Artwork, Flex, @@ -143,7 +143,7 @@ export const FanClubHoverCard = ({ {/* Token Icon */} {fanClubBadge?.logo_uri ? ( {user.fan_club_badge?.logo_uri ? ( - + ) : null} diff --git a/packages/web/src/components/user-badges/UserBadges.tsx b/packages/web/src/components/user-badges/UserBadges.tsx index 1377a897070..71c3708bb42 100644 --- a/packages/web/src/components/user-badges/UserBadges.tsx +++ b/packages/web/src/components/user-badges/UserBadges.tsx @@ -9,7 +9,7 @@ import { import { useUser } from '@audius/common/api' import { BadgeTier, ID } from '@audius/common/models' import { useTierAndVerifiedForUser } from '@audius/common/store' -import { Nullable } from '@audius/common/utils' +import { getCoinLogoUrl, Nullable } from '@audius/common/utils' import { Artwork, Box, @@ -199,7 +199,9 @@ const UserBadges = ({ }} > { > {coin.logoUri ? (