Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Inline redact form and hide Attest from authors
Daniel Norman committed 7 days ago
commit 973bd014910605c7807c1d2f0150bb533d98a719
parent 4ab0237ce91ca4bc3ad50730647b364627be0d3f
1 file changed +124 -15
modified src/views/repo/Release.svelte
@@ -11,7 +11,7 @@
  import { invoke, InvokeError } from "@app/lib/invoke";
  import { isDelegateOrAuthor } from "@app/lib/roles";
  import * as router from "@app/lib/router";
-
  import { authorForNodeId } from "@app/lib/utils";
+
  import { authorForNodeId, publicKeyFromDid } from "@app/lib/utils";

  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
@@ -68,6 +68,14 @@
    );
  }

+
  // Hide the Attest button for the artifact's own author — attestation
+
  // is a vouching signal from someone other than the author, and a
+
  // self-attestation is a no-op in the COB. Delegates who happen to also
+
  // be the author follow the same rule.
+
  function isOwnArtifact(artifact: Artifact): boolean {
+
    return publicKeyFromDid(artifact.author.did) === config.publicKey;
+
  }
+

  type PeerRole = "author" | "delegate" | "other";

  // Classify a peer DID relative to a specific artifact so the UI can
@@ -92,6 +100,12 @@
  // away by default but can be expanded per artifact.
  const revealLocations = $state<Record<string, boolean>>({});
  const revealAttestations = $state<Record<string, boolean>>({});
+
  // Per-artifact redact form: { reason } when open, undefined when closed.
+
  // Inline because window.prompt is a no-op in Tauri's WebKit webview, so
+
  // the dialog-based flow silently did nothing.
+
  const redactDraft = $state<Record<string, { reason: string } | undefined>>(
+
    {},
+
  );

  const draft = $state<Record<string, { key: string; value: string }>>({});
  $effect(() => {
@@ -262,13 +276,22 @@
    }
  }

-
  async function redact(artifact: Artifact) {
-
    const reason = window.prompt(
-
      `Redact artifact "${artifact.name}"?\n\n` +
-
        "This adds a redaction signed by you to the release COB. The CID stays\n" +
-
        "on the COB so other peers can see why you redacted it. Type a reason:",
-
    );
-
    if (reason === null) return;
+
  function openRedact(artifact: Artifact) {
+
    redactDraft[artifact.cid] = { reason: "" };
+
  }
+

+
  function cancelRedact(artifact: Artifact) {
+
    redactDraft[artifact.cid] = undefined;
+
  }
+

+
  async function confirmRedact(artifact: Artifact) {
+
    const draft = redactDraft[artifact.cid];
+
    if (!draft) return;
+
    const reason = draft.reason.trim();
+
    if (!reason) {
+
      actionErrors[artifact.cid] = "Please provide a reason for the redaction.";
+
      return;
+
    }
    busy[artifact.cid] = true;
    actionErrors[artifact.cid] = undefined;
    try {
@@ -278,6 +301,7 @@
        cid: artifact.cid,
        reason,
      });
+
      redactDraft[artifact.cid] = undefined;
      await refresh();
    } catch (err) {
      console.error("redact failed", err);
@@ -554,6 +578,51 @@
    font-size: 1rem;
    padding: 0;
  }
+
  .redact-form {
+
    margin-top: 0.5rem;
+
    padding: 0.75rem;
+
    border: 1px solid var(--color-feedback-error-border);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-feedback-error-bg);
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
  .redact-form-title {
+
    font: var(--txt-body-s-semibold);
+
    color: var(--color-feedback-error-text);
+
  }
+
  .redact-form textarea {
+
    resize: vertical;
+
    min-height: 3rem;
+
    padding: 0.4rem 0.5rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-canvas);
+
    font: var(--txt-body-s-regular);
+
  }
+
  .redact-form .row {
+
    display: flex;
+
    gap: 0.5rem;
+
    justify-content: flex-end;
+
  }
+
  .redact-form button {
+
    padding: 0.25rem 0.75rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-subtle);
+
    cursor: pointer;
+
    font: var(--txt-body-s-regular);
+
  }
+
  .redact-form button.confirm {
+
    background-color: var(--color-feedback-error-fill);
+
    color: var(--color-text-inverse);
+
    border-color: var(--color-feedback-error-fill);
+
  }
+
  .redact-form button:disabled {
+
    opacity: 0.5;
+
    cursor: default;
+
  }
  .empty {
    padding: 2rem;
    color: var(--color-text-secondary);
@@ -753,16 +822,19 @@
                    Seed
                  </button>
                {/if}
-
                <button
-
                  onclick={() => attest(artifact)}
-
                  disabled={busy[artifact.cid]}>
-
                  Attest
-
                </button>
+
                {#if !isOwnArtifact(artifact)}
+
                  <button
+
                    onclick={() => attest(artifact)}
+
                    disabled={busy[artifact.cid]}>
+
                    Attest
+
                  </button>
+
                {/if}
                {#if canEditMetadata(artifact)}
                  <button
                    class="danger"
-
                    onclick={() => redact(artifact)}
-
                    disabled={busy[artifact.cid]}>
+
                    onclick={() => openRedact(artifact)}
+
                    disabled={busy[artifact.cid] ||
+
                      redactDraft[artifact.cid] !== undefined}>
                    Redact
                  </button>
                {/if}
@@ -838,6 +910,43 @@
                {/each}
              </div>
            {/if}
+
            {#if redactDraft[artifact.cid]}
+
              <form
+
                class="redact-form"
+
                onsubmit={e => {
+
                  e.preventDefault();
+
                  void confirmRedact(artifact);
+
                }}>
+
                <div class="redact-form-title">
+
                  Redact "{artifact.name}"?
+
                </div>
+
                <div style:font="var(--txt-body-s-regular)">
+
                  This records a signed redaction on the release COB. Other
+
                  peers will see it and the artifact will appear blurred for
+
                  delegates and authors. The CID and existing locations stay on
+
                  the COB.
+
                </div>
+
                <textarea
+
                  placeholder="Reason (required)"
+
                  bind:value={redactDraft[artifact.cid]!.reason}
+
                  disabled={busy[artifact.cid]}>
+
                </textarea>
+
                <div class="row">
+
                  <button
+
                    type="button"
+
                    onclick={() => cancelRedact(artifact)}
+
                    disabled={busy[artifact.cid]}>
+
                    Cancel
+
                  </button>
+
                  <button
+
                    type="submit"
+
                    class="confirm"
+
                    disabled={busy[artifact.cid]}>
+
                    {busy[artifact.cid] ? "Redacting…" : "Redact"}
+
                  </button>
+
                </div>
+
              </form>
+
            {/if}
            {#if artifact.metadata && Object.keys(artifact.metadata).length > 0}
              <dl class="metadata">
                {#each Object.entries(artifact.metadata) as [key, value] (key)}