Improve commit history and detail view

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier
2022-04-08 15:36:47 +02:00
parent fabb90f91e
commit 0cced197ba
11 changed files with 265 additions and 112 deletions
+30 -24
View File
@@ -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"
}
}
]
}
+1 -1
View File
@@ -20,7 +20,7 @@
"peer": {
"id": "hyyg555wwkkutaysg6yr67qnu5d5ji54iur3n5uzzszndh8dp7ofue",
"person": {
"name": "sebastinez"
"name": "dabit3"
},
"delegate": true
}
+14 -19
View File
@@ -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()
+60 -4
View File
@@ -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",
+4 -1
View File
@@ -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",
+18 -23
View File
@@ -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} />
@@ -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>
+42 -29
View File
@@ -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";
import Icon from "@app/Icon.svelte";
import CommitAuthorship from "./CommitAuthorship.svelte";
export let commit: CommitMetadata;
export let project: Project;
const navigateHistory = (revision: string, content?: ProjectContent) => {
project.navigateTo({ content, revision, path: null });
};
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 {
.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-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>
+20 -10
View File
@@ -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>
+5 -1
View File
@@ -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[] {
+10
View File
@@ -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,