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

Christian Brauner <[email protected]> Fri, 31 Jul 2026 11:21:00 +0200
Newsgroups org.kernel.linux.tools
Message-ID <20260731-work-b4-editor-branch-guard-v1-1-de68a7c8e4cb@kernel.org>
Marking outgoing mail as Seen at send time is done in the review app, so
only per-patch and cover letter replies get it. The thank-you note from
the tracking UI and the follow-up reply from the lite thread viewer
don't. They rely on the pass that runs when the thread is fetched again
and marks anything whose From matches the configured identity.

That pass matches the address exactly, so it misses whenever the mail
went out from somewhere else. b4.thanks-from-email exists precisely so
that thank-yous can do that. The maintainer's own message then comes
back from the list and lights up the unread badge for the series.

Move it into a helper next to the other shared TUI utilities. It
respects --email-dry-run and it never raises, because it runs when the
message is already gone and mustn't be able to turn a delivered message
into a reported failure. Wire up the follow-up reply here. 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