Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor org and user routes
Sebastian Martinez committed 4 years ago
commit a43ac33ddc95a49faa1b8090b3d165bbb38262d6
parent 82a2ac6f6427ac2dc7edbcf3dc816e0e18fb7a8d
24 files changed +619 -799
modified src/Address.svelte
@@ -21,9 +21,14 @@
  const nameOrAddress = profile?.name || address;

  onMount(async () => {
-
    identifyAddress(address, config).then((t: AddressType) => addressType = t);
-
    if (resolve && !profile) {
-
      Profile.get(address, ProfileType.Minimal, config).then(p => profile = p);
+
    if (!profile) {
+
      identifyAddress(address, config).then((t: AddressType) => addressType = t);
+
      if (resolve) {
+
        Profile.get(address, ProfileType.Minimal, config).then(p => profile = p);
+
      }
+
    } else {
+
      // If there is a profile we can use the profile.type to avoid identifying it again.
+
      addressType = profile.type;
    }
  });
  $: addressLabel = profile?.name ? compact ? parseEnsLabel(profile.name, config) : profile.name : checksumAddress;
@@ -54,7 +59,7 @@
    <Avatar inline source={profile?.avatar ?? address} {address}/>
  {/if}
  {#if addressType === AddressType.Org}
-
    <a use:link href={`/orgs/${nameOrAddress}`}>{addressLabel}</a>
+
    <a use:link href={`/${nameOrAddress}`}>{addressLabel}</a>
    <span class="badge">org</span>
  {:else if addressType === AddressType.Safe}
    <a href={safeLink(address, config)} target="_blank">{addressLabel}</a>
@@ -63,7 +68,7 @@
    <a href={explorerLink(address, config)} target="_blank">{addressLabel}</a>
    <span class="badge">contract</span>
  {:else if addressType === AddressType.EOA}
-
    <a use:link href={`/users/${nameOrAddress}`}>{addressLabel}</a>
+
    <a use:link href={`/${nameOrAddress}`}>{addressLabel}</a>
  {:else} <!-- While we're waiting to find out what address type it is -->
    <a href={explorerLink(address, config)} target="_blank">{addressLabel}</a>
  {/if}
modified src/App.svelte
@@ -11,6 +11,7 @@
  import Seeds from '@app/base/seeds/Routes.svelte';
  import Faucet from '@app/base/faucet/Routes.svelte';
  import Projects from '@app/base/projects/Routes.svelte';
+
  import Profile from '@app/Profile.svelte';
  import Resolver from '@app/base/resolver/Routes.svelte';
  import Header from '@app/Header.svelte';
  import Loading from '@app/Loading.svelte';
@@ -89,9 +90,12 @@
        <Orgs {config} />
        <Seeds {config} />
        <Faucet {config} />
-
        <Users {config} />
-
        <Projects {config} />
+
        <Users />
        <Resolver {config} />
+
        <Route path="/:addressOrName" let:params>
+
          <Profile addressOrName={params.addressOrName} {config} />
+
        </Route>
+
        <Projects {config} />
      </Router>
    </div>
  {:catch err}
modified src/Cards.svelte
@@ -38,18 +38,18 @@
  <div class="list">
    {#each orgs as org}
      {#await Profile.get(org.name ?? org.address, ProfileType.Minimal, config)}
-
        <Card profile={{ address: org.address }} {config} path={`/orgs/${org.address}`} />
+
        <Card profile={{ address: org.address }} {config} path={`/${org.address}`} />
      {:then profile}
        {#if orgMembers[profile.address]?.length}
-
          <Card {profile} {config} path={`/orgs/${profile.nameOrAddress}`} members={orgMembers[profile.address]} />
+
          <Card {profile} {config} path={`/${profile.nameOrAddress}`} members={orgMembers[profile.address]} />
        {:else}
-
          <Card {profile} {config} path={`/orgs/${profile.nameOrAddress}`} />
+
          <Card {profile} {config} path={`/${profile.nameOrAddress}`} />
        {/if}
      {/await}
    {/each}

    {#each profiles as profile}
-
      <Card {profile} {config} path={`/users/${profile.nameOrAddress}`} />
+
      <Card {profile} {config} path={`/${profile.nameOrAddress}`} />
    {/each}

    {#if !orgs.length && !profiles.length}
added src/NotFound.svelte
@@ -0,0 +1,21 @@
+
<script lang="ts">
+
  import Modal from '@app/Modal.svelte';
+

+
  export let title = "";
+
  export let subtitle = "";
+

+
  const back = () => window.history.back();
+
</script>
+

+
<Modal subtle>
+
  <span slot="title">🏜️</span>
+
  <span slot="body">
+
    <p class="highlight"><strong>{title}</strong></p>
+
    <p>{subtitle}</p>
+
  </span>
+
  <span slot="actions">
+
    <button on:click={back}>
+
      Back
+
    </button>
+
  </span>
+
</Modal>
added src/Profile.svelte
@@ -0,0 +1,391 @@
+
<script lang="ts">
+
  import type { SvelteComponent } from 'svelte';
+
  import type { Config } from '@app/config';
+
  import Address from '@app/Address.svelte';
+
  import Avatar from '@app/Avatar.svelte';
+
  import Icon from '@app/Icon.svelte';
+
  import SetName from '@app/ens/SetName.svelte';
+
  import SeedAddress from '@app/SeedAddress.svelte';
+
  import TransferOwnership from '@app/base/orgs/TransferOwnership.svelte';
+
  import Link from '@app/Link.svelte';
+
  import { Profile, ProfileType } from '@app/profile';
+
  import Loading from '@app/Loading.svelte';
+
  import * as utils from '@app/utils';
+
  import { session } from '@app/session';
+
  import { Org } from '@app/base/orgs/Org';
+
  import Message from '@app/Message.svelte';
+
  import Error from '@app/Error.svelte';
+
  import { User } from '@app/base/users/User';
+
  import Projects from '@app/base/orgs/View/Projects.svelte';
+
  import { NotFoundError } from '@app/error';
+
  import NotFound from '@app/NotFound.svelte';
+

+
  export let config: Config;
+
  export let addressOrName: string;
+
  export let action: string | null = null;
+

+
  let setNameForm: typeof SvelteComponent | null =
+
    action === "setName" ? SetName : null;
+
  const setName = () => {
+
    setNameForm = SetName;
+
  };
+

+
  let transferOwnerForm: typeof SvelteComponent | null = null;
+
  const transferOwnership = () => {
+
    transferOwnerForm = TransferOwnership;
+
  };
+

+
  $: account = $session && $session.address;
+
  $: isOwner = (org: Org): boolean => $session
+
    ? utils.isAddressEqual(org.owner, $session.address)
+
    : false;
+
  $: getOrgTreasury = async (org: Org): Promise<Array<utils.Token>| undefined> => {
+
    const addressType = await utils.identifyAddress(org.owner, config);
+
    // We query the org treasury only for Gnosis Safes, to maintain some privacy for EOA org owners.
+
    if (addressType === utils.AddressType.Safe) {
+
      try {
+
        const tokens = await utils.getTokens(org.owner, config);
+
        const balance = await config.provider.getBalance(org.owner);
+

+
        if (! balance.isZero()) {
+
          // To maintain the format we hardcode the ETH specs.
+
          return [{ balance, decimals: 18, logo: "", name: "Ethereum", symbol: "ETH" }, ...tokens];
+
        } else {
+
          return tokens;
+
        }
+
      } catch (e) {
+
        console.error(e);
+
      }
+
    }
+
  };
+
  $: isUserAuthorized = (address: string): boolean | null => {
+
    return $session && utils.isAddressEqual(address, $session.address);
+
  };
+
  $: isOrgAuthorized = async (org: Org): Promise<boolean> => {
+
    if ($session) {
+
      if (isOwner(org)) {
+
        return true;
+
      }
+
      return await org.isMember($session.address, config);
+
    }
+
    return false;
+
  };
+
</script>
+

+
<style>
+
  main {
+
    padding: 5rem 0;
+
    width: 720px;
+
  }
+
  main > header {
+
    display: flex;
+
    align-items: center;
+
    justify-content: left;
+
    margin-bottom: 2rem;
+
  }
+
  main > header > * {
+
    margin: 0 1rem 0 0;
+
  }
+
  .info {
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: center;
+
    align-items: left;
+
  }
+
  .info a {
+
    border: none;
+
  }
+
  .fields {
+
    display: grid;
+
    grid-template-columns: 5rem 4fr 2fr;
+
    grid-gap: 1rem 2rem;
+
    margin-bottom: 1rem;
+
  }
+
  .fields > div {
+
    justify-self: start;
+
    align-self: center;
+
    height: 2rem;
+
    line-height: 2rem;
+
  }
+
  .avatar {
+
    width: 64px;
+
    height: 64px;
+
  }
+
  .title {
+
    display: flex;
+
    align-items: center;
+
  }
+
  .links {
+
    display: flex;
+
    align-items: center;
+
    justify-content: left;
+
  }
+
  .overflow-text {
+
    width: 100%;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
    white-space: nowrap;
+
  }
+
  .url {
+
    display: flex; /* Ensures correct vertical positioning of icons */
+
    margin-right: 1rem;
+
    height: 1.6rem;
+
  }
+
  .members {
+
    margin-top: 2rem;
+
    align-items: center;
+
    display: flex;
+
    flex-wrap: wrap;
+
  }
+
  .members .member {
+
    display: flex;
+
    align-items: center;
+
    margin-right: 2rem;
+
    margin-bottom: 1rem;
+
  }
+
  .members .member:last-child {
+
    margin-right: 0;
+
  }
+
  .members .member-icon {
+
    width: 2rem;
+
    height: 2rem;
+
    margin-right: 1rem;
+
  }
+
  @media (max-width: 720px) {
+
    main {
+
      width: 100%;
+
      padding: 1.5rem;
+
    }
+
    .fields {
+
      grid-template-columns: 5rem auto;
+
    }
+
    .members .member {
+
      margin-right: 1rem;
+
    }
+
  }
+
</style>
+

+
<svelte:head>
+
  <title>{addressOrName}</title>
+
</svelte:head>
+

+
{#await Profile.get(addressOrName, ProfileType.Full, config)}
+
  <div class="off-centered">
+
    <Loading center />
+
  </div>
+
{:then profile}
+
  <main>
+
    <header>
+
      <div class="avatar">
+
        <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
+
      </div>
+
      <div class="info">
+
        <span class="title">
+
          <span class="bold desktop">
+
            {profile.name ? utils.formatName(profile.name, config) : profile.address}
+
          </span>
+
          <span class="bold mobile">
+
            {profile.name ? utils.formatName(profile.name, config) : utils.formatAddress(profile.address)}
+
          </span>
+
          {#if profile.name && profile.org}
+
            <span class="badge">org</span>
+
          {/if}
+
        </span>
+
        <div class="links">
+
          {#if profile.url}
+
            <a class="url" href={profile.url}>
+
              <span class="mobile">
+
                <Icon name="url" fill inline />
+
              </span>
+
              <span class="desktop">
+
                {profile.url}
+
              </span>
+
            </a>
+
          {/if}
+
          {#if profile.twitter}
+
            <a class="url" href="https://twitter.com/{profile.twitter}">
+
              <Icon name="twitter" fill inline />
+
            </a>
+
          {/if}
+
          {#if profile.github}
+
            <a class="url" href="https://github.com/{profile.github}">
+
              <Icon name="github" fill inline />
+
            </a>
+
          {/if}
+
        </div>
+
      </div>
+
    </header>
+

+
    <div class="fields">
+
      <!-- Address -->
+
      <div class="label">Address</div>
+
      <div class="desktop"><Address {config} {profile} address={profile.address} /></div>
+
      <div class="mobile"><Address compact {config} {profile} address={profile.address} /></div>
+
      <div class="desktop" />
+
      <!-- Owner -->
+
      {#if profile.org}
+
        <div class="label">Owner</div>
+
        <div class="desktop"><Address resolve {config} address={profile.org.owner} /></div>
+
        <div class="mobile"><Address compact resolve {config} address={profile.org.owner} /></div>
+
        <div class="desktop">
+
          {#await account && profile.org.isMember(account, config) then isMember}
+
            {#if isOwner(profile.org) || isMember}
+
              <button class="tiny secondary" on:click={transferOwnership}>
+
                Transfer
+
              </button>
+
            {/if}
+
          {/await}
+
        </div>
+
        <!-- Org Treasury -->
+
        {#await getOrgTreasury(profile.org) then tokens}
+
          {#if tokens && tokens.length > 0}
+
            <div class="label">Treasury</div>
+
            <div>
+
              {#each tokens as token}
+
                {` ${utils.formatBalance(token.balance, token.decimals)} ${token.symbol} `}
+
              {/each}
+
            </div>
+
            <div class="desktop" />
+
          {/if}
+
        {/await}
+
      {:else}
+
        <!-- Project anchors -->
+
        {#if profile.anchorsAccount}
+
          <div class="label">Anchors</div>
+
          <div class="desktop"><Address {config} address={profile.anchorsAccount} /></div>
+
          <div class="mobile"><Address compact {config} address={profile.anchorsAccount} /></div>
+
          <div class="desktop" />
+
        {/if}
+
      {/if}
+
      <!-- Seed Address -->
+
      {#if profile.seed && profile.seed.valid}
+
        <div class="label">Seed</div>
+
        <SeedAddress seed={profile.seed} port={config.seed.link.port} />
+
      {/if}
+
      <!-- Org Name/Profile -->
+
      <div class="label">Profile</div>
+
      {#if profile.org}
+
        {#if utils.isAddressEqual(profile.address, profile.org.address)}
+
          <div class="overflow-text">
+
            {#if profile.name}
+
              <a href={profile.registry(config)} class="link">{profile.name}</a>
+
            {:else}
+
              <span class="subtle">Not set</span>
+
            {/if}
+
          </div>
+
          <div class="desktop">
+
            {#await isOrgAuthorized(profile.org)}
+
              <!-- Loading -->
+
            {:then authorized}
+
              {#if authorized}
+
                <button class="tiny secondary" on:click={setName}>
+
                  Set
+
                </button>
+
              {/if}
+
            {/await}
+
          </div>
+
        {/if}
+
        <!-- Quorum -->
+
        {#await profile.org.getSafe(config) then safe}
+
          {#if safe}
+
            <div class="label">Quorum</div>
+
            <div>
+
              {safe.threshold} <span class="faded">of</span> {safe.owners.length}
+
            </div>
+
            <div class="desktop"/>
+
          {/if}
+
        {/await}
+
      {:else}
+
        <!-- User Profile -->
+
        <div>
+
          {#if profile.name}
+
            <a href={profile.registry(config)} class="link">{profile.name}</a>
+
          {:else}
+
            <span class="subtle">Not set</span>
+
          {/if}
+
        </div>
+
        <div class="desktop">
+
          {#if isUserAuthorized(profile.address)}
+
            <button class="tiny secondary" on:click={setName}>
+
              Set
+
            </button>
+
          {/if}
+
        </div>
+
      {/if}
+
    </div>
+

+
    {#if profile.org}
+
      {#await profile.org.getMembers(config)}
+
        <Loading center />
+
      {:then members}
+
        {#if members.length > 0}
+
          <div class="members">
+
            <!-- We don't need to catch errors here, since it's not defined by user input and defaults to ETH addresses -->
+
            {#await Profile.getMulti(members, config)}
+
              <Loading small />
+
            {:then members}
+
              {#each members as profile}
+
                <div class="member">
+
                  <div class="member-icon">
+
                    <Link to="/{profile.address}">
+
                      <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
+
                    </Link>
+
                  </div>
+
                  <div class="desktop">
+
                    <Address address={profile.address} compact
+
                      resolve noBadge noAvatar {profile} {config} />
+
                  </div>
+
                </div>
+
              {/each}
+
            {/await}
+
          </div>
+
        {/if}
+
      {:catch err}
+
        <Message error>
+
          <strong>Error: </strong> failed to load org members: {err.message}.
+
        </Message>
+
      {/await}
+
    {:else}
+
      {#await Org.getOrgsByMember(profile.address, config)}
+
        <Loading center />
+
      {:then orgs}
+
        {#if orgs.length > 0}
+
          <div class="members">
+
            {#each orgs as org}
+
              <div class="member">
+
                <!-- We don't need to catch errors here, since it's not defined by user input and defaults to ETH addresses -->
+
                {#await Profile.get(org.address, ProfileType.Minimal, config)}
+
                  <Loading small margins />
+
                {:then profile}
+
                  <div class="member-icon">
+
                    <Link to="/{profile.address}">
+
                      <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
+
                    </Link>
+
                  </div>
+
                  <div class="desktop">
+
                    <Address address={profile.address} compact
+
                      resolve noBadge noAvatar {profile} {config} />
+
                  </div>
+
                {/await}
+
              </div>
+
            {/each}
+
          </div>
+
        {/if}
+
      {:catch err}
+
        <Message error>
+
          <strong>Error: </strong> failed to load orgs: {err.message}.
+
        </Message>
+
      {/await}
+
    {/if}
+
    <Projects {profile} {account} config={profile.config(config)} />
+
  </main>
+

+
  <svelte:component this={setNameForm} entity={profile.org ?? new User(profile.address)} {config} on:close={() => setNameForm = null} />
+
  <svelte:component this={transferOwnerForm} org={profile.org} {config} on:close={() => transferOwnerForm = null} />
+
{:catch err}
+
  {#if err instanceof NotFoundError}
+
    <NotFound title={addressOrName} subtitle="Sorry, the requested address or domain was not found." />
+
  {:else}
+
    <Error error={err} />
+
  {/if}
+
{/await}
added src/Redirect.svelte
@@ -0,0 +1,10 @@
+
<script>
+
  import { onMount } from "svelte";
+
  import { navigate } from "svelte-routing";
+

+
  export let to;
+

+
  onMount(() => {
+
    navigate(to, { replace: true });
+
  });
+
</script>
modified src/base/orgs/Create.svelte
@@ -135,7 +135,7 @@
    </span>

    <span slot="actions">
-
      <button on:click={() => navigate(`/orgs/${org?.address}`)}>
+
      <button on:click={() => navigate(`/${org?.address}`)}>
        Done
      </button>
    </span>
modified src/base/orgs/Routes.svelte
@@ -1,9 +1,8 @@
<script lang="ts">
  import { Route } from "svelte-routing";
  import Index from '@app/base/orgs/Index.svelte';
-
  import View from '@app/base/orgs/View.svelte';
  import type { Config } from '@app/config';
-
  import { getSearchParam } from '@app/utils';
+
  import Redirect from "@app/Redirect.svelte";

  export let config: Config;
</script>
@@ -12,6 +11,6 @@
  <Index {config} />
</Route>

-
<Route path="/orgs/:address" let:params let:location>
-
  <View {config} addressOrName={params.address} action={getSearchParam("action", location)} />
+
<Route path="/orgs/:addressOrName" let:params>
+
  <Redirect to="/{params.addressOrName}" />
</Route>
deleted src/base/orgs/View.svelte
@@ -1,369 +0,0 @@
-
<script lang="ts">
-
  import type { SvelteComponent } from 'svelte';
-
  import type { Config } from '@app/config';
-
  import { formatName, explorerLink, formatAddress } from '@app/utils';
-
  import { session } from '@app/session';
-
  import Loading from '@app/Loading.svelte';
-
  import Modal from '@app/Modal.svelte';
-
  import Error from '@app/Error.svelte';
-
  import Icon from '@app/Icon.svelte';
-
  import SetName from '@app/ens/SetName.svelte';
-
  import Address from '@app/Address.svelte';
-
  import Avatar from '@app/Avatar.svelte';
-
  import * as utils from '@app/utils';
-

-
  import { Org } from '@app/base/orgs/Org';
-
  import TransferOwnership from '@app/base/orgs/TransferOwnership.svelte';
-
  import { Profile, ProfileType } from '@app/profile';
-
  import Projects from './View/Projects.svelte';
-
  import Link from '@app/Link.svelte';
-
  import SeedAddress from '@app/SeedAddress.svelte';
-
  import Message from '@app/Message.svelte';
-

-
  export let addressOrName: string;
-
  export let config: Config;
-
  export let action: string | null = null;
-

-
  const back = () => window.history.back();
-

-
  let setNameForm: typeof SvelteComponent | null =
-
    action === "setName" ? SetName : null;
-
  const setName = () => {
-
    setNameForm = SetName;
-
  };
-

-
  let transferOwnerForm: typeof SvelteComponent | null = null;
-
  const transferOwnership = () => {
-
    transferOwnerForm = TransferOwnership;
-
  };
-
  $: getOrgTreasury = async (org: Org): Promise<Array<utils.Token>| undefined> => {
-
    const addressType = await utils.identifyAddress(org.owner, config);
-
    // We query the org treasury only for Gnosis Safes, to maintain some privacy for EOA org owners.
-
    if (addressType === utils.AddressType.Safe) {
-
      try {
-
        const tokens = await utils.getTokens(org.owner, config);
-
        const balance = await config.provider.getBalance(org.owner);
-

-
        if (! balance.isZero()) {
-
          // To maintain the format we hardcode the ETH specs.
-
          return [{ balance, decimals: 18, logo: "", name: "Ethereum", symbol: "ETH" }, ...tokens];
-
        } else {
-
          return tokens;
-
        }
-
      } catch (e) {
-
        console.error(e);
-
      }
-
    }
-
  };
-
  $: account = $session && $session.address;
-
  $: isOwner = (org: Org): boolean => $session
-
    ? utils.isAddressEqual(org.owner, $session.address)
-
    : false;
-
  $: isAuthorized = async (org: Org): Promise<boolean> => {
-
    if ($session) {
-
      if (isOwner(org)) {
-
        return true;
-
      }
-
      return await org.isMember($session.address, config);
-
    }
-
    return false;
-
  };
-
</script>
-

-
<style>
-
  main {
-
    padding: 5rem 0;
-
    width: 720px;
-
  }
-
  main > header {
-
    display: flex;
-
    align-items: center;
-
    justify-content: left;
-
    margin-bottom: 2rem;
-
  }
-
  main > header > * {
-
    margin: 0 1rem 0 0;
-
  }
-
  .info {
-
    display: flex;
-
    flex-direction: column;
-
    justify-content: center;
-
    align-items: left;
-
  }
-
  .info a {
-
    border: none;
-
  }
-
  .fields {
-
    display: grid;
-
    grid-template-columns: 5rem 4fr 2fr;
-
    grid-gap: 1rem 2rem;
-
    margin-bottom: 1rem;
-
  }
-
  .fields > div {
-
    justify-self: start;
-
    align-self: center;
-
    height: 2rem;
-
    line-height: 2rem;
-
  }
-
  .avatar {
-
    width: 64px;
-
    height: 64px;
-
  }
-
  .title {
-
    display: flex;
-
    align-items: center;
-
  }
-
  .links {
-
    display: flex;
-
    align-items: center;
-
    justify-content: left;
-
  }
-
  .overflow-text {
-
    width: 100%;
-
    overflow: hidden;
-
    text-overflow: ellipsis;
-
    white-space: nowrap;
-
  }
-
  .url {
-
    display: flex; /* Ensures correct vertical positioning of icons */
-
    margin-right: 1rem;
-
    height: 1.6rem;
-
  }
-
  .members {
-
    margin-top: 2rem;
-
    align-items: center;
-
    display: flex;
-
    flex-wrap: wrap;
-
  }
-
  .members .member {
-
    display: flex;
-
    align-items: center;
-
    margin-right: 2rem;
-
    margin-bottom: 1rem;
-
  }
-
  .members .member:last-child {
-
    margin-right: 0;
-
  }
-
  .members .member-icon {
-
    width: 2rem;
-
    height: 2rem;
-
    margin-right: 1rem;
-
  }
-
  @media (max-width: 720px) {
-
    main {
-
      width: 100%;
-
      padding: 1.5rem;
-
    }
-
    .fields {
-
      grid-template-columns: 5rem auto;
-
    }
-
    .members .member {
-
      margin-right: 1rem;
-
    }
-
  }
-
</style>
-

-
<svelte:head>
-
  <title>{utils.formatOrg(addressOrName, config)}</title>
-
</svelte:head>
-

-
{#await Org.get(addressOrName, config)}
-
  <main class="off-centered">
-
    <Loading center />
-
  </main>
-
{:then org}
-
  {#if org}
-
    <main>
-
      {#await Profile.get(org.name ?? org.address, ProfileType.Full, config)}
-
        <div class="off-centered">
-
          <Loading center />
-
        </div>
-
      {:then profile}
-
        <header>
-
          <div class="avatar">
-
            <Avatar source={profile.avatar ?? org.address} address={org.address} />
-
          </div>
-
          <div class="info">
-
            <span class="title">
-
              <span class="bold desktop">
-
                {profile.name ? formatName(profile.name, config) : profile.address}
-
              </span>
-
              <span class="bold mobile">
-
                {profile.name ? formatName(profile.name, config) : formatAddress(profile.address)}
-
              </span>
-
              {#if profile.name && profile.address === org.owner}
-
                <span class="badge">org</span>
-
              {/if}
-
            </span>
-
            <div class="links">
-
              {#if profile.url}
-
                <a class="url" href={profile.url}>
-
                  <span class="mobile">
-
                    <Icon name="url" fill inline />
-
                  </span>
-
                  <span class="desktop">
-
                    {profile.url}
-
                  </span>
-
                </a>
-
              {/if}
-
              {#if profile.twitter}
-
                <a class="url" href="https://twitter.com/{profile.twitter}">
-
                  <Icon name="twitter" fill inline />
-
                </a>
-
              {/if}
-
              {#if profile.github}
-
                <a class="url" href="https://github.com/{profile.github}">
-
                  <Icon name="github" fill inline />
-
                </a>
-
              {/if}
-
            </div>
-
          </div>
-
        </header>
-

-
        <div class="fields">
-
          <!-- Address -->
-
          <div class="label">Address</div>
-
          <div class="desktop"><Address {config} address={org.address} /></div>
-
          <div class="mobile"><Address compact {config} address={org.address} /></div>
-
          <div class="desktop" />
-
          <!-- Owner -->
-
          <div class="label">Owner</div>
-
          <div class="desktop"><Address resolve {config} address={org.owner} /></div>
-
          <div class="mobile"><Address compact resolve {config} address={org.owner} /></div>
-
          <div class="desktop">
-
            {#if isOwner(org) || (account && org.isMember(account, config))}
-
              <button class="tiny secondary" on:click={transferOwnership}>
-
                Transfer
-
              </button>
-
            {/if}
-
          </div>
-
          {#await getOrgTreasury(org) then tokens}
-
            {#if tokens && tokens.length > 0}
-
              <div class="label">Treasury</div>
-
              <div>
-
                {#each tokens as token}
-
                  {` ${utils.formatBalance(token.balance, token.decimals)} ${token.symbol} `}
-
                {/each}
-
              </div>
-
              <div class="desktop" />
-
            {/if}
-
          {/await}
-
          <!-- Seed Address -->
-
          {#if profile.seed?.valid}
-
            <div class="label">Seed</div>
-
            <SeedAddress seed={profile.seed} port={config.seed.link.port} />
-
          {:else if !profile.seed?.valid}
-
            <div class="label">Seed</div>
-
            <div class="subtle">✗ Seed configuration is invalid</div>
-
            <div class="desktop" />
-
          {/if}
-
          <!-- Name/Profile -->
-
          <div class="label">Profile</div>
-
          <!-- Only show the name if we aren't already using the name of the owner -->
-
          {#if utils.isAddressEqual(profile.address, org.address)}
-
            <div class="overflow-text">
-
              {#if profile.name}
-
                <a href={profile.registry(config)} class="link">{profile.name}</a>
-
              {:else}
-
                <span class="subtle">Not set</span>
-
              {/if}
-
            </div>
-
            <div class="desktop">
-
              {#await isAuthorized(org)}
-
                <!-- Loading -->
-
              {:then authorized}
-
                {#if authorized}
-
                  <button class="tiny secondary" on:click={setName}>
-
                    Set
-
                  </button>
-
                {/if}
-
              {/await}
-
            </div>
-
          {:else}
-
            <div class="subtle">
-
              Using owner's profile.
-
            </div>
-
            <div class="desktop">
-
              {#await isAuthorized(org) then authorized}
-
                {#if authorized}
-
                  <button class="tiny secondary" on:click={setName}>
-
                    Change
-
                  </button>
-
                {/if}
-
              {/await}
-
            </div>
-
          {/if}
-
          <!-- Quorum -->
-
          {#await org.getSafe(config) then safe}
-
            {#if safe}
-
              <div class="label">Quorum</div>
-
              <div>
-
                {safe.threshold} <span class="faded">of</span> {safe.owners.length}
-
              </div>
-
              <div class="desktop"/>
-
            {/if}
-
          {/await}
-
        </div>
-

-
        {#await org.getMembers(config)}
-
          <Loading center />
-
        {:then members}
-
          {#if members.length > 0}
-
            <div class="members">
-
              {#await Profile.getMulti(members, config)}
-
                <Loading small />
-
              {:then members}
-
                {#each members as profile}
-
                  <div class="member">
-
                    <div class="member-icon">
-
                      <Link to="/users/{profile.address}">
-
                        <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
-
                      </Link>
-
                    </div>
-
                    <div class="desktop">
-
                      <Address address={profile.address} compact
-
                        resolve noAvatar {profile} {config} />
-
                    </div>
-
                  </div>
-
                {/each}
-
              {/await}
-
            </div>
-
          {/if}
-
        {:catch err}
-
          <Message error>
-
            <strong>Error: </strong> failed to load org members: {err.message}.
-
          </Message>
-
        {/await}
-

-
        <Projects {org} {account} config={profile.config(config)} />
-
      {:catch err}
-
        <Message error>
-
          <strong>Error: </strong> {err.message}.
-
        </Message>
-
      {/await}
-
    </main>
-
  {:else}
-
    <Modal subtle>
-
      <span slot="title">🏜️</span>
-
      <span slot="body">
-
        <p class="highlight"><strong>{addressOrName}</strong></p>
-
        <p>Sorry, there is no Org at this address.</p>
-
        {#if utils.isAddress(addressOrName)}
-
          <p>
-
            <a href={explorerLink(addressOrName, config)} class="link" target="_blank">View in explorer</a>
-
            <span class="faded">↗</span>
-
          </p>
-
        {/if}
-
      </span>
-
      <span slot="actions">
-
        <button on:click={back}>
-
          Back
-
        </button>
-
      </span>
-
    </Modal>
-
  {/if}
-
  <svelte:component this={setNameForm} entity={org} {config} on:close={() => setNameForm = null} />
-
  <svelte:component this={transferOwnerForm} {org} {config} on:close={() => transferOwnerForm = null} />
-
{:catch err}
-
  <Error error={err} />
-
{/await}
modified src/base/orgs/View/Projects.svelte
@@ -1,14 +1,15 @@
<script lang="ts">
  import type { Config } from "@app/config";
-
  import type { Org } from "@app/base/orgs/Org";
-
  import type { PendingProject, Project } from "@app/project";
+
  import { Org } from "@app/base/orgs/Org";
+
  import type * as proj from "@app/project";
  import Loading from "@app/Loading.svelte";
  import Message from "@app/Message.svelte";
  import Widget from '@app/base/projects/Widget.svelte';
  import Anchor from './Anchor.svelte';
  import { formatCommit } from "@app/utils";
+
  import type { Profile } from "@app/profile";

-
  export let org: Org;
+
  export let profile: Profile;
  export let config: Config;
  export let account: string | null;

@@ -16,12 +17,14 @@
    getProjects = queryProjects;
  };

-
  $: queryProjects = async (): Promise<(Project | PendingProject)[]> => {
-
    if (account) {
-
      const result = await org.isMember(account, config);
-
      return result ? org.getAllProjects(config) : org.getProjects(config);
+
  $: queryProjects = async (): Promise<(proj.Project | proj.PendingProject)[]> => {
+
    if (profile.org) {
+
      if (account) {
+
        const result = await profile.org.isMember(account, config);
+
        return result ? profile.org.getAllProjects(config) : profile.org.getProjects(config);
+
      }
    }
-
    return org.getProjects(config);
+
    return [];
  };
  $: getProjects = queryProjects;
</script>
@@ -40,36 +43,65 @@
</style>

<div class="projects">
-
  {#await getProjects()}
-
    <Loading center />
-
  {:then projects}
-
    {#each projects as project}
-
      <div class="project">
-
        {#if "safeTxHash" in project} <!-- Pending project -->
-
          <Widget {project} org={org.address} {config} faded>
-
            <span slot="stateHash">
-
              <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
-
              <span class="desktop">commit {project.anchor.stateHash}</span>
-
            </span>
-
            <span class="anchor" slot="actions">
-
              {#if org.safe && account}
-
                <Anchor {project} safe={org.safe} on:success={() => updateRecords()} {account} {config} />
-
              {/if}
-
            </span>
-
          </Widget>
-
        {:else} <!-- Anchored project -->
-
          <Widget {project} org={org.address} {config}>
-
            <span slot="stateHash">
-
              <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
-
              <span class="desktop">commit {project.anchor.stateHash}</span>
-
            </span>
-
          </Widget>
-
        {/if}
-
      </div>
-
    {/each}
-
  {:catch err}
-
    <Message error>
-
      <strong>Error: </strong> failed to load projects: {err.message}.
-
    </Message>
-
  {/await}
+
  {#if profile.org}
+
    {#await getProjects()}
+
      <Loading center />
+
    {:then projects}
+
      {#each projects as project}
+
        <div class="project">
+
          {#if "safeTxHash" in project} <!-- Pending project -->
+
            <Widget {project} addressOrName={profile.name ?? profile.address} {config} faded>
+
              <span slot="stateHash">
+
                <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
+
                <span class="desktop">commit {project.anchor.stateHash}</span>
+
              </span>
+
              <span class="anchor" slot="actions">
+
                {#if profile.org.safe && account}
+
                  <Anchor {project} safe={profile.org.safe} on:success={() => updateRecords()} {account} {config} />
+
                {/if}
+
              </span>
+
            </Widget>
+
          {:else} <!-- Anchored project -->
+
            <Widget {project} addressOrName={profile.name ?? profile.address} {config}>
+
              <span slot="stateHash">
+
                <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
+
                <span class="desktop">commit {project.anchor.stateHash}</span>
+
              </span>
+
            </Widget>
+
          {/if}
+
        </div>
+
      {/each}
+
    {:catch err}
+
      <Message error>
+
        <strong>Error: </strong> failed to load anchored projects: {err.message}.
+
      </Message>
+
    {/await}
+
  {:else}
+
    <div class="projects">
+
      {#if profile.anchorsAccount}
+
        {#await Org.get(profile.anchorsAccount, config)}
+
          <Loading center fadeIn />
+
        {:then org}
+
          {#if org}
+
            {#await org.getProjects(config) then projects}
+
              {#each projects as project}
+
                <div class="project">
+
                  <Widget {project} addressOrName={profile.name ?? profile.address} {config}>
+
                    <span slot="stateHash">
+
                      <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
+
                      <span class="desktop">commit {project.anchor.stateHash}</span>
+
                    </span>
+
                  </Widget>
+
                </div>
+
              {/each}
+
            {:catch err}
+
              <Message error>
+
                <strong>Error: </strong> failed to load projects: {err.message}.
+
              </Message>
+
            {/await}
+
          {/if}
+
        {/await}
+
      {/if}
+
    </div>
+
  {/if}
</div>
modified src/base/projects/Browser.svelte
@@ -23,7 +23,7 @@
  export let revision: string;
  export let path: string;

-
  let { urn, org, user, seed, peer, project, config, branches } = source;
+
  let { urn, addressOrName, seed, peer, project, config, branches } = source;

  // This is reactive to respond to path changes that don't originate from this
  // component, eg. when using the browser's "back" button.
@@ -70,10 +70,8 @@
    // Replaces path with current path if none passed.
    if (path === undefined) path = state.path;

-
    if (org) {
-
      navigate(proj.path({ peer, urn, org, revision, path }));
-
    } else if (user) {
-
      navigate(proj.path({ peer, urn, user, revision, path }));
+
    if (addressOrName) {
+
      navigate(proj.path({ peer, urn, addressOrName, revision, path }));
    } else if (seed) {
      navigate(proj.path({ peer, urn, seed, revision, path }));
    } else {
modified src/base/projects/Commit.svelte
@@ -10,16 +10,14 @@
  export let locator: string;
  export let source: any;

-
  const { org, user, peer, seed } = source;
+
  const { addressOrName, peer, seed } = source;

  const navigateCommit = (path: string, content?: proj.ProjectContent) => {
    // Replaces path with current path if none passed.
    if (path === undefined) path = "/";

-
    if (org) {
-
      navigate(proj.path({ content, peer, urn, org, revision, path }));
-
    } else if (user) {
-
      navigate(proj.path({ content, peer, urn, user, revision, path }));
+
    if (addressOrName) {
+
      navigate(proj.path({ content, peer, urn, addressOrName, revision, path }));
    } else if (seed) {
      navigate(proj.path({ content, peer, urn, seed, revision, path }));
    } else {
modified src/base/projects/History.svelte
@@ -12,7 +12,7 @@
  export let revision: string;
  export let path: string;

-
  let { urn, user, seed, org, peer, config, project, branches } = source;
+
  let { urn, seed, addressOrName, peer, config, project, branches } = source;

  // Bind content to commit history to trigger updates in parent components.
  $: [revision_,] = splitPrefixFromPath(locator, branches, project.head);
@@ -23,10 +23,8 @@
    // Replaces path with current path if none passed.
    if (path === undefined) path = "/";

-
    if (org) {
-
      navigate(proj.path({ content, peer, urn, org, revision, path }));
-
    } else if (user) {
-
      navigate(proj.path({ content, peer, urn, user, revision, path }));
+
    if (addressOrName) {
+
      navigate(proj.path({ content, peer, urn, addressOrName, revision, path }));
    } else if (seed) {
      navigate(proj.path({ content, peer, urn, seed, revision, path }));
    } else {
modified src/base/projects/Routes.svelte
@@ -2,6 +2,7 @@
  import { Route } from "svelte-routing";
  import View from '@app/base/projects/View.svelte';
  import type { Config } from '@app/config';
+
  import Redirect from "@app/Redirect.svelte";

  export let config: Config;

@@ -9,22 +10,24 @@

<!-- With an Seed context -->

-
<Route path="/seeds/:seed/projects/:id/*" let:params>
+
<Route path="/seeds/:seed/:id/*" let:params>
  <View {config} seed={params.seed} id={params.id} />
</Route>

-
<Route path="/seeds/:seed/projects/:id/remotes/:peer/*" let:params>
+
<Route path="/seeds/:seed/:id/remotes/:peer/*" let:params>
  <View {config} seed={params.seed} peer={params.peer} id={params.id} />
</Route>

-
<!-- With an Org context -->
-

-
<Route path="/orgs/:org/projects/:id/*" let:params>
-
  <View {config} org={params.org} id={params.id} />
+
<!-- Explicit user and org context, will at some point be replaced by the generic route -->
+
<Route path="/orgs/:addressOrName/projects/:id/*" let:params>
+
  <Redirect to="/{params.addressOrName}/{params.id}/{params["*"]}" />
</Route>

-
<!-- With a User context -->
+
<Route path="/users/:addressOrName/projects/:id/*" let:params>
+
  <Redirect to="/{params.addressOrName}/{params.id}/{params["*"]}" />
+
</Route>
+
<!-- End of eventual dropped routes -->

-
<Route path="/users/:user/projects/:id/*" let:params>
-
  <View {config} user={params.user} id={params.id} />
+
<Route path="/:addressOrName/:id/*" let:params>
+
  <View {config} addressOrName={params.addressOrName} id={params.id} />
</Route>
modified src/base/projects/View.svelte
@@ -3,7 +3,6 @@
  import type { Config } from '@app/config';
  import * as proj from '@app/project';
  import Loading from '@app/Loading.svelte';
-
  import Modal from '@app/Modal.svelte';
  import Avatar from '@app/Avatar.svelte';
  import { Profile, ProfileType } from '@app/profile';
  import type { Info } from '@app/project';
@@ -14,27 +13,25 @@

  import Header from '@app/base/projects/Header.svelte';
  import ProjectContentRoutes from '@app/base/projects/ProjectContentRoutes.svelte';
+
  import NotFound from '@app/NotFound.svelte';

  export let id: string; // Project name or URN.
-
  export let org = "";
-
  export let user = "";
+
  export let addressOrName = "";
  export let seed = "";
  export let peer = "";
  export let config: Config;

-
  let parentName = formatOrg(org || user, config);
+
  let parentName = formatOrg(addressOrName, config);
  let pageTitle = parentName ? `${parentName}/${id}` : id;
  let projectInfo: Info | null = null;
  let revision: string;
  let content: proj.ProjectContent;
  let path: string;
-
  let getProject = new Promise<{ profile?: Profile; seed?: Seed } | null>(resolve => {
-
    if (org) {
-
      Profile.get(org, ProfileType.Project, config).then(p => resolve({ profile: p }));
-
    } else if (user) {
-
      Profile.get(user, ProfileType.Project, config).then(p => resolve({ profile: p }));
+
  let getProject = new Promise<{ profile?: Profile | null; seed?: Seed } | null>((resolve, reject) => {
+
    if (addressOrName) {
+
      Profile.get(addressOrName, ProfileType.Project, config).then(p => resolve({ profile: p })).catch(err => reject(err.message));
    } else if (seed) {
-
      Seed.get(config.withSeed({ host: seed })).then(s => resolve({ seed: s }));
+
      Seed.get(config.withSeed({ host: seed })).then(s => resolve({ seed: s })).catch(err => reject(err.message));
    } else {
      resolve(null);
    }
@@ -44,16 +41,12 @@
    const cfg = seedInstance && seedInstance.valid ? config.withSeed(seedInstance) : config;
    const info = await proj.getInfo(id, cfg);
    const urn = isRadicleId(id) ? id : info.meta.urn;
-
    const anchors = await getAllAnchors(config, urn, profile?.anchorsAccount ?? org);
-

+
    const anchors = await getAllAnchors(config, urn, profile?.anchorsAccount ?? addressOrName);
    let branches = Array([info.meta.defaultBranch, info.head]) as [string, string][];
    let peers: proj.Peer[] = [];

    projectInfo = info;

-
    // Replace project name with URN, in URL bar.
-
    navigate(proj.path({ peer, urn, org, revision, path, seed }), { replace: true });
-

    // Checks for delegates returned from seed node, as feature check of the seed node
    if (info.meta.delegates) {
      // Check for selected peer to override available branches.
@@ -63,15 +56,9 @@
      }
      peers = await proj.getPeers(urn, cfg);
    }
-
    return { urn, org, user, seed, peer, project: info, branches, peers, config: cfg, profile, anchors };
+
    return { urn, addressOrName, seed, peer, project: info, branches, peers, config: cfg, profile, anchors };
  });

-
  const parentUrl = (profile: Profile) => {
-
    return org
-
      ? `/orgs/${profile.nameOrAddress}`
-
      : `/users/${profile.nameOrAddress}`;
-
  };
-

  $: if (projectInfo) {
    const baseName = parentName
      ? `${parentName}/${projectInfo.meta.name}`
@@ -85,7 +72,7 @@
  }

  function updateRouteParams({ detail: newParams }: { detail: { urn: string; path: string; revision: string; peer: string; content: proj.ProjectContent } }) {
-
    let newLocation = proj.path({ user, org, seed, urn: newParams.urn, content: newParams.content, peer: newParams.peer, revision: newParams.revision, path: newParams.path });
+
    let newLocation = proj.path({ addressOrName, seed, urn: newParams.urn, content: newParams.content, peer: newParams.peer, revision: newParams.revision, path: newParams.path });
    if (newLocation !== window.location.pathname) {
      navigate(newLocation);
    }
@@ -94,8 +81,6 @@
    if (path !== newParams.path) path = newParams.path;
    if (peer !== newParams.peer) peer = newParams.peer;
  }
-

-
  const back = () => window.history.back();
</script>

<style>
@@ -172,12 +157,12 @@
    <header>
      <div class="title bold">
        {#if result.profile}
-
          <a class="org-avatar" title={result.profile.nameOrAddress} href={parentUrl(result.profile)}>
+
          <a class="org-avatar" title={result.profile.nameOrAddress} href="/{result.profile.nameOrAddress}">
            <Avatar source={result.profile.avatar || result.profile.address} address={result.profile.address}/>
          </a>
          <span class="divider">/</span>
        {/if}
-
        <Link to={proj.path({ urn: result.urn, user, org, seed })}>{result.project.meta.name}</Link>
+
        <Link to={proj.path({ urn: result.urn, addressOrName, seed })}>{result.project.meta.name}</Link>
        {#if peer}
          <span class="divider" title={peer}>/ {formatSeedId(peer)}</span>
        {/if}
@@ -205,17 +190,6 @@
      </div>
    {/await}
  {:catch}
-
    <Modal subtle>
-
      <span slot="title">🏜️</span>
-
      <span slot="body">
-
        <p class="highlight"><strong>{id}</strong></p>
-
        <p>This project was not found.</p>
-
      </span>
-
      <span slot="actions">
-
        <button on:click={back}>
-
          Back
-
        </button>
-
      </span>
-
    </Modal>
+
    <NotFound title={id} subtitle="This project was not found." />
  {/await}
</main>
modified src/base/projects/Widget.svelte
@@ -16,8 +16,7 @@

  export let project: proj.Project;
  export let config: Config;
-
  export let org: string | undefined = undefined;
-
  export let user: string | undefined = undefined;
+
  export let addressOrName: string | undefined = undefined;
  export let seed: string | undefined = undefined;
  export let faded = false;

@@ -40,8 +39,7 @@
      navigate(
        proj.path({
          urn: project.id,
-
          org,
-
          user,
+
          addressOrName,
          seed,
          revision: project.anchor?.stateHash,
        })
modified src/base/resolver/Query.svelte
@@ -16,10 +16,8 @@
    if (query) {
      if (ethers.utils.isAddress(query)) {
        const addressType = query && await utils.identifyAddress(query, config);
-
        if (addressType === utils.AddressType.Org) {
-
          navigate(`/orgs/${query}`, { replace: true });
-
        } else if (addressType === utils.AddressType.EOA) {
-
          navigate(`/users/${query}`, { replace: true });
+
        if (addressType === utils.AddressType.Org || addressType === utils.AddressType.EOA) {
+
          navigate(`/${query}`, { replace: true });
        }
      } else if (utils.isRadicleId(query)) {
        // Go to Radicle project.
@@ -33,10 +31,8 @@
          // address type is an EOA and jumps to the user page else it just goes to the registration.
          const address = await utils.resolveLabel(label, config);
          const addressType = address && await utils.identifyAddress(address, config);
-
          if (addressType === utils.AddressType.Org) {
-
            navigate(`/orgs/${address}`, { replace: true });
-
          } else if (addressType === utils.AddressType.EOA) {
-
            navigate(`/users/${address}`, { replace: true });
+
          if (addressType === utils.AddressType.Org || addressType === utils.AddressType.EOA) {
+
            navigate(`/${address}`, { replace: true });
          } else {
            navigate(`/registrations/${label}`, { replace: true });
          }
modified src/base/seeds/View.svelte
@@ -4,13 +4,11 @@
  import Widget from "@app/base/projects/Widget.svelte";
  import Loading from "@app/Loading.svelte";
  import SeedAddress from "@app/SeedAddress.svelte";
-
  import Modal from "@app/Modal.svelte";
+
  import NotFound from "@app/NotFound.svelte";

  export let config: Config;
  export let seedAddress: string;

-
  const back = () => window.history.back();
-

  config = config.withSeed({ host: seedAddress });
</script>

@@ -130,16 +128,5 @@
    {/if}
  </main>
{:catch}
-
  <Modal subtle>
-
    <span slot="title">🏜️</span>
-
    <span slot="body">
-
      <p class="highlight"><strong>{seedAddress}</strong></p>
-
      <p>Not able to query information from this seed.</p>
-
    </span>
-
    <span slot="actions">
-
      <button on:click={back}>
-
        Back
-
      </button>
-
    </span>
-
  </Modal>
+
  <NotFound title={seedAddress} subtitle="Not able to query information from this seed." />
{/await}
modified src/base/users/Routes.svelte
@@ -1,12 +1,8 @@
<script lang="ts">
  import { Route } from "svelte-routing";
-
  import View from '@app/base/users/View.svelte';
-
  import type { Config } from '@app/config';
-
  import { getSearchParam } from '@app/utils';
-

-
  export let config: Config;
+
  import Redirect from "@app/Redirect.svelte";
</script>

-
<Route path="/users/:address" let:params let:location>
-
  <View {config} addressOrName={params.address} action={getSearchParam("action", location)} />
+
<Route path="/users/:addressOrName" let:params>
+
  <Redirect to="/{params.addressOrName}" />
</Route>
deleted src/base/users/View.svelte
@@ -1,260 +0,0 @@
-
<script lang="ts">
-
  import type { SvelteComponent } from 'svelte';
-
  import type { Config } from '@app/config';
-
  import Icon from '@app/Icon.svelte';
-
  import Address from '@app/Address.svelte';
-
  import Avatar from '@app/Avatar.svelte';
-
  import { ProfileType, Profile } from '@app/profile';
-
  import Loading from '@app/Loading.svelte';
-
  import { Org } from '@app/base/orgs/Org';
-
  import Message from '@app/Message.svelte';
-
  import Project from '@app/base/projects/Widget.svelte';
-
  import { session } from '@app/session';
-
  import { formatCommit, isAddressEqual } from '@app/utils';
-
  import Error from '@app/Error.svelte';
-
  import SetName from '@app/ens/SetName.svelte';
-
  import { User } from '@app/base/users/User';
-
  import Link from '@app/Link.svelte';
-
  import SeedAddress from '@app/SeedAddress.svelte';
-

-
  export let addressOrName: string;
-
  export let config: Config;
-
  export let action: string | null = null;
-

-
  let setNameForm: typeof SvelteComponent | null =
-
    action === "setName" ? SetName : null;
-
  const setName = () => {
-
    setNameForm = SetName;
-
  };
-

-
  $: isAuthorized = (address: string): boolean | null => {
-
    return $session && isAddressEqual(address, $session.address);
-
  };
-
</script>
-

-
<style>
-
  main {
-
    padding: 5rem 0;
-
    width: 720px;
-
  }
-
  main > header {
-
    display: flex;
-
    align-items: center;
-
    justify-content: left;
-
    margin-bottom: 2rem;
-
  }
-
  main > header > * {
-
    margin: 0 1rem 0 0;
-
  }
-
  .info {
-
    display: flex;
-
    flex-direction: column;
-
    justify-content: center;
-
    align-items: left;
-
  }
-
  .info a {
-
    border: none;
-
  }
-
  .fields {
-
    display: grid;
-
    grid-template-columns: 5rem 4fr 2fr;
-
    grid-gap: 1rem 2rem;
-
  }
-
  .fields > div {
-
    justify-self: start;
-
    align-self: center;
-
    height: 2rem;
-
    line-height: 2rem;
-
  }
-
  .avatar {
-
    width: 64px;
-
    height: 64px;
-
  }
-
  .links {
-
    display: flex;
-
    align-items: center;
-
    justify-content: left;
-
  }
-
  .url {
-
    display: flex; /* Ensures correct vertical positioning of icons */
-
    margin-right: 1rem;
-
  }
-
  .url {
-
    height: 1.6rem; /* Height of the icon */
-
  }
-
  .projects {
-
    margin-top: 1rem;
-
  }
-
  .projects .project {
-
    margin-bottom: 1rem;
-
  }
-
  .members {
-
    margin-top: 2rem;
-
    align-items: center;
-
    display: flex;
-
    flex-wrap: wrap;
-
  }
-
  .members .member {
-
    display: flex;
-
    align-items: center;
-
    margin-right: 2rem;
-
    margin-bottom: 1rem;
-
  }
-
  .members .member:last-child {
-
    margin-right: 0;
-
  }
-
  .members .member-icon {
-
    width: 2rem;
-
    height: 2rem;
-
    margin-right: 1rem;
-
  }
-

-
  @media (max-width: 720px) {
-
    .fields {
-
      grid-template-columns: 5rem auto;
-
    }
-
    main {
-
      width: 100%;
-
      padding: 1.5rem;
-
    }
-
    .members .member {
-
      margin-right: 1rem;
-
    }
-
  }
-
</style>
-

-
<svelte:head>
-
  <title>{addressOrName}</title>
-
</svelte:head>
-

-
{#await Profile.get(addressOrName, ProfileType.Full, config)}
-
  <Loading fadeIn />
-
{:then profile}
-
  <main>
-
    <header>
-
      <div class="avatar">
-
        <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
-
      </div>
-
      <div class="info">
-
        <span class="title bold">
-
          <Address compact noAvatar noBadge {profile} address={profile.address} {config} resolve/>
-
        </span>
-
        <div class="links">
-
          {#if profile.url}
-
            <a class="url mobile" href={profile.url}>
-
              <Icon name="url" fill inline />
-
            </a>
-
            <a class="url desktop" href={profile.url}>
-
              {profile.url}
-
            </a>
-
          {/if}
-
          {#if profile.twitter}
-
            <a class="url" href="https://twitter.com/{profile.twitter}">
-
              <Icon name="twitter" fill inline />
-
            </a>
-
          {/if}
-
          {#if profile.github}
-
            <a class="url" href="https://github.com/{profile.github}">
-
              <Icon name="github" fill inline />
-
            </a>
-
          {/if}
-
        </div>
-
      </div>
-
    </header>
-
    <div class="fields">
-
      <!-- Address -->
-
      <div class="label">Address</div>
-
      <div class="desktop"><Address {config} address={profile.address} /></div>
-
      <div class="mobile"><Address compact {config} address={profile.address} /></div>
-
      <div class="desktop" />
-
      <!-- Project anchors -->
-
      {#if profile.anchorsAccount}
-
        <div class="label">Anchors</div>
-
        <div class="desktop"><Address {config} address={profile.anchorsAccount} /></div>
-
        <div class="mobile"><Address compact {config} address={profile.anchorsAccount} /></div>
-
        <div class="desktop" />
-
      {/if}
-
      <!-- Seed Address -->
-
      {#if profile.seed && profile.seed.valid}
-
        {#if profile.seed.id && profile.seed.host}
-
          <div class="label">Seed</div>
-
          <SeedAddress seed={profile.seed} port={config.seed.link.port} />
-
        {/if}
-
      {/if}
-
      <!-- Profile -->
-
      <div class="label">Profile</div>
-
      <div>
-
        {#if profile.name}
-
          <a href={profile.registry(config)} class="link">{profile.name}</a>
-
        {:else}
-
          <span class="subtle">Not set</span>
-
        {/if}
-
      </div>
-
      <div class="desktop">
-
        {#if isAuthorized(profile.address)}
-
          <button class="tiny secondary" on:click={setName}>
-
            Set
-
          </button>
-
        {/if}
-
      </div>
-
    </div>
-
    {#await Org.getOrgsByMember(profile.address, config)}
-
      <Loading center />
-
    {:then orgs}
-
      {#if orgs.length > 0}
-
        <div class="members">
-
          {#each orgs as org}
-
            <div class="member">
-
              {#await Profile.get(org.address, ProfileType.Minimal, config)}
-
                <Loading small margins />
-
              {:then profile}
-
                <div class="member-icon">
-
                  <Link to="/orgs/{profile.address}">
-
                    <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
-
                  </Link>
-
                </div>
-
                <div class="desktop">
-
                  <Address address={profile.address} compact
-
                    resolve noBadge noAvatar {profile} {config} />
-
                </div>
-
              {/await}
-
            </div>
-
          {/each}
-
        </div>
-
      {/if}
-
    {:catch err}
-
      <Message error>
-
        <strong>Error: </strong> failed to load orgs: {err.message}.
-
      </Message>
-
    {/await}
-
    <div class="projects">
-
      {#if profile.anchorsAccount}
-
        {#await Org.get(profile.anchorsAccount, 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)}>
-
                    <span slot="stateHash">
-
                      <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
-
                      <span class="desktop">commit {project.anchor.stateHash}</span>
-
                    </span>
-
                  </Project>
-
                </div>
-
              {/each}
-
            {:catch err}
-
              <Message error>
-
                <strong>Error: </strong> failed to load projects: {err.message}.
-
              </Message>
-
            {/await}
-
          {/if}
-
        {/await}
-
      {/if}
-
    </div>
-
  </main>
-
  <svelte:component this={setNameForm} entity={new User(profile.address)} {config} on:close={() => setNameForm = null} />
-
{:catch err}
-
  <Error error={err} />
-
{/await}
modified src/error.ts
@@ -24,6 +24,16 @@ export class Unreachable extends Error {
  }
}

+
export class NotFoundError extends Error {
+
  constructor(message?: string) {
+
    if (message) {
+
      super(`not found: ${message}`);
+
    } else {
+
      super(`not found`);
+
    }
+
  }
+
}
+

class AssertionError extends Error {
  constructor(message?: string) {
    if (message) {
modified src/profile.ts
@@ -1,15 +1,19 @@
import type { EnsProfile } from "@app/base/registrations/registrar";
import type { BasicProfile } from '@datamodels/identity-profile-basic';
import {
-
  isAddress, formatCAIP10Address, formatIpfsFile, resolveEnsProfile, resolveIdxProfile, parseUsername, parseEnsLabel
+
  isAddress, formatCAIP10Address, formatIpfsFile, resolveEnsProfile, resolveIdxProfile, parseUsername, parseEnsLabel, AddressType, identifyAddress
} from "@app/utils";
import type { Config } from "@app/config";
import type { Seed, InvalidSeed } from "@app/base/seeds/Seed";
+
import { Org } from "@app/base/orgs/Org";
+
import { NotFoundError } from "@app/error";

export interface IProfile {
  address: string;
+
  type: AddressType;
  ens?: EnsProfile;
  idx?: BasicProfile;
+
  org?: Org;
}

export enum ProfileType {
@@ -30,6 +34,16 @@ export class Profile {
    return this.profile.ens?.address ?? this.profile.address;
  }

+
  // Get the address type
+
  get type(): AddressType {
+
    return this.profile.type;
+
  }
+

+
  // Get the org instance
+
  get org(): Org | undefined {
+
    return this.profile.org;
+
  }
+

  // Get the ENS profile
  get ens(): EnsProfile | undefined {
    return this.profile.ens;
@@ -121,13 +135,23 @@ export class Profile {
    profileType: ProfileType,
    config: Config
  ): Promise<IProfile> {
+
    let type = AddressType.EOA;
+
    let org: Org | null = null;
    const ens = await resolveEnsProfile(addressOrName, profileType, config);

    if (ens) {
      if (ens.address) {
+
        type = await identifyAddress(ens.address, config);
+

+
        if (type === AddressType.Org) {
+
          org = await Org.get(ens.address, config);
+
        }
+

        return {
          address: ens.address.toLowerCase(),
-
          ens: { ...ens, address: ens.address.toLowerCase() }
+
          type,
+
          ens: { ...ens, address: ens.address.toLowerCase() },
+
          org: org ?? undefined
        };
      }
      throw new Error(`No address set for ${addressOrName}`);
@@ -135,20 +159,30 @@ export class Profile {
    } else if (isAddress(addressOrName)) {
      const address = addressOrName.toLowerCase();

+
      type = await identifyAddress(addressOrName, config);
+
      if (type === AddressType.Org) {
+
        org = await Org.get(addressOrName, config);
+
      }
+

      try {
        const idx = await resolveIdxProfile(
          formatCAIP10Address(address, "eip155", config.network.chainId), config
        );
-
        return { address, idx: idx ?? undefined };
+
        return {
+
          address,
+
          type,
+
          idx: idx ?? undefined,
+
          org: org ?? undefined
+
        };
      } catch (e: any) {
        // Look for the No DID found for error by the resolveIdxProfile fn and send it to console.debug
        if (e.message.match("No DID found for")) console.debug(e.message);
        else console.error(e);

-
        return { address };
+
        return { address, type, org: org ?? undefined };
      }
    }
-
    throw new Error(`Name ${addressOrName} was not found`);
+
    throw new NotFoundError(`Not able to resolve profile for ${addressOrName}`);
  }

  static async getMulti(addressesOrNames: string[], config: Config): Promise<Profile[]> {
modified src/project.ts
@@ -22,8 +22,7 @@ export interface Project {
// Params to render correctly source code related views
export interface Source {
  urn: string;
-
  org: string;
-
  user: string;
+
  addressOrName: string;
  peer: string;
  config: Config;
  project: Info;
@@ -31,7 +30,7 @@ export interface Source {
  anchors: string[];
  seed: string;
  branches: [string, string][];
-
  profile?: Profile;
+
  profile?: Profile | null;
}

export interface PendingProject extends Project {
@@ -155,8 +154,7 @@ export async function getReadme(
export function path(
  opts: {
    urn: string;
-
    org?: string;
-
    user?: string;
+
    addressOrName?: string;
    seed?: string;
    peer?: string;
    content?: ProjectContent;
@@ -164,17 +162,15 @@ export function path(
    path?: string;
  }
): string {
-
  const { urn, org, user, seed, peer, content, revision, path } = opts;
+
  const { urn, addressOrName, seed, peer, content, revision, path } = opts;
  const result = [];

-
  if (org) {
-
    result.push("orgs", org);
-
  } else if (user) {
-
    result.push("users", user);
+
  if (addressOrName) {
+
    result.push(addressOrName);
  } else if (seed) {
    result.push("seeds", seed);
  }
-
  result.push("projects", urn);
+
  result.push(urn);

  if (peer) {
    result.push("remotes", peer);
modified vite.config.ts
@@ -1,4 +1,3 @@
-
/// <reference types="vitest" />
import path from 'path';
import { UserConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';