mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-11 04:30:34 +08:00
Add revision commit list
This commit is contained in:
@@ -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