diff --git a/src/base/projects/Browser.svelte b/src/base/projects/Browser.svelte index 1bbccf13c..a4072464c 100644 --- a/src/base/projects/Browser.svelte +++ b/src/base/projects/Browser.svelte @@ -27,9 +27,9 @@ export let config: Config; export let path: string; export let profile: Profile | null = null; + export let anchors: string | null = null; export let org: string | null = null; - - const orgAddress = org ?? undefined; + export let user: string | null = null; // When the component is loaded the first time, the blob is yet to be loaded. let state: State = { status: Status.Idle }; @@ -61,7 +61,13 @@ const blob = await loadBlob(path); getBlob = new Promise(resolve => resolve(blob)); - navigate(proj.path({ urn, org: orgAddress, commit, path })); + if (org) { + navigate(proj.path({ urn, org, commit, path })); + } else if (user) { + navigate(proj.path({ urn, user, commit, path })); + } else { + navigate(proj.path({ urn, commit, path })); + } }; const fetchTree = async (path: string) => { @@ -71,7 +77,7 @@ // This is reactive to respond to path changes that don't originate from this // component, eg. when using the browser's "back" button. $: getBlob = loadBlob(path); - $: getAnchor = orgAddress ? Org.getAnchor(orgAddress, urn, config) : null; + $: getAnchor = anchors ? Org.getAnchor(anchors, urn, config) : null; $: loadingPath = state.status == Status.Loading ? state.path : null; @@ -244,16 +250,16 @@ {:then tree}
- commit {commit} + {commit}
- {#if orgAddress} + {#if anchors} {#await getAnchor} {:then anchor} {#if anchor === commit} - anchored 🔒 + anchored 🔒 {:else} diff --git a/src/base/projects/Routes.svelte b/src/base/projects/Routes.svelte index e87534968..83edcfdf2 100644 --- a/src/base/projects/Routes.svelte +++ b/src/base/projects/Routes.svelte @@ -31,3 +31,17 @@ + + + + + + + + + + + + + + diff --git a/src/base/projects/View.svelte b/src/base/projects/View.svelte index da4fcef4a..75d5e36c7 100644 --- a/src/base/projects/View.svelte +++ b/src/base/projects/View.svelte @@ -6,7 +6,7 @@ import Modal from '@app/Modal.svelte'; import Avatar from '@app/Avatar.svelte'; import { Org } from '@app/base/orgs/Org'; - import type { Profile } from '@app/profile'; + import { Profile, ProfileType } from '@app/profile'; import type { Info } from '@app/project'; import { formatOrg, isAddressEqual } from '@app/utils'; @@ -14,6 +14,7 @@ export let urn: string; export let org = ""; + export let user = ""; export let commit = ""; export let config: Config; export let path: string; @@ -24,22 +25,24 @@ let getProject = new Promise(resolve => { if (org) { Org.getProjectProfile(org, config).then(p => resolve(p)); + } else if (user) { + Profile.get(user, ProfileType.Project, config).then(p => resolve(p)); } else { resolve(null); } - }).then(async (orgProfile) => { - const seedHost = orgProfile?.seedHost; - const seedId = orgProfile?.seedId || undefined; + }).then(async (profile) => { + const seedHost = profile?.seedHost; + const seedId = profile?.seedId || undefined; const cfg = seedHost ? config.withSeed(seedHost, seedId) : config; const info = await proj.getInfo(urn, cfg); projectInfo = info; - return { project: info, config: cfg, org: orgProfile }; + return { project: info, config: cfg, profile }; }); const parentUrl = (profile: Profile) => { - return org === profile.name || isAddressEqual(org, profile.address) + return org ? `/orgs/${profile.nameOrAddress}` : `/users/${profile.nameOrAddress}`; }; @@ -111,9 +114,9 @@ {:then result}
- {#if result.org} - - + {#if result.profile} + + / {/if} @@ -122,7 +125,9 @@
{urn}
{result.project.meta.description}
- {:catch} diff --git a/src/base/projects/Widget.svelte b/src/base/projects/Widget.svelte index 5d357be29..5e3a2d9d8 100644 --- a/src/base/projects/Widget.svelte +++ b/src/base/projects/Widget.svelte @@ -15,7 +15,8 @@ export let project: proj.Project; export let config: Config; - export let org: string | undefined; + export let org: string | undefined = undefined; + export let user: string | undefined = undefined; let state: State = { status: Status.Loading }; let info: proj.Info | null = null; @@ -36,6 +37,7 @@ proj.path({ urn: project.id, org, + user, commit: project.anchor.stateHash, }) ); diff --git a/src/base/registrations/View.svelte b/src/base/registrations/View.svelte index ac43cbf19..da040d725 100644 --- a/src/base/registrations/View.svelte +++ b/src/base/registrations/View.svelte @@ -86,6 +86,9 @@ { name: "seed.id", label: "Seed ID", validate: "id", placeholder: "hynkyndc6w3p8urucakobzncqny7xxtw88...", description: "The Device ID of a Radicle Link node that hosts entities associated with this name.", value: r.profile.seedId, editable: true }, + { name: "project.anchors", label: "Anchors", validate: "address", placeholder: "Ethereum address, eg. 0x4a9cf21...bc91...", + description: "The alternative Ethereum address under which associated project anchors can be found.", + value: r.profile.projectAnchors, editable: true }, ]; state = { status: Status.Found, registration: r, owner }; } else { diff --git a/src/base/registrations/registrar.ts b/src/base/registrations/registrar.ts index 22198e45f..04f7c85bf 100644 --- a/src/base/registrations/registrar.ts +++ b/src/base/registrations/registrar.ts @@ -20,6 +20,7 @@ export interface EnsProfile { address: string | null; seedId: string | null; seedHost: string | null; + projectAnchors: string | null; url: string | null; avatar: string | null; twitter: string | null; @@ -74,11 +75,12 @@ export async function getRegistration(name: string, config: Config, resolver?: E resolver.getText('url'), resolver.getText('eth.radicle.seed.id'), resolver.getText('eth.radicle.seed.host'), + resolver.getText('eth.radicle.project.anchors'), resolver.getText('com.twitter'), resolver.getText('com.github'), ]); - const [address, avatar, url, seedId, seedHost, twitter, github] = + const [address, avatar, url, seedId, seedHost, projectAnchors, twitter, github] = meta.map(r => r.status == "fulfilled" ? r.value : null); return { @@ -89,6 +91,7 @@ export async function getRegistration(name: string, config: Config, resolver?: E avatar, seedId, seedHost, + projectAnchors, address, twitter, github, @@ -126,6 +129,16 @@ export async function getSeedId(name: string, config: Config, resolver?: EnsReso return resolver.getText('eth.radicle.seed.id'); } +export async function getProjectAnchorsAddress(name: string, config: Config, resolver?: EnsResolver): Promise { + name = name.toLowerCase(); + + resolver = resolver ?? await config.provider.getResolver(name); + if (! resolver) { + return null; + } + return resolver.getText('eth.radicle.project.anchors'); +} + export function registrar(config: Config): ethers.Contract { return new ethers.Contract(config.registrar.address, config.abi.registrar, config.provider); } diff --git a/src/base/registrations/resolver.ts b/src/base/registrations/resolver.ts index 465894332..1a4d0d431 100644 --- a/src/base/registrations/resolver.ts +++ b/src/base/registrations/resolver.ts @@ -36,6 +36,7 @@ export async function setRecords(name: string, records: EnsRecord[], resolver: E break; case "seed.id": case "seed.host": + case "project.anchors": calls.push( iface.encodeFunctionData("setText", [node, "eth.radicle." + r.name, r.value]) ); diff --git a/src/base/users/View.svelte b/src/base/users/View.svelte index 87cf3f8cf..4db769f4b 100644 --- a/src/base/users/View.svelte +++ b/src/base/users/View.svelte @@ -104,8 +104,14 @@
Address
-
+
+ + {#if profile.projectAnchors} +
Anchors
+
+
+ {/if}
Profile
@@ -117,23 +123,25 @@
- {#await Org.getOrgsByOwner(profile.address, config)} - - {:then orgs} - {#each orgs as org} - {#await org.getProjects(config) then projects} - {#each projects as project} -
- -
- {/each} - {:catch err} - - Error: failed to load projects: {err.message}. - - {/await} - {/each} - {/await} + {#if profile.projectAnchors} + {#await Org.get(profile.projectAnchors, config)} + + {:then org} + {#if org} + {#await org.getProjects(config) then projects} + {#each projects as project} +
+ +
+ {/each} + {:catch err} + + Error: failed to load projects: {err.message}. + + {/await} + {/if} + {/await} + {/if}
{/await} diff --git a/src/profile.ts b/src/profile.ts index d4beb6d2e..5609d05c7 100644 --- a/src/profile.ts +++ b/src/profile.ts @@ -84,6 +84,11 @@ export class Profile { return this.profile?.ens?.seedId ?? undefined; } + // Using undefined as return type if nothing to be returned since it works better with links + get projectAnchors(): string | undefined { + return this.profile?.ens?.projectAnchors ?? undefined; + } + // Get the name, and if not available, the address. get nameOrAddress(): string { return this.name ?? this.address; diff --git a/src/project.ts b/src/project.ts index 2c4b12293..85ecbc738 100644 --- a/src/project.ts +++ b/src/project.ts @@ -107,13 +107,15 @@ export async function getReadme( } export function path( - opts: { urn: string; org?: string; commit?: string; path?: string } + opts: { urn: string; org?: string; user?: string; commit?: string; path?: string } ): string { - const { urn, org, commit, path } = opts; + const { urn, org, user, commit, path } = opts; const result = []; if (org) { result.push("orgs", org); + } else if (user) { + result.push("users", user); } result.push("projects", urn); diff --git a/src/utils.ts b/src/utils.ts index f3cc87a61..235c461b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,7 +7,7 @@ import type { Config } from '@app/config'; import config from "@app/config.json"; import { assert } from '@app/error'; import type { EnsProfile } from "@app/base/registrations/registrar"; -import { getAvatar, getSeedHost, getSeedId, getRegistration } from '@app/base/registrations/registrar'; +import { getAvatar, getSeedHost, getSeedId, getProjectAnchorsAddress, getRegistration } from '@app/base/registrations/registrar'; import type { BasicProfile } from "@ceramicstudio/idx-constants"; import { ProfileType } from '@app/profile'; @@ -301,12 +301,13 @@ export async function resolveEnsProfile(addressOrName: string, profileType: Prof if (profileType === ProfileType.Project) { promises.push(getSeedHost(name, config, resolver)); promises.push(getSeedId(name, config, resolver)); + promises.push(getProjectAnchorsAddress(name, config, resolver)); } else if (profileType === ProfileType.Minimal) { promises.push(Promise.resolve(null)); } const project = await Promise.allSettled(promises); - const [avatar, address, seedHost, seedId] = + const [avatar, address, seedHost, seedId, projectAnchors] = project.map(r => r.status == "fulfilled" ? r.value : null); return { @@ -315,6 +316,7 @@ export async function resolveEnsProfile(addressOrName: string, profileType: Prof address, seedHost, seedId, + projectAnchors, url: null, twitter: null, github: null,