-
Notifications
You must be signed in to change notification settings - Fork 15
feat(ensnode-react): useAvatarUrl #1142
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
Open
notrab
wants to merge
78
commits into
main
Choose a base branch
from
use-avatar-url
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
78 commits
Select commit
Hold shift + click to select a range
59a4c2f
feat: useAvatarUrl with manual fallback
notrab dfbc7e2
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab a584559
feat: useAvatarUrl with fallback (#1143)
notrab 4251e40
apply feedback
notrab 018f43d
move buildUrl
notrab 40192ae
apply feedback
notrab 8cc88a0
update jsdoc for avatar
notrab a233cf4
apply changes
notrab 35bc13a
fix: missing generics for useQuery;
notrab 43d8902
docs(changeset): useAvatarUrl
notrab b128d60
docs(changeset): Added useAvatarUrl hook
notrab 41e9f11
docs(changeset): Added buildUrl
notrab 66dc496
individual changesets
notrab cd5f6c5
export correct types for use
notrab b190da0
apply suggestion
notrab ddc875b
apply feedback
notrab 8c1c4bd
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab 65bdc7f
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab a10e774
toBrowserSupportedUrl
notrab 0b89a9d
update comments for normalizeAvatarUrl
notrab 8e42580
remove datasources pkg
notrab 5c0c095
move comment to above property
notrab a5aeabd
remove redundant enabled propetty
notrab 4c0891d
update loading state
notrab f064b24
use buildUrl
notrab c71a0a6
use toBrowserSupportedUrl only
notrab f844aed
call toBrowserSupportedUrl
notrab 0b888b8
revisit feedback
notrab 2baeb88
apply feedback
notrab a25a686
toString
notrab 1285379
Merge branch 'main' into use-avatar-url
notrab 2493e5e
add docs to readme
notrab a7b412b
Apply suggestions from code review
notrab 7abe501
apply missed usesProxy
notrab 4c9fd42
apply feedback
notrab 34d131b
apply example feedback
notrab 22aab5f
fix async proxy
notrab c230242
more fallback > proxy terminology
notrab b0c6adc
toBrowserSupportedUrl jsdoc improvements
notrab 4c98d73
catch and buildUrl
notrab 1faed00
fix: use browserSupportedAvatarUrlProxy
notrab 03ceb2c
Merge branch 'main' into use-avatar-url
notrab 1cec6c9
lint
notrab 5d484b9
fix merge conflict
notrab 9675b64
Update packages/ensnode-react/README.md
notrab 9933fad
apply doc changes
notrab d6bf7cd
ipfs example
notrab 994166c
remove notes
notrab 6264add
handle data protocol
notrab efb4806
cleanup
notrab 72158da
ignore eip155 use avatar url (#1182)
notrab 4150634
Avatar mock input (#1183)
notrab 741d716
build URL
notrab 578a8c5
Update packages/ensnode-react/README.md
notrab 0165bce
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab 70f2b43
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab 98ee6be
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab 9e79135
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab e204547
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab ef145e0
Update packages/ensnode-react/src/hooks/useAvatarUrl.ts
notrab 4d46cc3
mock page updates
notrab a30ef90
rename variables again
notrab 839a328
move files
notrab cb03bf8
caip
notrab ebe9df7
terminology
notrab adc847e
update mocks
notrab 21c91ef
old tests
notrab 4151338
replace old tests
notrab 9912ed2
replace old tests
notrab a012f44
Merge branch 'main' into use-avatar-url
notrab b4b91d3
handle invalid URLs
notrab 5b58014
Merge branch 'main' into use-avatar-url
notrab 050c8a6
merge conflicts
notrab 7ab5cb8
revert changes to buildEnsMetadataServiceAvatarUrl
notrab 112c20e
lint
notrab 2c8ea1e
Merge branch 'main' into use-avatar-url
lightwalker-eth a0c58de
simplify mock loading state
notrab b7e971e
fix loading overloads
notrab File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@ensnode/ensnode-sdk": minor | ||
| --- | ||
|
|
||
| Added `buildEnsMetadataServiceAvatarUrl` function to generate ENS Metadata Service avatar URLs for supported namespaces | ||
| Added `buildUrl` utility function to normalize URLs with implicit `https://` protocol handling | ||
| Exported metadata service utilities from `ens` module |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "ensadmin": minor | ||
| --- | ||
|
|
||
| Refactored avatar URL handling to use centralized utilities from `ensnode-sdk` | ||
| Removed duplicate `buildEnsMetadataServiceAvatarUrl` and `buildUrl` functions in favor of SDK exports | ||
| Updated `ens-avatar` component to use new avatar URL utilities |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@ensnode/ensnode-react": minor | ||
| --- | ||
|
|
||
| Added `useAvatarUrl` hook for resolving ENS avatar URLs with browser-supported protocols | ||
| Added `UseAvatarUrlResult` interface for avatar URL query results | ||
| Added `UseAvatarUrlParameters` interface for hook configuration | ||
| Added `AvatarUrl` type alias for avatar URL objects | ||
| Added support for custom fallback functions when avatar uses non-http/https protocols (e.g., `ipfs://`, `ar://`) | ||
| Added automatic fallback to ENS Metadata Service for unsupported protocol |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
apps/ensadmin/src/app/@breadcrumbs/mock/ens-avatar/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| "use client"; | ||
|
|
||
| import { | ||
| BreadcrumbItem, | ||
| BreadcrumbLink, | ||
| BreadcrumbPage, | ||
| BreadcrumbSeparator, | ||
| } from "@/components/ui/breadcrumb"; | ||
| import { useRawConnectionUrlParam } from "@/hooks/use-connection-url-param"; | ||
|
|
||
| export default function Page() { | ||
| const { retainCurrentRawConnectionUrlParam } = useRawConnectionUrlParam(); | ||
| const uiMocksBaseHref = retainCurrentRawConnectionUrlParam("/mock"); | ||
| return ( | ||
| <> | ||
| <BreadcrumbLink href={uiMocksBaseHref} className="hidden md:block"> | ||
| UI Mocks | ||
| </BreadcrumbLink> | ||
| <BreadcrumbSeparator className="hidden md:block" /> | ||
| <BreadcrumbItem> | ||
| <BreadcrumbPage>ENS Avatar</BreadcrumbPage> | ||
| </BreadcrumbItem> | ||
| </> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,295 @@ | ||
| "use client"; | ||
|
|
||
| import { AlertCircle, Check, X } from "lucide-react"; | ||
| import { useMemo, useState } from "react"; | ||
|
|
||
| import { ENSNamespaceIds } from "@ensnode/datasources"; | ||
| import { useAvatarUrl } from "@ensnode/ensnode-react"; | ||
| import { buildBrowserSupportedAvatarUrl, ENSNamespaceId, Name } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { EnsAvatar, EnsAvatarDisplay } from "@/components/ens-avatar"; | ||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; | ||
| import { Input } from "@/components/ui/input"; | ||
| import { Label } from "@/components/ui/label"; | ||
| import { | ||
| Select, | ||
| SelectContent, | ||
| SelectItem, | ||
| SelectTrigger, | ||
| SelectValue, | ||
| } from "@/components/ui/select"; | ||
| import { Skeleton } from "@/components/ui/skeleton"; | ||
|
|
||
| const TEST_NAMES: Name[] = [ | ||
| "lightwalker.eth", | ||
| "brantly.eth", | ||
| "ada.eth", | ||
| "jesse.base.eth", | ||
| "skeleton.mfpurrs.eth", | ||
| "vitalik.eth", | ||
| ]; | ||
|
|
||
| interface AvatarTestCardProps { | ||
| name: Name; | ||
| } | ||
|
|
||
| function AvatarTestCard({ name }: AvatarTestCardProps) { | ||
| const { data, isLoading, error } = useAvatarUrl({ name }); | ||
|
|
||
| const hasAvatar = data?.browserSupportedAvatarUrl !== null; | ||
| const hasRawUrl = data?.rawAvatarTextRecord !== null; | ||
| const hasError = !!error; | ||
|
|
||
| return ( | ||
| <Card | ||
| className={hasError ? "border-red-200" : hasAvatar ? "border-green-200" : "border-gray-200"} | ||
| > | ||
| <CardHeader> | ||
| <CardTitle className="text-lg flex items-center gap-2"> | ||
| {hasError ? ( | ||
| <AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0" /> | ||
| ) : hasAvatar ? ( | ||
| <Check className="h-5 w-5 text-green-600 flex-shrink-0" /> | ||
| ) : ( | ||
| <X className="h-5 w-5 text-gray-400 flex-shrink-0" /> | ||
| )} | ||
| <span>{name}</span> | ||
| </CardTitle> | ||
| </CardHeader> | ||
| <CardContent className="space-y-4"> | ||
| <div className="flex justify-center"> | ||
| <EnsAvatar name={name} className="h-32 w-32" /> | ||
| </div> | ||
|
|
||
| {error && ( | ||
| <div className="p-2 bg-red-50 border border-red-200 rounded"> | ||
| <p className="text-sm text-red-600">{error.message}</p> | ||
| </div> | ||
| )} | ||
|
|
||
| <div className="space-y-3"> | ||
| <div className="space-y-1"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-sm font-medium">Raw Avatar URL:</span> | ||
| {isLoading ? null : hasRawUrl ? ( | ||
| <Check className="h-4 w-4 text-green-600" /> | ||
| ) : ( | ||
| <X className="h-4 w-4 text-gray-400" /> | ||
| )} | ||
| </div> | ||
| {isLoading || !data ? ( | ||
| <Skeleton className="h-8 w-full rounded" /> | ||
| ) : ( | ||
| <div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded"> | ||
| {data.rawAvatarTextRecord || "Not set"} | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="space-y-1"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-sm font-medium">Browser-Supported URL:</span> | ||
| {isLoading ? null : hasAvatar ? ( | ||
| <Check className="h-4 w-4 text-green-600" /> | ||
| ) : ( | ||
| <X className="h-4 w-4 text-gray-400" /> | ||
| )} | ||
| </div> | ||
| {isLoading || !data ? ( | ||
| <Skeleton className="h-8 w-full rounded" /> | ||
| ) : ( | ||
| <div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded"> | ||
| {data.browserSupportedAvatarUrl?.toString() || "Not available"} | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="flex items-center justify-between p-2 bg-muted rounded"> | ||
| <span className="text-sm font-medium">Uses Proxy:</span> | ||
| {isLoading || !data ? ( | ||
| <Skeleton className="h-4 w-12 rounded" /> | ||
| ) : ( | ||
| <div className="flex items-center gap-2"> | ||
| {data.usesProxy ? ( | ||
| <> | ||
| <Check className="h-4 w-4 text-green-600" /> | ||
| <span className="text-sm text-green-600">Yes</span> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <X className="h-4 w-4 text-gray-400" /> | ||
| <span className="text-sm text-gray-600">No</span> | ||
| </> | ||
| )} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Wrapper component that resolves and renders an avatar using a custom raw avatar text record. | ||
| * Does not make any requests - only uses the provided inputs for resolution. | ||
| */ | ||
| function CustomAvatarWrapper({ | ||
| rawAssetTextRecord, | ||
| name, | ||
| namespaceId, | ||
| }: { | ||
| rawAssetTextRecord: string; | ||
| name: Name; | ||
| namespaceId: ENSNamespaceId; | ||
| }) { | ||
| // Resolve the avatar URL using the same logic as useAvatarUrl | ||
| // This does NOT make any network requests - it only processes the provided raw text record | ||
| const resolvedData = useMemo(() => { | ||
| return buildBrowserSupportedAvatarUrl(rawAssetTextRecord, name, namespaceId); | ||
| }, [rawAssetTextRecord, name, namespaceId]); | ||
|
|
||
| const hasAvatar = resolvedData.browserSupportedAssetUrl !== null; | ||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| <div className="flex justify-center"> | ||
| <EnsAvatarDisplay | ||
| name={name} | ||
| avatarUrl={resolvedData.browserSupportedAssetUrl} | ||
| className="h-32 w-32" | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Display the resolution information */} | ||
| <div className="space-y-3"> | ||
| <div className="space-y-1"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-sm font-medium">Raw Avatar Text Record:</span> | ||
| {resolvedData.rawAssetTextRecord ? ( | ||
| <Check className="h-4 w-4 text-green-600" /> | ||
| ) : ( | ||
| <X className="h-4 w-4 text-gray-400" /> | ||
| )} | ||
| </div> | ||
| <div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded"> | ||
| {resolvedData.rawAssetTextRecord || "Not set"} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="space-y-1"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-sm font-medium">Browser-Supported URL:</span> | ||
| {hasAvatar ? ( | ||
| <Check className="h-4 w-4 text-green-600" /> | ||
| ) : ( | ||
| <X className="h-4 w-4 text-gray-400" /> | ||
| )} | ||
| </div> | ||
| <div className="text-xs text-muted-foreground break-all bg-muted p-2 rounded"> | ||
| {resolvedData.browserSupportedAssetUrl?.toString() || "Not available"} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function CustomAvatarUrlTestCard() { | ||
| const [namespaceId, setNamespaceId] = useState<ENSNamespaceId>(ENSNamespaceIds.Mainnet); | ||
| const [name, setName] = useState<Name>("" as Name); | ||
| const [rawAssetTextRecord, setRawAssetTextRecord] = useState(""); | ||
|
|
||
| return ( | ||
| <Card className="border-blue-200"> | ||
| <CardHeader> | ||
| <CardTitle className="text-lg">Custom Avatar Text Record</CardTitle> | ||
| <CardDescription> | ||
| Enter an ENS namespace, name, and raw avatar text record to test resolution and display. | ||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent className="space-y-4"> | ||
| {/* Namespace Selection */} | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="namespace">ENS Namespace</Label> | ||
| <Select | ||
| value={namespaceId} | ||
| onValueChange={(value) => setNamespaceId(value as ENSNamespaceId)} | ||
| > | ||
| <SelectTrigger id="namespace"> | ||
| <SelectValue placeholder="Select namespace" /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value={ENSNamespaceIds.Mainnet}>Mainnet</SelectItem> | ||
| <SelectItem value={ENSNamespaceIds.Sepolia}>Sepolia</SelectItem> | ||
| <SelectItem value={ENSNamespaceIds.Holesky}>Holesky</SelectItem> | ||
| <SelectItem value={ENSNamespaceIds.EnsTestEnv}>ENS Test Env</SelectItem> | ||
| </SelectContent> | ||
| </Select> | ||
| </div> | ||
|
|
||
| {/* Name Input */} | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="name">Name</Label> | ||
| <Input | ||
| id="name" | ||
| type="text" | ||
| placeholder="vitalik.eth" | ||
| value={name} | ||
| onChange={(e) => setName(e.target.value as Name)} | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Avatar URL Input */} | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="avatar-url">Raw Avatar Text Record</Label> | ||
| <Input | ||
| id="avatar-url" | ||
| type="text" | ||
| placeholder="https://example.com/avatar.jpg, ipfs://..., eip155:1/erc721:..., etc." | ||
| value={rawAssetTextRecord} | ||
| onChange={(e) => setRawAssetTextRecord(e.target.value)} | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Avatar Display */} | ||
| {rawAssetTextRecord && name && ( | ||
| <CustomAvatarWrapper | ||
| rawAssetTextRecord={rawAssetTextRecord} | ||
| name={name} | ||
| namespaceId={namespaceId} | ||
| /> | ||
| )} | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| export default function MockAvatarUrlPage() { | ||
| return ( | ||
| <section className="flex flex-col gap-6 p-6 max-sm:p-4"> | ||
| <Card> | ||
| <CardHeader> | ||
| <CardTitle className="text-2xl leading-normal">Mock: ENS Avatar</CardTitle> | ||
| <CardDescription> | ||
| Displays avatar images, raw URLs, browser-supported URLs, and proxy usage for each ENS | ||
| name. | ||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <div className="space-y-4"> | ||
| {/* Custom URL Test Section */} | ||
| <CustomAvatarUrlTestCard /> | ||
|
|
||
| {/* Existing Test Names Grid */} | ||
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | ||
| {TEST_NAMES.map((name) => ( | ||
| <AvatarTestCard key={name} name={name} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| </section> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a UI where I can input the following:
And then view the result of this. Note how this functionality I'm requesting should make 0 requests for the real avatar text record of the name and therefore does not need to care at all about the active namespace of the connected ENSNode instance.