Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Users can specify anchors address in ENS
Alexis Sellier committed 4 years ago
commit 18b28010bbb4ab100b4e12a88634858b9c200593
parent 08d7ee6a2944f81753c3473f4d94f8730bfa50dd
11 files changed +102 -41
modified 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;
</script>

@@ -244,16 +250,16 @@
  {:then tree}
    <header>
      <div class="commit">
-
        commit {commit}
+
        {commit}
      </div>
      <div class="anchor">
-
        {#if orgAddress}
+
        {#if anchors}
          {#await getAnchor}
            <Loading small margins />
          {:then anchor}
            {#if anchor === commit}
              <span class="anchor-widget">
-
                <span class="anchor-label" title="{orgAddress}">anchored 🔒</span>
+
                <span class="anchor-label" title="{anchors}">anchored 🔒</span>
              </span>
            {:else}
              <span class="anchor-widget not-anchored">
modified src/base/projects/Routes.svelte
@@ -31,3 +31,17 @@
<Route path="/orgs/:org/projects/:urn" let:params>
  <View {config} org={params.org} urn={params.urn} path="/" />
</Route>
+

+
<!-- With a User context -->
+

+
<Route path="/users/:user/projects/:urn/head/*" let:params>
+
  <View {config} user={params.user} urn={params.urn} path={params['*'] || "/"} />
+
</Route>
+

+
<Route path="/users/:user/projects/:urn/:commit/*" let:params>
+
  <View {config} user={params.user} urn={params.urn} commit={params.commit} path={params["*"] || "/"} />
+
</Route>
+

+
<Route path="/users/:user/projects/:urn" let:params>
+
  <View {config} user={params.user} urn={params.urn} path="/" />
+
</Route>
modified 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<Profile | null>(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}
    <header>
      <div class="title bold">
-
        {#if result.org}
-
          <a class="org-avatar" title={result.org.nameOrAddress} href={parentUrl(result.org)}>
-
            <Avatar source={result.org.avatar || result.org.address} address={result.org.address}/>
+
        {#if result.profile}
+
          <a class="org-avatar" title={result.profile.nameOrAddress} href={parentUrl(result.profile)}>
+
            <Avatar source={result.profile.avatar || result.profile.address} address={result.profile.address}/>
          </a>
          <span class="divider">/</span>
        {/if}
@@ -122,7 +125,9 @@
      <div class="urn">{urn}</div>
      <div class="description">{result.project.meta.description}</div>
    </header>
-
    <Browser {urn} {org} profile={result.org} {path}
+
    <Browser {urn} {org} {user} {path}
+
      anchors={result.profile?.projectAnchors ?? org}
+
      profile={result.profile}
      commit={commit || result.project.head}
      config={result.config} />
  {:catch}
modified 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,
        })
      );
modified 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 {
modified 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<string | null> {
+
  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);
}
modified 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])
        );
modified src/base/users/View.svelte
@@ -104,8 +104,14 @@
      <div class="fields">
        <!-- Address -->
        <div class="label">Address</div>
-
        <div><Address noAvatar {config} address={profile.address} /></div>
+
        <div><Address {config} address={profile.address} /></div>
        <div></div>
+
        <!-- Project anchors -->
+
        {#if profile.projectAnchors}
+
          <div class="label">Anchors</div>
+
          <div><Address {config} address={profile.projectAnchors} /></div>
+
          <div></div>
+
        {/if}
        <!-- Profile -->
        <div class="label">Profile</div>
        <div>
@@ -117,23 +123,25 @@
        </div>
      </div>
      <div class="projects">
-
        {#await Org.getOrgsByOwner(profile.address, config)}
-
          <Loading center fadeIn />
-
        {:then orgs}
-
          {#each orgs as org}
-
            {#await org.getProjects(config) then projects}
-
              {#each projects as project}
-
                <div class="project">
-
                  <Project {project} org={org.address} config={profile.config(config)} />
-
                </div>
-
              {/each}
-
            {:catch err}
-
              <Message error>
-
                <strong>Error: </strong> failed to load projects: {err.message}.
-
              </Message>
-
            {/await}
-
          {/each}
-
        {/await}
+
        {#if profile.projectAnchors}
+
          {#await Org.get(profile.projectAnchors, config)}
+
            <Loading center fadeIn />
+
          {:then org}
+
            {#if org}
+
              {#await org.getProjects(config) then projects}
+
                {#each projects as project}
+
                  <div class="project">
+
                    <Project {project} user={addressOrName} config={profile.config(config)} />
+
                  </div>
+
                {/each}
+
              {:catch err}
+
                <Message error>
+
                  <strong>Error: </strong> failed to load projects: {err.message}.
+
                </Message>
+
              {/await}
+
            {/if}
+
          {/await}
+
        {/if}
      </div>
  </main>
{/await}
modified 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 <a href> 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;
modified 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);

modified 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,