Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Improve commit history and detail view
Alexis Sellier committed 4 years ago
commit 0cced197baef052aa2dae7ef5acc3bc5ac27a7d0
parent fabb90f91e4b3ac8fb469faf2574857ab9173421
11 files changed +265 -112
modified cypress/fixtures/groupedCommits.json
@@ -3,14 +3,16 @@
    "groupDate": "Thursday, March 3, 2022",
    "commits": [
      {
-
        "sha": "9cd3532",
-
        "summary": "Second commit",
-
        "description": "",
-
        "committer": {
-
          "name": "dabit3",
-
          "mail": "dabit3@gmail.com"
-
        },
-
        "committerTime": "Thu, 03 Mar 2022 05:51:25 GMT"
+
        "header": {
+
          "sha": "9cd3532",
+
          "summary": "Second commit",
+
          "description": "",
+
          "committer": {
+
            "name": "dabit3",
+
            "mail": "dabit3@gmail.com"
+
          },
+
          "committerTime": "06:51 GMT+1"
+
        }
      }
    ]
  },
@@ -18,24 +20,28 @@
    "groupDate": "Wednesday, March 2, 2022",
    "commits": [
      {
-
        "sha": "e045b92",
-
        "summary": "Update README",
-
        "description": "",
-
        "committer": {
-
          "name": "dabit3",
-
          "mail": "dabit3@gmail.com"
-
        },
-
        "committerTime": "Wed, 02 Mar 2022 16:14:45 GMT"
+
        "header": {
+
          "sha": "e045b92",
+
          "summary": "Update README",
+
          "description": "",
+
          "committer": {
+
            "name": "dabit3",
+
            "mail": "dabit3@gmail.com"
+
          },
+
          "committerTime": "17:14 GMT+1"
+
        }
      },
      {
-
        "sha": "cbf5df4",
-
        "summary": "initial commit",
-
        "description": "this is the first commit of many",
-
        "committer": {
-
          "name": "dabit3",
-
          "mail": "dabit3@gmail.com"
-
        },
-
        "committerTime": "Wed, 02 Mar 2022 15:58:05 GMT"
+
        "header": {
+
          "sha": "cbf5df4",
+
          "summary": "initial commit",
+
          "description": "this is the first commit of many",
+
          "committer": {
+
            "name": "dabit3",
+
            "mail": "dabit3@gmail.com"
+
          },
+
          "committerTime": "16:58 GMT+1"
+
        }
      }
    ]
  }
modified cypress/fixtures/projectCommits.json
@@ -20,7 +20,7 @@
          "peer": {
            "id": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
            "person": {
-
              "name": "sebastinez"
+
              "name": "dabit3"
            },
            "delegate": true
          }
modified cypress/integration/project.spec.ts
@@ -43,7 +43,7 @@ describe("Project view", () => {
    cy.get("div.clone").click();
  });

-
  it("Peer selector ", () => {
+
  it("Peer selector", () => {
    cy.get("div.selector").click();
    cy.get("div.dropdown-item").contains("dabit3 hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke delegate").click();
    cy.location().should((location) => {
@@ -54,7 +54,7 @@ describe("Project view", () => {
    cy.get("div.stat.peer span.badge").should("have.text", "delegate");
  });

-
  it("Branch selector ", () => {
+
  it("Branch selector", () => {
    cy.get("div.commit div.stat.branch").click();
    cy.get("div.dropdown-item")
      .first()
@@ -69,7 +69,7 @@ describe("Project view", () => {
    cy.get("div.hash.desktop").should("have.text", "cbf5df4");
  });

-
  it("Commit button ", () => {
+
  it("Commits button", () => {
    cy.get("div.stat.commit-count")
      .should("not.have.class", "active")
      .click();
@@ -79,42 +79,37 @@ describe("Project view", () => {
    cy.get("div.stat.commit-count").should("have.class", "active");
  });

-
  it("Commit group & commit trailer ", () => {
+
  it("Commit group & commit trailer", () => {
    cy.fixture("groupedCommits.json").then((commitGroup) => {
      // This iterates over the commit groups and then over each commit.
-
      cy.get("div.commit-group").should("have.length", 2)
+
      cy.get(".commit-group").should("have.length", 2)
        .each((item, index) => {
          expect(Cypress.$(item.children(".commit-date")).text()).to.eq(commitGroup[index].groupDate);
-
          const $el = Cypress.$(item.find(".commit"));
+
          const $el = Cypress.$(item.find(".commit-teaser"));
          cy.wrap($el).each((commit, commitIndex) => {
-
            expect(Cypress.$(commit).find(".hash").text()).to.eq(commitGroup[index].commits[commitIndex].sha);
-
            expect(Cypress.$(commit).find("span.summary").text()).to.eq(commitGroup[index].commits[commitIndex].summary);
-
            expect(Cypress.$(commit).find(".author").text()).to.eq(commitGroup[index].commits[commitIndex].committer.name);
-
            expect(Cypress.$(commit).find(".time").text()).to.eq(commitGroup[index].commits[commitIndex].committerTime);
+
            expect(Cypress.$(commit).find(".hash").text()).to.eq(commitGroup[index].commits[commitIndex].header.sha);
+
            expect(Cypress.$(commit).find(".summary").text()).to.eq(commitGroup[index].commits[commitIndex].header.summary);
+
            expect(Cypress.$(commit).find(".committer").text()).to.eq(commitGroup[index].commits[commitIndex].header.committer.name);
          });
        });

      // Checking that the initial commit has the Verified badge
      cy.get(".badge").last().should("have.text", "Verified");
-
      cy.get("div.summary").last().click();
+
      cy.get(".commit").last().click();
    });
  });

-
  it("Commit detail view ", () => {
+
  it("Commit detail view", () => {
    cy.fixture("groupedCommits.json").then((commitGroup) => {
      // We get the initial commit from the fixture file to assert here
-
      const { committer, committerTime, summary, description } = commitGroup[1].commits[1];
+
      const { committer, committerTime, summary, description } = commitGroup[1].commits[1].header;

      cy.location().should((location) => {
        expect(location.pathname).to.eq('/seeds/willow.radicle.garden/rad:git:hnrk8mbpirp7ua7sy66o4t9soasbq4y8uwgoy/remotes/hyndc7nx9keq76p1bkw9831arcndeeu3trwsc7kxt3osmpi6j9oeke/commits/cbf5df499ab4f4a908f1756fbe2c236a4530516a');
      });
-
      cy.get("header div.summary h3").should("have.text", summary);
+
      cy.get("header .summary h3").should("have.text", summary);
      cy.get("header pre.description").should("have.text", description);
-
      cy.get("header div.meta")
-
        .first()
-
        .should("have.text", `Committed by ${committer.name} <${committer.mail}> ${committerTime}`)
-
        .next()
-
        .should("have.text", `Authored by ${committer.name} <${committer.mail}>`);
+
      cy.get("header .committer").should("have.text", `${committer.name}`);
      cy.get("div.changeset-summary").should("have.text", "1 file(s) changed\n    with\n    0 addition(s)\n    and\n    0 deletion(s)");
      cy.get("header.file-header:nth-child(1) div.file-data > *")
        .first()
modified package-lock.json
@@ -16,6 +16,7 @@
        "@self.id/core": "^0.1.0",
        "@stardazed/streams": "^3.1.0",
        "@types/marked": "^4.0.1",
+
        "@types/md5": "^2.3.2",
        "@walletconnect/client": "^1.6.0",
        "@walletconnect/signer-connection": "^1.6.0",
        "@walletconnect/types": "^1.6.0",
@@ -27,6 +28,7 @@
        "ethers": "^5.0.31",
        "eventemitter3": "4.0.7",
        "marked": "^4.0.12",
+
        "md5": "^2.3.0",
        "multibase": "^4.0.4",
        "multihashes": "^4.0.2",
        "pure-svg-code": "^1.0.6",
@@ -3515,6 +3517,11 @@
      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz",
      "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ=="
    },
+
    "node_modules/@types/md5": {
+
      "version": "2.3.2",
+
      "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
+
      "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og=="
+
    },
    "node_modules/@types/node": {
      "version": "14.18.10",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.10.tgz",
@@ -4999,6 +5006,14 @@
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
+
    "node_modules/charenc": {
+
      "version": "0.0.2",
+
      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+
      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
+
      "engines": {
+
        "node": "*"
+
      }
+
    },
    "node_modules/check-error": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@@ -5444,6 +5459,14 @@
        "node": ">= 8"
      }
    },
+
    "node_modules/crypt": {
+
      "version": "0.0.2",
+
      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+
      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
+
      "engines": {
+
        "node": "*"
+
      }
+
    },
    "node_modules/crypto-browserify": {
      "version": "3.12.0",
      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -7997,8 +8020,7 @@
    "node_modules/is-buffer": {
      "version": "1.1.6",
      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-
      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
-
      "dev": true
+
      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
    },
    "node_modules/is-callable": {
      "version": "1.2.4",
@@ -8953,6 +8975,16 @@
        "node": ">= 12"
      }
    },
+
    "node_modules/md5": {
+
      "version": "2.3.0",
+
      "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
+
      "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
+
      "dependencies": {
+
        "charenc": "0.0.2",
+
        "crypt": "0.0.2",
+
        "is-buffer": "~1.1.6"
+
      }
+
    },
    "node_modules/md5.js": {
      "version": "1.3.5",
      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -14795,6 +14827,11 @@
      "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.2.tgz",
      "integrity": "sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ=="
    },
+
    "@types/md5": {
+
      "version": "2.3.2",
+
      "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
+
      "integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og=="
+
    },
    "@types/node": {
      "version": "14.18.10",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.10.tgz",
@@ -15992,6 +16029,11 @@
        "supports-color": "^7.1.0"
      }
    },
+
    "charenc": {
+
      "version": "0.0.2",
+
      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+
      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
+
    },
    "check-error": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@@ -16358,6 +16400,11 @@
        "which": "^2.0.1"
      }
    },
+
    "crypt": {
+
      "version": "0.0.2",
+
      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+
      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
+
    },
    "crypto-browserify": {
      "version": "3.12.0",
      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -18209,8 +18256,7 @@
    "is-buffer": {
      "version": "1.1.6",
      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-
      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
-
      "dev": true
+
      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
    },
    "is-callable": {
      "version": "1.2.4",
@@ -18918,6 +18964,16 @@
      "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz",
      "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ=="
    },
+
    "md5": {
+
      "version": "2.3.0",
+
      "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
+
      "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
+
      "requires": {
+
        "charenc": "0.0.2",
+
        "crypt": "0.0.2",
+
        "is-buffer": "~1.1.6"
+
      }
+
    },
    "md5.js": {
      "version": "1.3.5",
      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
modified package.json
@@ -8,7 +8,8 @@
    "coverage": "npx nyc report",
    "test:unit": "vitest run",
    "test:components": "cypress run-ct",
-
    "test:e2e": "cypress run"
+
    "test:e2e": "cypress run",
+
    "test:e2e:open": "cypress open"
  },
  "type": "module",
  "engines": {
@@ -46,6 +47,7 @@
    "@self.id/core": "^0.1.0",
    "@stardazed/streams": "^3.1.0",
    "@types/marked": "^4.0.1",
+
    "@types/md5": "^2.3.2",
    "@walletconnect/client": "^1.6.0",
    "@walletconnect/signer-connection": "^1.6.0",
    "@walletconnect/types": "^1.6.0",
@@ -57,6 +59,7 @@
    "ethers": "^5.0.31",
    "eventemitter3": "4.0.7",
    "marked": "^4.0.12",
+
    "md5": "^2.3.0",
    "multibase": "^4.0.4",
    "multihashes": "^4.0.2",
    "pure-svg-code": "^1.0.6",
modified src/base/projects/Commit.svelte
@@ -1,9 +1,10 @@
<script lang="ts">
  import * as proj from "@app/project";
-
  import Changeset from "@app/base/projects/SourceBrowser/Changeset.svelte";
-
  import { formatCommitTime } from "@app/commit";
  import { formatCommit } from "@app/utils";

+
  import Changeset from "@app/base/projects/SourceBrowser/Changeset.svelte";
+
  import CommitAuthorship from "@app/base/projects/Commit/CommitAuthorship.svelte";
+

  export let project: proj.Project;
  export let commit: string;

@@ -19,7 +20,6 @@
<style>
  .commit {
    padding: 0 2rem 0 8rem;
-
    font-size: 0.875rem;
  }
  h3 {
    margin: 0;
@@ -38,21 +38,19 @@
  .description {
    margin: 0.5rem 0 1rem 0;
  }
-
  .meta {
-
    color: var(--color-foreground-80);
-
  }
-
  .email {
-
    font-family: var(--font-family-monospace);
-
    font-size: 0.875rem;
-
  }
  .sha1 {
    color: var(--color-foreground-80);
    font-size: 0.875rem;
  }
-
  .time {
-
    margin-left: 0.25rem;
-
    color: var(--color-foreground-faded);
+
  .badge {
+
    margin: 0;
  }
+
  .authorship {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
  }
+

  @media (max-width: 960px) {
    .commit {
      padding-left: 2rem;
@@ -68,19 +66,16 @@
        <div class="desktop font-mono sha1">
          <span>{commit.header.sha1}</span>
        </div>
-
        <div class="mobile font-mono sha1">
+
        <div class="mobile font-mono sha1 text-small">
          {formatCommit(commit.header.sha1)}
        </div>
      </div>
-
      <pre class="description">{commit.header.description}</pre>
-
      <div class="meta">
-
        <span>Committed by <strong>{commit.header.committer.name}</strong></span>
-
        <span class="font-mono email desktop-inline">&lt;{commit.header.committer.email}&gt; </span>
-
        <span class="time desktop-inline">{formatCommitTime(commit.header.committerTime)}</span>
-
      </div>
-
      <div class="meta">
-
        <span>Authored by <strong>{commit.header.author.name}</strong> </span>
-
        <span class="font-mono email desktop-inline">&lt;{commit.header.author.email}&gt;</span>
+
      <pre class="description text-small">{commit.header.description}</pre>
+
      <div class="authorship">
+
        <CommitAuthorship {commit} />
+
        {#if commit.context?.committer}
+
          <span class="badge tertiary">Verified</span>
+
        {/if}
      </div>
    </header>
    <Changeset stats={commit.stats} diff={commit.diff} on:browse={onBrowse} />
added src/base/projects/Commit/CommitAuthorship.svelte
@@ -0,0 +1,61 @@
+
<script lang="ts">
+
  import type { CommitMetadata } from "@app/commit";
+
  import { gravatarURL } from "@app/utils";
+
  import { formatCommitTime } from "@app/commit";
+

+
  export let commit: CommitMetadata;
+
</script>
+

+
<style>
+
  .authorship {
+
    display: flex;
+
    align-items: center;
+
    color: var(--color-foreground-faded);
+
    padding: 0.125rem 0;
+
  }
+
  .authorship .author, .authorship .committer {
+
    color: var(--color-foreground);
+
    white-space: nowrap;
+
  }
+
  .authorship .avatar {
+
    width: 1rem;
+
    height: 1rem;
+
    border-radius: 0.5rem;
+
    margin-right: 0.25rem;
+
  }
+
  .authorship .avatar:not(:first-child) {
+
    margin-left: 0.125rem;
+
  }
+
</style>
+

+
<span class="authorship text-xsmall">
+
  <img class="avatar" alt="avatar" src="{gravatarURL(commit.header.author.email)}" />
+
  {#if commit.header.author.email === commit.header.committer.email}
+
    {#if commit.context?.committer}
+
      <span class="bold committer verified-committer">
+
        {commit.context?.committer.peer.person.name}
+
      </span>
+
      <span>&nbsp;committed</span>
+
    {:else}
+
      <span class="desktop-inline committer">{commit.header.committer.name}</span>
+
      <span>&nbsp;committed</span>
+
    {/if}
+
  {:else}
+
    <span class="desktop-inline author">{commit.header.author.name}</span>
+
    <span>&nbsp;authored&nbsp;</span>
+
    <img class="avatar" alt="avatar" src="{gravatarURL(commit.header.committer.email)}" />
+
    {#if commit.context?.committer}
+
      <span class="bold committer verified-committer">
+
        {commit.context?.committer.peer.person.name}
+
      </span>
+
      <span>&nbsp;committed</span>
+
    {:else}
+
      <span class="desktop-inline committer">
+
        {commit.header.committer.name}
+
      </span>
+
      <span>&nbsp;committed</span>
+
    {/if}
+
  {/if}
+
  <span>&nbsp;at&nbsp;</span>
+
  <span class="desktop-inline text-xsmall time">{formatCommitTime(commit.header.committerTime)}</span>
+
</span>
modified src/base/projects/Commit/CommitTeaser.svelte
@@ -1,18 +1,12 @@
<script lang="ts">
-
  import Icon from "@app/Icon.svelte";
  import type { CommitMetadata } from "@app/commit";
-
  import type { Project } from "@app/project";
  import { formatCommit } from "@app/utils";
  import { createEventDispatcher } from "svelte";
-
  import { ProjectContent } from "@app/project";
-
  import { formatCommitTime } from "@app/commit";

-
  export let commit: CommitMetadata;
-
  export let project: Project;
+
  import Icon from "@app/Icon.svelte";
+
  import CommitAuthorship from "./CommitAuthorship.svelte";

-
  const navigateHistory = (revision: string, content?: ProjectContent) => {
-
    project.navigateTo({ content, revision, path: null });
-
  };
+
  export let commit: CommitMetadata;

  const dispatch = createEventDispatcher();

@@ -24,23 +18,38 @@
<style>
  .hash {
    font-family: var(--font-family-monospace);
-
    padding: 0 1rem 0 0;
  }
-
  .author {
-
    margin-right: 0.5rem;
-
    white-space: nowrap;
+
  .commit-teaser {
+
    background-color: var(--color-foreground-background);
+
    padding: 0.5rem 0rem;
+
  }
+
  .commit-teaser:hover {
+
    background-color: var(--color-foreground-background-lighter);
+
  }
+
  .commit-teaser:first-child {
+
    border-top-left-radius: 0.25rem;
+
    border-top-right-radius: 0.25rem;
  }
-
  .commit {
+
  .commit-teaser:last-child {
+
    border-bottom-left-radius: 0.25rem;
+
    border-bottom-right-radius: 0.25rem;
+
  }
+
  .commit-teaser {
    display: flex;
    align-items: center;
    justify-content: space-between;
-
    padding: 0.5rem 1rem;
  }
-
  .commit .right {
+

+
  .column-left {
+
    padding: 0 1.5rem;
+
  }
+
  .column-center {
+
    flex: min-content;
+
  }
+
  .commit-teaser .column-right {
    display: flex;
-
    flex-direction: row;
    align-items: center;
-
    gap: 0.5rem;
+
    padding-right: 1.5rem;
  }
  .summary {
    overflow: hidden;
@@ -48,16 +57,15 @@
    text-overflow: ellipsis;
    padding-right: 1rem;
  }
-
  .time {
-
    margin-right: 0.5rem;
-
    white-space: nowrap;
+
  .badge {
+
    margin: 0 1rem 0 0;
  }

  @media (max-width: 960px) {
    .hash {
      padding-right: 0.5rem;
    }
-
    .time, .author, .hash {
+
    .hash {
      font-size: 0.75rem;
    }
    .summary {
@@ -69,17 +77,22 @@
  }
</style>

-
<div class="commit" on:click={() => navigateHistory(commit.header.sha1, ProjectContent.Commit)}>
-
  <div class="summary">
+
<div class="commit-teaser">
+
  <div class="column-left">
    <span class="secondary hash">{formatCommit(commit.header.sha1)}</span>
-
    <span class="summary">{commit.header.summary}</span>
  </div>
-
  <div class="right">
+
  <div class="column-center">
+
    <div class="header">
+
      <div class="summary">
+
        {commit.header.summary}
+
      </div>
+
    </div>
+
    <CommitAuthorship {commit} />
+
  </div>
+
  <div class="column-right">
    {#if commit.context.committer}
-
      <span class="badge primary">Verified</span>
+
      <span class="badge tertiary">Verified</span>
    {/if}
-
    <span class="desktop-inline bold author">{commit.header.committer.name}</span>
-
    <span class="desktop-inline font-mono text-small time">{formatCommitTime(commit.header.committerTime)}</span>
    <Icon name="browse" width={17} inline fill on:click={() => browseCommit(commit.header.sha1)} />
  </div>
</div>
modified src/base/projects/History.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
-
  import { Project } from "@app/project";
+
  import { Project, ProjectContent } from "@app/project";
  import Loading from "@app/Loading.svelte";
  import { groupCommitHistory, GroupedCommitsHistory } from "@app/commit";
  import Message from "@app/Message.svelte";
@@ -8,6 +8,10 @@
  export let project: Project;
  export let commit: string;

+
  const navigateHistory = (revision: string, content?: ProjectContent) => {
+
    project.navigateTo({ content, revision, path: null });
+
  };
+

  const fetchCommits = async (parentCommit: string): Promise<GroupedCommitsHistory> => {
    const commitsQuery = await Project.getCommits(project.urn, project.seed.api, {
      parent: parentCommit, verified: true
@@ -25,24 +29,28 @@
    color: var(--color-foreground-faded);
  }
  .commit-group-headers {
-
    border-radius: 0.25rem;
    margin-bottom: 2rem;
-
    background: var(--color-foreground-background);
  }
-
  .commit-group-headers {
+

+
  .commit {
+
    background-color: var(--color-foreground-background);
+
  }
+
  .commit:not(:last-child) {
+
    border-bottom: 1px dashed var(--color-background);
+
  }
+
  .commit:hover {
+
    background-color: var(--color-foreground-background-lighter);
    cursor: pointer;
  }
-
  .commit-group-headers:first-child {
+
  .commit:first-child {
    border-top-left-radius: 0.25rem;
    border-top-right-radius: 0.25rem;
  }
-
  .commit-group-headers:last-child {
+
  .commit:last-child {
    border-bottom-left-radius: 0.25rem;
    border-bottom-right-radius: 0.25rem;
  }
-
  .commit-group-headers {
-
    background: var(--color-foreground-background-lighter);
-
  }
+

  @media (max-width: 960px) {
    .history {
      padding-left: 2rem;
@@ -61,7 +69,9 @@
        </header>
        <div class="commit-group-headers">
          {#each group.commits as commit (commit.header.sha1)}
-
            <CommitTeaser {project} {commit} />
+
            <div class="commit" on:click={() => navigateHistory(commit.header.sha1, ProjectContent.Commit)}>
+
              <CommitTeaser {commit} />
+
            </div>
          {/each}
        </div>
      </div>
modified src/commit.ts
@@ -69,6 +69,7 @@ export interface Commit {
  stats: CommitStats;
  diff: Diff;
  branches: string[];
+
  context: CommitContext;
}

export function formatGroupTime(timestamp: number): string {
@@ -129,7 +130,10 @@ export function groupCommits(commits: { header: CommitHeader; context: CommitCon
}

export const formatCommitTime = (t: number): string => {
-
  return new Date(t * 1000).toUTCString();
+
  const options: any = {
+
    hour: "2-digit", minute: "2-digit", timeZoneName: "short", hour12: false
+
  };
+
  return new Date(t * 1000).toLocaleTimeString("en-us", options);
};

export function groupCommitsByWeek(commits: CommitMetadata[]): CommitGroup[] {
modified src/utils.ts
@@ -1,4 +1,5 @@
import { ethers } from "ethers";
+
import md5 from "md5";
import { BigNumber } from "ethers";
import multibase from 'multibase';
import multihashes from 'multihashes';
@@ -482,6 +483,15 @@ export function isDomain(input: string): boolean {
    || (! import.meta.env.PROD && /^0.0.0.0$/.test(input));
}

+

+
// Get the gravatar URL of an email.
+
export function gravatarURL(email: string): string {
+
  const address = email.trim().toLowerCase();
+
  const hash = md5(address);
+

+
  return `https://www.gravatar.com/avatar/${hash}`;
+
}
+

// Propose a Gnosis Safe multi-sig transaction.
export async function proposeSafeTransaction(
  safeTx: SafeTransaction,