thunderbird-patch-review

Easy-to-use patch review interface for Thunderbird

Contribute: ~marcc/thunderbird-review-plugin@lists.sr.ht

git clone git://mccd.space/thunderbird-patch-review

commit a50534bb5d2f55be7d2f3112fe979aad4e0d81a1
parent 661802884b46373d88e7e52ee3806a87248a5813
Author: Pi Agent <agent@pi.local>
Date:   Sat, 18 Jul 2026 13:11:52 +0200

review: hide Comment-on-patch button while editing; add Discard drafts

When the patch-level comment editor opens, hide the trigger button until
the editor is cancelled (post re-renders, dropping it anyway once a draft
exists). The button is no longer offered as "Edit comment" when a draft
is present — the readonly card (double-click / click to edit) is the only
entry point, so there is no redundant modify button after posting.

Add a red "Discard drafts…" button in the toolbar, to the left of the
comment count and repo path. It asks for confirmation, then clears every
draft comment in the series (per-patch Message-ID storage) and re-renders.

Diffstat:
Mextension/review/review.css | 14++++++++++++++
Mextension/review/review.html | 1+
Mextension/review/review.js | 44++++++++++++++++++++++++++++++++++++--------
3 files changed, 51 insertions(+), 8 deletions(-)
diff --git a/extension/review/review.css b/extension/review/review.css
@@ -109,6 +109,20 @@ button {
 }
 
 button:hover { border-color: var(--accent); color: var(--accent); }
+
+#btn-discard {
+  border-color: #cf222e;
+  color: #cf222e;
+}
+#btn-discard:hover {
+  background: #cf222e;
+  border-color: #cf222e;
+  color: #fff;
+}
+@media (prefers-color-scheme: dark) {
+  #btn-discard { border-color: #f85149; color: #f85149; }
+  #btn-discard:hover { background: #f85149; border-color: #f85149; color: #0d1117; }
+}
 button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
 button.primary:hover { color: #fff; opacity: 0.9; }
 
diff --git a/extension/review/review.html b/extension/review/review.html
@@ -9,6 +9,7 @@
   <header id="topbar">
     <div id="series-title">Loading…</div>
     <div id="actions">
+      <button id="btn-discard" title="Discard all draft comments in this series" hidden>Discard drafts…</button>
       <span id="comment-count" title="Draft comments">0 comments</span>
       <input id="repo-path" type="text" placeholder="repository path" spellcheck="false">
       <button id="btn-browse" title="Choose the repository directory">Browse…</button>
diff --git a/extension/review/review.js b/extension/review/review.js
@@ -139,16 +139,21 @@ function renderPatch() {
     ? parsed.commitMessage || patch.body
     : parsed.commitMessage || patch.title;
   msg.appendChild(body);
-  const generalBtn = document.createElement("button");
-  generalBtn.className = "btn-general";
-  generalBtn.textContent = state.comments[i][store.GENERAL] ? "Edit comment" : "Comment on patch";
-  generalBtn.addEventListener("click", () => openGeneralEditor(msg));
-  msg.appendChild(generalBtn);
+  const existingGeneral = state.comments[i][store.GENERAL];
+  // Only offer "Comment on patch" when there is no draft yet; once a comment
+  // exists the readonly card (double-click / click to edit) is the only
+  // entry point, so we don't also surface an "Edit comment" button.
+  if (!existingGeneral) {
+    const generalBtn = document.createElement("button");
+    generalBtn.className = "btn-general";
+    generalBtn.textContent = "Comment on patch";
+    generalBtn.addEventListener("click", () => openGeneralEditor(msg, generalBtn));
+    msg.appendChild(generalBtn);
+  }
   box.appendChild(msg);
 
-  const existingGeneral = state.comments[i][store.GENERAL];
   if (existingGeneral) {
-    box.appendChild(commentCard(existingGeneral.text, () => openGeneralEditor(msg)));
+    box.appendChild(commentCard(existingGeneral.text, () => openGeneralEditor(msg, null)));
   }
   content.appendChild(box);
 
@@ -369,7 +374,7 @@ function findReadonly(lineRow) {
   return null;
 }
 
-function openGeneralEditor(anchorEl) {
+function openGeneralEditor(anchorEl, triggerBtn) {
   closeEditors();
   const existing = state.comments[state.current][store.GENERAL];
   // The readonly general comment, if any, is the element right after the
@@ -379,9 +384,17 @@ function openGeneralEditor(anchorEl) {
     ? anchorEl.nextElementSibling
     : null;
   if (ro) ro.hidden = true;
+  // While the editor is open the trigger button ("Comment on patch") is
+  // hidden; cancel re-shows it. Post re-renders, which drops it anyway when
+  // a comment now exists.
+  if (triggerBtn) triggerBtn.hidden = true;
+  const restoreTrigger = () => {
+    if (triggerBtn) triggerBtn.hidden = false;
+  };
   const box = editorBox(existing ? existing.text : "", store.GENERAL, () => {
     box.remove();
     if (ro) ro.hidden = false;
+    restoreTrigger();
   });
   box.classList.add("general-editor");
   anchorEl.after(box);
@@ -448,8 +461,23 @@ function editorBox(initial, loc, close) {
 function updateCount() {
   const total = state.comments.reduce((sum, c) => sum + Object.keys(c).length, 0);
   $("#comment-count").textContent = `${total} comment${total === 1 ? "" : "s"}`;
+  // The discard button only makes sense once there are drafts to lose.
+  $("#btn-discard").hidden = total === 0;
 }
 
+$("#btn-discard").addEventListener("click", async () => {
+  const total = state.comments.reduce((sum, c) => sum + Object.keys(c).length, 0);
+  if (total === 0) return;
+  const fill = total === 1 ? `the 1 draft comment` : `all ${total} draft comments`;
+  if (!window.confirm(`Discard ${fill} in this series? This cannot be undone.`)) {
+    return;
+  }
+  await Promise.all(state.series.map((p) => store.clearComments(p.headerMessageId)));
+  state.comments = state.series.map(() => ({}));
+  renderPatch();
+  updateCount();
+});
+
 // ---------------------------------------------------------------------------
 // Send review / apply series