mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Add revision commit list
This commit is contained in:
@@ -42,9 +42,8 @@ pub async fn diff_stats(
|
||||
pub async fn list_commits(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: RepoId,
|
||||
parent: Option<String>,
|
||||
skip: Option<usize>,
|
||||
take: Option<usize>,
|
||||
) -> Result<types::cobs::PaginatedQuery<Vec<types::repo::Commit>>, Error> {
|
||||
ctx.list_commits(rid, parent, skip, take)
|
||||
base: String,
|
||||
head: String,
|
||||
) -> Result<Vec<types::repo::Commit>, Error> {
|
||||
ctx.list_commits(rid, base, head)
|
||||
}
|
||||
|
||||
@@ -210,158 +210,26 @@ pub trait Repo: Profile {
|
||||
fn list_commits(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
parent: Option<String>,
|
||||
skip: Option<usize>,
|
||||
take: Option<usize>,
|
||||
) -> Result<cobs::PaginatedQuery<Vec<repo::Commit>>, Error> {
|
||||
base: String,
|
||||
head: String,
|
||||
) -> Result<Vec<repo::Commit>, Error> {
|
||||
let profile = self.profile();
|
||||
let cursor = skip.unwrap_or(0);
|
||||
let take = take.unwrap_or(20);
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
|
||||
let sha = match parent {
|
||||
Some(commit) => commit,
|
||||
None => repo.head()?.1.to_string(),
|
||||
};
|
||||
|
||||
let repo = surf::Repository::open(repo.path())?;
|
||||
let history = repo.history(&sha)?;
|
||||
let history = repo.history(&head)?;
|
||||
|
||||
let mut commits = history
|
||||
let commits = history
|
||||
.take_while(|c| {
|
||||
if let Ok(c) = c {
|
||||
c.id.to_string() != base
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.filter_map(|c| c.map(Into::into).ok())
|
||||
.skip(cursor)
|
||||
.take(take + 1); // Take one extra item to check if there's more.
|
||||
.collect();
|
||||
|
||||
let paginated_commits: Vec<_> = commits.by_ref().take(take).collect();
|
||||
let more = commits.next().is_some();
|
||||
|
||||
Ok::<_, Error>(cobs::PaginatedQuery {
|
||||
cursor,
|
||||
more,
|
||||
content: paginated_commits.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
use std::vec;
|
||||
|
||||
use radicle::crypto::test::signer::MockSigner;
|
||||
use radicle::{git, test};
|
||||
use radicle_surf::Author;
|
||||
|
||||
use crate::cobs;
|
||||
use crate::repo;
|
||||
use crate::traits::repo::Repo;
|
||||
use crate::AppState;
|
||||
|
||||
#[test]
|
||||
fn list_commits_pagination() {
|
||||
let signer = MockSigner::from_seed([0xff; 32]);
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let profile = crate::test::profile(tempdir.path(), [0xff; 32]);
|
||||
let (rid, _, _, _) =
|
||||
test::fixtures::project(tempdir.path().join("original"), &profile.storage, &signer)
|
||||
.unwrap();
|
||||
let state = AppState { profile };
|
||||
let commits = Repo::list_commits(&state, rid, None, None, Some(1)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
commits,
|
||||
cobs::PaginatedQuery {
|
||||
cursor: 0,
|
||||
more: true,
|
||||
content: vec![repo::Commit {
|
||||
id: git::Oid::from_str("f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354").unwrap(),
|
||||
author: Author {
|
||||
name: "anonymous".to_string(),
|
||||
email: "anonymous@radicle.xyz".to_string(),
|
||||
time: radicle::git::raw::Time::new(1514817556, 0).into(),
|
||||
},
|
||||
committer: Author {
|
||||
name: "anonymous".to_string(),
|
||||
email: "anonymous@radicle.xyz".to_string(),
|
||||
time: radicle::git::raw::Time::new(1514817556, 0).into(),
|
||||
},
|
||||
message: "Second commit".to_string(),
|
||||
summary: "Second commit".to_string(),
|
||||
parents: vec![
|
||||
git::Oid::from_str("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9").unwrap()
|
||||
],
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
let commits = Repo::list_commits(&state, rid, None, Some(1), None).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
commits,
|
||||
cobs::PaginatedQuery {
|
||||
cursor: 1,
|
||||
more: false,
|
||||
content: vec![repo::Commit {
|
||||
id: git::Oid::from_str("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9").unwrap(),
|
||||
author: Author {
|
||||
name: "anonymous".to_string(),
|
||||
email: "anonymous@radicle.xyz".to_string(),
|
||||
time: radicle::git::raw::Time::new(1514817556, 0).into(),
|
||||
},
|
||||
committer: Author {
|
||||
name: "anonymous".to_string(),
|
||||
email: "anonymous@radicle.xyz".to_string(),
|
||||
time: radicle::git::raw::Time::new(1514817556, 0).into(),
|
||||
},
|
||||
message: "Initial commit".to_string(),
|
||||
summary: "Initial commit".to_string(),
|
||||
parents: vec![],
|
||||
}],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_commits_with_head() {
|
||||
let signer = MockSigner::from_seed([0xff; 32]);
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let profile = crate::test::profile(tempdir.path(), [0xff; 32]);
|
||||
let (rid, _, _, _) =
|
||||
test::fixtures::project(tempdir.path().join("original"), &profile.storage, &signer)
|
||||
.unwrap();
|
||||
let state = AppState { profile };
|
||||
let commits = Repo::list_commits(
|
||||
&state,
|
||||
rid,
|
||||
Some("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9".to_string()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
commits,
|
||||
cobs::PaginatedQuery {
|
||||
cursor: 0,
|
||||
more: false,
|
||||
content: vec![repo::Commit {
|
||||
id: git::Oid::from_str("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9").unwrap(),
|
||||
author: Author {
|
||||
name: "anonymous".to_string(),
|
||||
email: "anonymous@radicle.xyz".to_string(),
|
||||
time: radicle::git::raw::Time::new(1514817556, 0).into(),
|
||||
},
|
||||
committer: Author {
|
||||
name: "anonymous".to_string(),
|
||||
email: "anonymous@radicle.xyz".to_string(),
|
||||
time: radicle::git::raw::Time::new(1514817556, 0).into(),
|
||||
},
|
||||
message: "Initial commit".to_string(),
|
||||
summary: "Initial commit".to_string(),
|
||||
parents: vec![],
|
||||
}],
|
||||
}
|
||||
);
|
||||
Ok(commits)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+48
@@ -25,6 +25,7 @@
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@wooorm/starry-night": "^3.5.0",
|
||||
@@ -46,6 +47,7 @@
|
||||
"marked-footnote": "^1.2.4",
|
||||
"marked-katex-extension": "^5.1.3",
|
||||
"marked-linkify-it": "^3.1.12",
|
||||
"md5": "^2.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"svelte": "^5.14.0",
|
||||
@@ -1373,6 +1375,13 @@
|
||||
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/md5": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz",
|
||||
"integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||
@@ -1972,6 +1981,16 @@
|
||||
"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": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
@@ -2056,6 +2075,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -2867,6 +2896,13 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"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,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
@@ -3197,6 +3233,18 @@
|
||||
"marked": ">=4 <16"
|
||||
}
|
||||
},
|
||||
"node_modules/md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@wooorm/starry-night": "^3.5.0",
|
||||
@@ -61,6 +62,7 @@
|
||||
"marked-footnote": "^1.2.4",
|
||||
"marked-katex-extension": "^5.1.3",
|
||||
"marked-linkify-it": "^3.1.12",
|
||||
"md5": "^2.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"svelte": "^5.14.0",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import CompactCommitAuthorship from "@app/components/CompactCommitAuthorship.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Id from "@app/components/Id.svelte";
|
||||
import InlineTitle from "@app/components/InlineTitle.svelte";
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
|
||||
interface Props {
|
||||
commit: Commit;
|
||||
}
|
||||
|
||||
const { commit }: Props = $props();
|
||||
|
||||
let commitMessageVisible = $state(false);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.teaser {
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
align-items: start;
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
.message {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-left: auto;
|
||||
height: 21px;
|
||||
}
|
||||
.commit-message {
|
||||
font-size: var(--font-size-tiny);
|
||||
}
|
||||
.commit-expand-button {
|
||||
height: 21px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="teaser" aria-label="commit-teaser">
|
||||
<div class="left">
|
||||
<div class="message">
|
||||
<div class="summary" use:twemoji>
|
||||
<InlineTitle fontSize="small" content={commit.summary} />
|
||||
</div>
|
||||
{#if commit.message.trim() !== commit.summary.trim()}
|
||||
<div class="commit-expand-button">
|
||||
<NakedButton
|
||||
stylePadding="0 4px"
|
||||
variant="ghost"
|
||||
onclick={() => {
|
||||
commitMessageVisible = !commitMessageVisible;
|
||||
}}>
|
||||
<Icon name="ellipsis" />
|
||||
</NakedButton>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if commitMessageVisible}
|
||||
<div class="commit-message">
|
||||
<pre>{commit.message.replace(commit.summary, "").trim()}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right">
|
||||
<CompactCommitAuthorship {commit}>
|
||||
<Id id={commit.id} variant="commit" />
|
||||
</CompactCommitAuthorship>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
import Border from "./Border.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import NakedButton from "./NakedButton.svelte";
|
||||
|
||||
interface Props {
|
||||
leftHeader: Snippet;
|
||||
children: Snippet;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { leftHeader, children, expanded }: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
padding-left: 0.25rem;
|
||||
font-size: var(--font-size-small);
|
||||
background-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.divider {
|
||||
width: calc(100% + 4px);
|
||||
position: relative;
|
||||
top: -6px;
|
||||
left: -2px;
|
||||
z-index: 1;
|
||||
height: 2px;
|
||||
background-color: var(--color-fill-ghost);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Border
|
||||
variant="ghost"
|
||||
styleFlexDirection="column"
|
||||
styleAlignItems="flex-start">
|
||||
<div class="header" class:collapsed={!expanded}>
|
||||
<div class="left">
|
||||
<NakedButton
|
||||
stylePadding="0 4px"
|
||||
variant="ghost"
|
||||
onclick={async () => {
|
||||
expanded = !expanded;
|
||||
}}>
|
||||
<Icon name={expanded ? "chevron-down" : "chevron-right"} />
|
||||
</NakedButton>
|
||||
{@render leftHeader()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
<div class="divider"></div>
|
||||
<div style:width="100%">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</Border>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import HoverPopover from "./HoverPopover.svelte";
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
commit: Commit;
|
||||
}
|
||||
|
||||
const { children, commit }: Props = $props();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.authorship {
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.person {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
gap: 0.5rem;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
.label {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--color-foreground-dim);
|
||||
}
|
||||
.avatar {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
clip-path: var(--1px-corner-fill);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="authorship">
|
||||
<HoverPopover
|
||||
stylePopoverPositionLeft="-8rem"
|
||||
stylePopoverPositionBottom="1.5rem">
|
||||
{#snippet toggle()}
|
||||
<div style="display: flex;">
|
||||
{#if commit.author.email === commit.committer.email}
|
||||
<img
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
src={utils.gravatarURL(commit.committer.email)} />
|
||||
{:else}
|
||||
<img
|
||||
style:margin-right="0.25rem"
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
src={utils.gravatarURL(commit.author.email)} />
|
||||
<img
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
src={utils.gravatarURL(commit.committer.email)} />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet popover()}
|
||||
<div class="popover">
|
||||
{#if commit.author.email === commit.committer.email}
|
||||
<div class="person">
|
||||
<div class="label">Author</div>
|
||||
<img
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
src={utils.gravatarURL(commit.committer.email)} />
|
||||
{commit.author.name}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="person">
|
||||
<div class="label">Author</div>
|
||||
<img
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
src={utils.gravatarURL(commit.author.email)} />
|
||||
{commit.author.name}
|
||||
</div>
|
||||
<div class="person">
|
||||
<div class="label">Committer</div>
|
||||
<img
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
src={utils.gravatarURL(commit.committer.email)} />
|
||||
{commit.committer.name}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</HoverPopover>
|
||||
{@render children()}
|
||||
<div title={utils.absoluteTimestamp(commit.committer.time * 1000)}>
|
||||
{utils.formatTimestamp(commit.committer.time * 1000)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
interface Props {
|
||||
popover: Snippet;
|
||||
stylePopoverPositionBottom: string | undefined;
|
||||
stylePopoverPositionLeft: string | undefined;
|
||||
toggle: Snippet;
|
||||
}
|
||||
|
||||
const {
|
||||
popover,
|
||||
stylePopoverPositionBottom,
|
||||
stylePopoverPositionLeft,
|
||||
toggle,
|
||||
}: Props = $props();
|
||||
|
||||
let visible: boolean = $state(false);
|
||||
|
||||
const setVisible = debounce((value: boolean) => {
|
||||
visible = value;
|
||||
}, 50);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.popover {
|
||||
background: var(--color-fill-ghost);
|
||||
border-radius: var(--border-radius-regular);
|
||||
padding: 0.5rem 1rem;
|
||||
box-shadow: var(--elevation-low);
|
||||
position: absolute;
|
||||
clip-path: var(--2px-corner-fill);
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onmouseenter={() => setVisible(true)}
|
||||
onmouseleave={() => setVisible(false)}>
|
||||
{@render toggle()}
|
||||
|
||||
{#if visible}
|
||||
<div style:position="absolute">
|
||||
<div
|
||||
class="popover"
|
||||
style:left={stylePopoverPositionLeft}
|
||||
style:bottom={stylePopoverPositionBottom}>
|
||||
{@render popover()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,6 +29,7 @@
|
||||
| "dashboard"
|
||||
| "delegate"
|
||||
| "diff"
|
||||
| "ellipsis"
|
||||
| "expand"
|
||||
| "expand-panel"
|
||||
| "eye"
|
||||
@@ -432,6 +433,10 @@
|
||||
<path d="M7 4H8V9H7V4Z" />
|
||||
<path d="M5 6H10V7H5V6Z" />
|
||||
<path d="M5 10H10V11H5V10Z" />
|
||||
{:else if name === "ellipsis"}
|
||||
<path d="M2 7H4V9H2V7Z" />
|
||||
<path d="M7 7H9V9H7V7Z" />
|
||||
<path d="M12 7H14V9H12V7Z" />
|
||||
{:else if name === "expand"}
|
||||
<path d="M9 1.5H8V2.5H9V1.5Z" />
|
||||
<path d="M10 2.5H9V3.5H10V2.5Z" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Diff } from "@bindings/diff/Diff";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
@@ -16,9 +17,12 @@
|
||||
import { publicKeyFromDid, scrollIntoView } from "@app/lib/utils";
|
||||
|
||||
import Changeset from "@app/components/Changeset.svelte";
|
||||
import CobCommitTeaser from "./CobCommitTeaser.svelte";
|
||||
import CommentComponent from "@app/components/Comment.svelte";
|
||||
import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
|
||||
import CommitsContainer from "@app/components/CommitsContainer.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Id from "./Id.svelte";
|
||||
import NakedButton from "./NakedButton.svelte";
|
||||
import ReviewTeaser from "@app/components/ReviewTeaser.svelte";
|
||||
import ThreadComponent from "@app/components/Thread.svelte";
|
||||
@@ -226,6 +230,14 @@
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCommits(rid: string, base: string, head: string) {
|
||||
return invoke<Commit[]>("list_commits", {
|
||||
rid,
|
||||
base,
|
||||
head,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -255,6 +267,32 @@
|
||||
margin-left: 1.25rem;
|
||||
background-color: var(--color-background-float);
|
||||
}
|
||||
.commits {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 0.5rem 0.5rem 1rem;
|
||||
border-left: 1px solid var(--color-fill-separator);
|
||||
}
|
||||
.commit:last-of-type::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -18.5px;
|
||||
top: 14px;
|
||||
bottom: -0.5rem;
|
||||
border-left: 4px solid var(--color-background-default);
|
||||
}
|
||||
.commit-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
position: absolute;
|
||||
top: 0.625rem;
|
||||
left: -18.5px;
|
||||
background-color: var(--color-fill-separator);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="txt-small patch-body">
|
||||
@@ -282,15 +320,14 @@
|
||||
</div>
|
||||
|
||||
<div style:margin="1rem 0">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="txt-semibold global-flex"
|
||||
style:margin-bottom="1rem"
|
||||
style:cursor="pointer"
|
||||
onclick={() => (hideDiscussion = !hideDiscussion)}>
|
||||
<Icon name={hideDiscussion ? "chevron-right" : "chevron-down"} />Discussion
|
||||
<div class="global-flex" style:margin-bottom="1rem">
|
||||
<NakedButton
|
||||
stylePadding="0 4px"
|
||||
variant="ghost"
|
||||
onclick={() => (hideDiscussion = !hideDiscussion)}>
|
||||
<Icon name={hideDiscussion ? "chevron-right" : "chevron-down"} />
|
||||
<div class="txt-semibold global-flex txt-regular">Discussion</div>
|
||||
</NakedButton>
|
||||
</div>
|
||||
<div class:hide={hideDiscussion}>
|
||||
{#each threads as thread}
|
||||
@@ -325,17 +362,16 @@
|
||||
|
||||
{#if revision.reviews && revision.reviews.length}
|
||||
<div style:margin="1rem 0">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="txt-semibold global-flex"
|
||||
style:margin-bottom="1rem"
|
||||
style:cursor="pointer"
|
||||
onclick={() => (hideReviews = !hideReviews)}>
|
||||
<Icon name={hideReviews ? "chevron-right" : "chevron-down"} />Reviews
|
||||
<div class="global-flex" style:margin-bottom="1rem">
|
||||
<NakedButton
|
||||
stylePadding="0 4px"
|
||||
variant="ghost"
|
||||
onclick={() => (hideReviews = !hideReviews)}>
|
||||
<Icon name={hideReviews ? "chevron-right" : "chevron-down"} />
|
||||
<div class="txt-semibold global-flex txt-regular">Reviews</div>
|
||||
</NakedButton>
|
||||
</div>
|
||||
<div class:hide={hideReviews}>
|
||||
<div class:hide={hideReviews} style:margin-top="1rem">
|
||||
{#each revision.reviews as review}
|
||||
<ReviewTeaser {rid} {review} />
|
||||
{/each}
|
||||
@@ -343,19 +379,16 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="txt-semibold global-flex"
|
||||
style:margin-bottom={hideChanges ? undefined : "1rem"}>
|
||||
<div
|
||||
style:cursor="pointer"
|
||||
style:min-height="2rem"
|
||||
class="global-flex"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
<NakedButton
|
||||
stylePadding="0 4px"
|
||||
variant="ghost"
|
||||
onclick={() => (hideChanges = !hideChanges)}>
|
||||
<Icon name={hideChanges ? "chevron-right" : "chevron-down"} />Changes
|
||||
</div>
|
||||
<Icon name={hideChanges ? "chevron-right" : "chevron-down"} />
|
||||
<div class="txt-semibold global-flex txt-regular">Changes</div>
|
||||
</NakedButton>
|
||||
{#if !hideChanges}
|
||||
<div style:margin-left="auto">
|
||||
<NakedButton
|
||||
@@ -372,7 +405,36 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class:hide={hideChanges}>
|
||||
{#await loadCommits(rid, revision.base, revision.head) then commits}
|
||||
<div style:margin-bottom="1rem">
|
||||
<CommitsContainer expanded={filesExpanded}>
|
||||
{#snippet leftHeader()}
|
||||
<div class="txt-semibold">Commits</div>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
<div style:padding="0 1rem">
|
||||
<div
|
||||
class="global-flex txt-small"
|
||||
style:color="var(--color-foreground-dim)">
|
||||
<Icon name="branch" /><Id id={revision.base} variant="commit" />
|
||||
<div class="global-counter">base</div>
|
||||
</div>
|
||||
<div class="commits">
|
||||
{#each commits.reverse() as commit}
|
||||
<div class="commit" style:position="relative">
|
||||
<div class="commit-dot"></div>
|
||||
<CobCommitTeaser {commit} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</CommitsContainer>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
{#await loadHighlightedDiff(rid, revision.base, revision.head)}
|
||||
<span class="txt-small">Loading…</span>
|
||||
{:then diff}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
|
||||
import bs58 from "bs58";
|
||||
import twemojiModule from "twemoji";
|
||||
import md5 from "md5";
|
||||
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
|
||||
@@ -211,3 +212,11 @@ export function parseNodeId(
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user