Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor search logic
Rūdolfs Ošiņš committed 3 years ago
commit d0edc52831e26a532dc9581c353bf1de6ec6647e
parent 01e01160297c6eafdc930413a8034531f29a1a6c
6 files changed +198 -165
modified src/Header.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { Config } from "@app/config";
-
  import type { ResolvedSearch } from "@app/resolver";
+
  import type { ProjectsAndProfiles } from "@app/Search.svelte";
  import type { Session } from "@app/session";

  import { link } from "svelte-routing";
@@ -27,14 +27,9 @@
  export let config: Config;

  let query: string;
-
  let results: ResolvedSearch;
+
  let results: ProjectsAndProfiles | null = null;

  let sessionButtonHover = false;
-
  let searchResultsDisplayed = false;
-

-
  function toggleSearchResults() {
-
    searchResultsDisplayed = !searchResultsDisplayed;
-
  }

  $: address = session && session.address;
  $: tokenBalance = session && session.tokenBalance;
@@ -177,7 +172,6 @@
        {config}
        on:search={e => {
          ({ query, results } = e.detail);
-
          toggleSearchResults();
        }} />
    </div>
    <div class="nav">
@@ -259,7 +253,6 @@
                }}
                on:search={e => {
                  ({ query, results } = e.detail);
-
                  toggleSearchResults();
                }} />
            </div>
            <a
@@ -276,7 +269,13 @@
    </div>
  </div>

-
  {#if searchResultsDisplayed}
-
    <SearchResults {config} {results} {query} on:close={toggleSearchResults} />
+
  {#if results}
+
    <SearchResults
+
      {config}
+
      {results}
+
      {query}
+
      on:close={() => {
+
        results = null;
+
      }} />
  {/if}
</header>
modified src/Search.svelte
@@ -1,42 +1,196 @@
-
<script lang="ts">
+
<script lang="ts" context="module">
+
  import type { Host } from "@app/api";
+
  import type { ProjectInfo } from "@app/project";
+

+
  import { ethers } from "ethers";
+

+
  import * as utils from "@app/utils";
+
  import { Profile } from "@app/profile";
+
  import { Project } from "@app/project";
+

+
  export interface ProjectsAndProfiles {
+
    projects: { info: ProjectInfo; seed: Host }[];
+
    profiles: Profile[];
+
  }
+

+
  type SearchResult =
+
    | { type: "nothing" }
+
    | { type: "error"; message: string }
+
    | { type: "singleProfile"; id: string }
+
    | { type: "singleProject"; seedHost: string; id: string }
+
    | { type: "projectsAndProfiles"; projectsAndProfiles: ProjectsAndProfiles };
+

+
  async function searchProjectsAndProfiles(
+
    query: string,
+
    config: Config,
+
  ): Promise<SearchResult> {
+
    try {
+
      // The query is a plain Ethereum address.
+
      if (ethers.utils.isAddress(query)) {
+
        return { type: "singleProfile", id: query };
+
      }
+

+
      const projectOnSeeds = Object.keys(config.seeds.pinned).map(seed => ({
+
        nameOrUrn: query,
+
        seed,
+
      }));
+

+
      // The query is a radicle project URN.
+
      if (utils.isRadicleId(query)) {
+
        const projects = await Project.getMulti(projectOnSeeds);
+

+
        if (projects.length === 1) {
+
          return {
+
            type: "singleProject",
+
            seedHost: projects[0].seed.host,
+
            id: query,
+
          };
+
        } else {
+
          return {
+
            type: "projectsAndProfiles",
+
            projectsAndProfiles: { projects, profiles: [] },
+
          };
+
        }
+
      }
+

+
      // The query is either a project or a profile name.
+
      const normalizedQuery = query.toLowerCase();
+
      const projectsAndProfiles: ProjectsAndProfiles = {
+
        projects: [],
+
        profiles: [],
+
      };
+

+
      try {
+
        const projects = await Project.getMulti(projectOnSeeds);
+
        projectsAndProfiles.projects.push(...projects);
+
      } catch {
+
        // TODO: collect errors and forward to user.
+
      }
+

+
      try {
+
        let params: string[];
+
        if (utils.isENSName(normalizedQuery, config)) {
+
          params = [normalizedQuery];
+
        } else {
+
          params = [
+
            `${normalizedQuery}.${config.registrar.domain}`,
+
            `${normalizedQuery}.eth`,
+
          ];
+
        }
+
        const profiles = await Profile.getMulti(params, config);
+
        projectsAndProfiles.profiles.push(...profiles);
+
      } catch {
+
        // TODO: collect errors and forward to user.
+
      }
+

+
      const projectCount = projectsAndProfiles.projects.length;
+
      const profileCount = projectsAndProfiles.profiles.length;
+

+
      if (profileCount === 1 && projectCount === 0) {
+
        return {
+
          type: "singleProfile",
+
          id: projectsAndProfiles.profiles[0].address,
+
        };
+
      }
+

+
      if (profileCount === 0 && projectCount === 1) {
+
        return {
+
          type: "singleProject",
+
          seedHost: projectsAndProfiles.projects[0].seed.host,
+
          id: query,
+
        };
+
      }
+

+
      if (profileCount > 0 || projectCount > 0) {
+
        return {
+
          type: "projectsAndProfiles",
+
          projectsAndProfiles,
+
        };
+
      }
+

+
      return { type: "nothing" };
+
    } catch (error) {
+
      let message = "An unknown error occoured while searching.";
+

+
      if (error instanceof Error) {
+
        message = error.message;
+
      }
+

+
      return { type: "error", message };
+
    }
+
  }
+
</script>
+

+
<script lang="ts" strictEvents>
  import type { Config } from "@app/config";

-
  import { createEventDispatcher } from "svelte";
  import debounce from "lodash/debounce";
+
  import { createEventDispatcher } from "svelte";
+
  import { navigate } from "svelte-routing";

-
  import { resolve } from "@app/resolver";
  import Loading from "@app/Loading.svelte";
  import TextInput from "@app/TextInput.svelte";
+
  import { unreachable } from "@app/utils";

  export let config: Config;

+
  const dispatch = createEventDispatcher<{
+
    finished: boolean;
+
    search: { query: string; results: ProjectsAndProfiles };
+
  }>();
+

  let input = "";
  let searching = false;
-
  let shake = false;
+
  let shaking = false;
+

+
  function shake() {
+
    shaking = true;
+
    debounce(() => (shaking = false), 500)();
+
  }

-
  const dispatch = createEventDispatcher();
-
  const handleKeydown = async (event: KeyboardEvent) => {
+
  async function search(event: KeyboardEvent) {
    if (event.key === "Enter") {
+
      if (input === "") {
+
        return;
+
      }
+

      searching = true;
-
      resolve(input, config)
-
        .then(results => {
-
          const query = input;
-
          input = "";
-
          searching = false;
-
          if (results) dispatch("search", { query, results });
-
        })
-
        .catch(() => {
-
          searching = false;
-
          shake = true;
-
          debounce(() => (shake = false), 500)();
-
          dispatch("finished");
+

+
      const query = input;
+
      const searchResult = await searchProjectsAndProfiles(input, config);
+

+
      if (searchResult.type === "nothing") {
+
        searching = false;
+
        shake();
+
      } else if (searchResult.type === "error") {
+
        // TODO: show some kind of notification to the user.
+
        shake();
+
      } else if (searchResult.type === "singleProfile") {
+
        input = "";
+
        navigate(`/${searchResult.id}`, { replace: true });
+
      } else if (searchResult.type === "singleProject") {
+
        input = "";
+
        navigate(`/seeds/${searchResult.seedHost}/${searchResult.id}`, {
+
          replace: true,
        });
+
      } else if (searchResult.type === "projectsAndProfiles") {
+
        // TODO: show some kind of notification about any errors to the user.
+
        input = "";
+
        dispatch("search", {
+
          query,
+
          results: searchResult.projectsAndProfiles,
+
        });
+
      } else {
+
        unreachable(searchResult);
+
      }
+
      searching = false;
+
      dispatch("finished");
    }
-
  };
+
  }
</script>

<style>
-
  .horizontal-shake {
+
  .shaking {
    animation: horizontal-shaking 0.35s;
  }
  @keyframes horizontal-shaking {
@@ -58,12 +212,12 @@
  }
</style>

-
<div class:horizontal-shake={shake}>
+
<div class:shaking>
  <TextInput
    variant="dashed"
    disabled={searching}
    bind:value={input}
-
    on:keydown={handleKeydown}
+
    on:keydown={search}
    placeholder="Search a name or address…">
    <svelte:fragment slot="right">
      {#if searching}
modified src/base/home/Index.svelte
@@ -19,7 +19,12 @@

  const getProjects =
    config.projects.pinned.length > 0
-
      ? Project.getMulti(config.projects.pinned)
+
      ? Project.getMulti(
+
          config.projects.pinned.map(project => ({
+
            nameOrUrn: project.urn,
+
            seed: project.seed,
+
          })),
+
        )
      : Promise.resolve([]);

  const onClick = (project: ProjectInfo, seed: Host) => {
modified src/components/Modal/SearchResults.svelte
@@ -6,10 +6,10 @@
  import Address from "@app/Address.svelte";
  import Button from "@app/Button.svelte";
  import { createEventDispatcher } from "svelte";
-
  import type { ResolvedSearch } from "@app/resolver";
+
  import type { ProjectsAndProfiles } from "@app/Search.svelte";

  export let query: string;
-
  export let results: ResolvedSearch;
+
  export let results: ProjectsAndProfiles;
  export let config: Config;

  const dispatch = createEventDispatcher();
@@ -63,10 +63,10 @@
        {/each}
      </ul>
    {/if}
-
    {#if results.ens.length > 0}
+
    {#if results.profiles.length > 0}
      <p class="highlight txt-medium">ENS names</p>
      <ul>
-
        {#each results.ens as profile}
+
        {#each results.profiles as profile}
          <li>
            <Address address={profile.address} {profile} {config} resolve />
          </li>
modified src/project.ts
@@ -485,14 +485,14 @@ export class Project implements ProjectInfo {
  }

  static async getMulti(
-
    projs: { urn: Urn; seed: string }[],
+
    projs: { nameOrUrn: Urn; seed: string }[],
  ): Promise<{ info: ProjectInfo; seed: Host }[]> {
    const promises = [];

    for (const proj of projs) {
      const seed = { host: proj.seed, port: null };
      promises.push(
-
        Project.getInfo(proj.urn, seed).then(info => {
+
        Project.getInfo(proj.nameOrUrn, seed).then(info => {
          return { info, seed };
        }),
      );
deleted src/resolver.ts
@@ -1,125 +0,0 @@
-
import type { Host } from "@app/api";
-
import type { Config } from "@app/config";
-
import type { ProjectInfo } from "@app/project";
-

-
import { navigate } from "svelte-routing";
-
import { ethers } from "ethers";
-

-
import * as utils from "@app/utils";
-
import { Project } from "@app/project";
-
import { NotFoundError } from "@app/error";
-
import { Profile, ProfileType } from "@app/profile";
-

-
export interface IProject {
-
  info: ProjectInfo;
-
  seed: Host;
-
}
-

-
export interface ResolvedSearch {
-
  projects: IProject[];
-
  ens: Profile[];
-
}
-

-
export async function resolve(
-
  q: string | null,
-
  config: Config,
-
): Promise<ResolvedSearch | null> {
-
  const results: ResolvedSearch = {
-
    projects: [],
-
    ens: [],
-
  };
-

-
  try {
-
    if (q) {
-
      const seeds = Object.keys(config.seeds.pinned).map(seed => ({
-
        urn: q as string,
-
        seed,
-
      }));
-

-
      if (ethers.utils.isAddress(q)) {
-
        navigate(`/${q}`, { replace: true });
-
        return null;
-

-
        // ========= Projects =========
-
      } else if (utils.isRadicleId(q)) {
-
        const projects = await Project.getMulti(seeds);
-
        if (projects.length === 1) {
-
          navigate(`/seeds/${projects[0].seed.host}/${q}`, {
-
            replace: true,
-
          });
-
          return null;
-
        } else {
-
          results.projects.push(...projects);
-
        }
-
      } else {
-
        const projects = await Project.getMulti(seeds);
-
        results.projects.push(...projects);
-

-
        // ========= ENS Names =========
-
        const normalizedQuery = q.toLowerCase();
-
        let profile: Profile | null;
-
        try {
-
          profile = await Profile.get(
-
            normalizedQuery,
-
            ProfileType.Minimal,
-
            config,
-
          );
-
        } catch (e) {
-
          profile = null;
-
        }
-

-
        if (profile) {
-
          if (results.projects.length === 0) {
-
            navigate(`/${profile.address}`, { replace: true });
-
            return null;
-
          } else {
-
            results.ens.push(profile);
-
          }
-
        } else {
-
          let profiles: Profile[];
-
          try {
-
            profiles = await Profile.getMulti(
-
              [
-
                `${normalizedQuery}.${config.registrar.domain}`,
-
                `${normalizedQuery}.eth`,
-
              ],
-
              config,
-
            );
-
          } catch (e) {
-
            profiles = [];
-
          }
-

-
          if (profiles.length > 1) {
-
            results.ens.push(...profiles);
-
          } else if (profiles.length === 1) {
-
            if (results.projects.length === 0) {
-
              navigate(`/${profiles[0].address}`, { replace: true });
-
              return null;
-
            } else {
-
              results.ens.push(...profiles);
-
            }
-
          } else {
-
            if (results.projects.length === 1) {
-
              navigate(`/seeds/${projects[0].seed.host}/${q}`, {
-
                replace: true,
-
              });
-
              return null;
-
            }
-
          }
-
        }
-
      }
-
    }
-

-
    if (results.projects.length > 0 || results.ens.length > 0) {
-
      return results;
-
    }
-

-
    throw new NotFoundError("No search results found");
-
  } catch (e) {
-
    if (e instanceof NotFoundError) {
-
      throw new NotFoundError(e.message);
-
    } else {
-
      throw Error("Not able to resolve search query");
-
    }
-
  }
-
}