[PATCH b4 v2 01/44] review-tui: mark all outgoing mail as read, not just review replies

Christian Brauner <[email protected]> Fri, 31 Jul 2026 23:58:42 +0200
Newsgroups org.kernel.linux.tools
Message-ID <20260731-work-b4-editor-branch-guard-v2-1-243fd19d322d@kernel.org>
Only review replies were marked as read at send time. The thank-you
note and the lite viewer's follow-up reply relied on matching the
sender address on the next fetch, which fails when the mail goes out
from another address such as b4.thanks-from-email. The message then
comes back from the list and lights up the unread badge.

Add a shared helper that marks outgoing mail as read at send time. It
respects --email-dry-run and never raises. Use it for the follow-up
reply; the thank-you path follows in a later patch.

Signed-off-by: Christian Brauner (Amutable) <[email protected]>
---
 src/b4/review_tui/_common.py     | 23 +++++++++++++++++++++++
 src/b4/review_tui/_lite_app.py   |  2 ++
 src/b4/review_tui/_review_app.py | 16 +++-------------
 3 files changed, 28 insertions(+), 13 deletions(-)

diff --git a/src/b4/review_tui/_common.py b/src/b4/review_tui/_common.py
index 935f553..33ab7ea 100644
--- a/src/b4/review_tui/_common.py
+++ b/src/b4/review_tui/_common.py
@@ -114,6 +114,29 @@ _CallFromThreadReturn = TypeVar('_CallFromThreadReturn')
 _WorkerResult = TypeVar('_WorkerResult')
 
 
+def mark_outgoing_seen(
+    msgs: List[email.message.EmailMessage], dryrun: bool = False
+) -> None:
+    """Flag messages b4 just sent as read, before the list echoes them back.
+
+    Every send path that puts a message into a tracked thread wants this:
+    a reply, a follow-up, a thank-you note.  Without it the maintainer's
+    own mail comes back a few minutes later and lights up the series'
+    unread badge.
+
+    Never raises: the message is already gone, so a bookkeeping failure
+    here must not be reported to the caller as a failure to send.
+    """
+    if dryrun:
+        return
+    try:
+        from b4.review import messages
+
+        messages.mark_outgoing_seen(msgs)
+    except Exception as ex:
+        logger.debug('Could not mark sent messages as seen: %s', ex)
+
+
 def get_thread_msgs(
     topdir: str,
     message_id: str,
diff --git a/src/b4/review_tui/_lite_app.py b/src/b4/review_tui/_lite_app.py
index 9c418ad..86a201e 100644
--- a/src/b4/review_tui/_lite_app.py
+++ b/src/b4/review_tui/_lite_app.py
@@ -26,6 +26,7 @@ from b4.review_tui._common import (
     _quiet_worker,
     _write_diff_line,
     display_width,
+    mark_outgoing_seen,
     pad_display,
     resolve_styles,
     run_lore_worker,
@@ -816,6 +817,7 @@ class LiteThreadScreen(ModalScreen[None]):
                 self.app.notify(f'Dry-run: reply to {lmsg.fromemail} logged, not sent')
                 self._mark_answered(node)
             else:
+                mark_outgoing_seen([msg], dryrun=self._email_dryrun)
                 self.app.notify(f'Reply sent to {lmsg.fromemail}')
                 self._mark_answered(node)
         except Exception as ex:
diff --git a/src/b4/review_tui/_review_app.py b/src/b4/review_tui/_review_app.py
index 81c3890..8139518 100644
--- a/src/b4/review_tui/_review_app.py
+++ b/src/b4/review_tui/_review_app.py
@@ -48,6 +48,7 @@ from b4.review_tui._common import (
     _write_followup_trailers,
     get_thread_msgs,
     logger,
+    mark_outgoing_seen,
     notify_quit_hint,
     resolve_styles,
     reviewer_colours,
@@ -1664,7 +1665,7 @@ class ReviewApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[None]):
                             review['sent-revision'] = current_rev
                     self._save_tracking()
                     self._mark_patches_answered(msgs)
-                    self._mark_outgoing_seen(msgs)
+                    mark_outgoing_seen(msgs, dryrun=self._email_dryrun)
                     self.notify(f'Sent {sent} review email(s).')
             except Exception as ex:
                 self.notify(f'Send failed: {ex}', severity='error')
@@ -1746,7 +1747,7 @@ class ReviewApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[None]):
             elif self._email_dryrun:
                 self.notify(f'Dry-run: reply to {entry["fromemail"]} logged, not sent')
             else:
-                self._mark_outgoing_seen([msg])
+                mark_outgoing_seen([msg], dryrun=self._email_dryrun)
                 self.notify(f'Reply sent to {entry["fromemail"]}')
         except Exception as ex:
             self.notify(f'Send failed: {ex}', severity='error')
@@ -2063,17 +2064,6 @@ class ReviewApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[None]):
         except Exception:
             pass
 
-    def _mark_outgoing_seen(self, msgs: List[Any]) -> None:
-        """Mark just-sent messages as Seen so they never show up as unread."""
-        if self._email_dryrun:
-            return
-        try:
-            from b4.review import messages
-
-            messages.mark_outgoing_seen(msgs)
-        except Exception:
-            pass
-
     def _mark_patches_answered(self, msgs: List[Any]) -> None:
         """Mark the original patches as Answered based on In-Reply-To."""
         entries = []

-- 
2.53.0