Skip to content

feat: mark a message as unread from its menu - #1933

Merged
elie222 merged 1 commit into
elie222:mainfrom
ald3b:feat/unread-mark-message
Oct 23, 2026
Merged

elie222 merged 1 commit into
elie222:mainfrom
ald3b:feat/unread-mark-message

Conversation

@ald3b

@ald3b ald3b commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Why

"Mark as Unread" on a chat brings its dot back, but not the place to pick up from. When you read a long answer and want to come back to it later, the next open lands at the latest message with no "New" line, so you have to find that reply again yourself.

What changed

Every sent message's menu now has Mark as unread, right after Reply:

  • The chat's unread dot comes back straight away.
  • The chat stays unread for the rest of the visit, even though it's still on screen.
  • The next time you open it, the "New" divider sits above the first reply from that message on. On a reply, that's the reply itself. On your own message, it's the reply that followed it.
  • Live progress and subagent rows can't be marked, because they aren't stored messages.

Server.

  • threads/markUnread takes an optional messageId. With it, the thread is marked unread and the read cursor moves to just before that message.
  • A message that isn't in the thread is rejected as not found, so a stale or foreign id can't move the cursor.
  • Without messageId, the call behaves exactly as before.

Shared. One helper in @rakazo/core decides which messages offer the action, so mobile and web agree.

Mobile.

  • The native context menu shows the item second, after Reply, with the message.badge symbol (an outline speech bubble with a dot, matching the menu's other outline icons).
  • The Android action sheet and the VoiceOver actions get the same item in the same place.
  • While the chat is held unread, the screen skips its usual mark-read on focus, so the dot doesn't clear under you. Leaving or switching chats ends the hold.

Web and desktop.

  • The message's More menu shows the item after Reply, with a speech-bubble-with-dot icon.
  • The More menu now sizes to its content instead of its icon button, so labels stay on one line.

New copy. "Mark as unread", the menu item's label and accessible name.

  • It's needed because the item is the only way to set the resume point at a message. An icon alone in a text menu would be ambiguous.
  • The chat list's existing "Mark as Unread" is title case. The message menu's other items use sentence case ("Speak message", "Select text"), so this label follows the menu it sits in.
  • It appears only inside the message menu, which is already a reveal-on-demand surface.
  • It's translated in the mobile catalogs (de, ru, zh, nl) and in every web catalog, reusing each locale's existing "Mark as Unread" wording.

How it was tested

On an iOS 27 Simulator with a Debug build against a local server, and in Chromium against the same kind of server. The test account had a bot chat with two answered questions, all read. Light and dark mode were both checked.

  • Before the change: long-pressing a reply showed Reply, Copy, React, Quote, Speak message and Select text. There was no way to mark a single message unread.
  • After:
    • Long-pressing a reply shows Reply, Mark as unread, Copy and React, then Quote, Speak message and Select text. Light and dark mode were both checked.
    • Long-pressing the user's own second question shows the same item in the same place.
    • Choosing it there and going back shows the chat with its unread dot in the chat list.
    • Opening the chat again shows the "NEW" line between that question and the bot's reply to it. The first exchange is above it.
    • On web, the More menu lists Reply, Mark as unread and Copy on one line each. Marking the first reply and reopening shows the divider above that reply. Marking the user's second question and reopening shows the divider above the reply to it.

Unit tests. Each new test fails without this change.

  • Server: marking from a message sets the thread unread and moves the cursor to just before it, so the next markRead reports that message as the start. A message that isn't in the thread is refused and changes nothing.
  • Shared helper: any stored message, the user's own included, can be marked, but live progress and subagent rows can't. Marking the user's message starts the divider at the reply after it.
  • Mobile menu: "Mark as unread" comes right after Reply, only when asked for, and not in the inline group.

Full suites.

  • The mobile, API, core and contracts suites pass.
  • On a local Node version with built-in web storage, three web files that read localStorage fail. They fail the same way without this change, and they pass with that storage turned off.
  • Typecheck and lint pass.

Web e2e: "marking a message unread brings the chat back with New above the reply from there" (new).

  • Setup: a bot answers two questions and the chat is read.
  • The first reply's More menu lists Reply, Mark as unread and Copy. Choosing Mark as unread brings the chat's unread state back.
  • Reopening the chat shows the divider directly above that reply.
  • The same item on the user's second question puts the divider above the reply to it.
  • It captures message-mark-unread-menu and message-mark-unread-divider for CI.

Mobile. CI can't capture the native context menu; the iOS results above come from a simulator.

Not in this PR

  • The off-screen unread count in the chat list.

That is a separate change.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 23, 2026

Copy link
Copy Markdown

@ald3b is attempting to deploy a commit to the Inbox Zero Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change reaches unread behavior across web, mobile, and the thread API.

The delayed-read finding remains partly unresolved, so the PR should not merge yet.

Findings

  1. P1 Delayed reads erase reminders ▶

Summary

Adds “Mark as unread” to stored messages on mobile, web, and desktop.

  • Chats can resume unread from a chosen message.
  • Mobile message menus can mark a chat unread.
  • Web message menus can set where a chat resumes unread.

Diagram

sequenceDiagram
  participant User
  participant Client
  participant API
  participant Thread
  User->>Client: Mark message unread
  Client->>Client: Hold unread for this visit
  Client->>API: Queue markUnread with messageId
  API->>API: Check message belongs to thread
  API->>Thread: Set unread and cursor to message seq minus one
  User->>Client: Reopen chat
  Client->>API: Mark read
  API-->>Client: Return saved cursor
  Client->>Client: Show New above first reply after cursor
Loading

Reviews (4) · Last reviewed commit: "feat: mark a message as unread from its ..." · Reviewed by Greptile

Comment thread apps/web/src/pages/Shell.tsx Outdated
Comment thread apps/mobile/app/thread.tsx Outdated
Comment thread apps/api/src/thread-target.ts
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Playwright screenshots

Feature frames for this PR:

Open screenshot gallery · Dashboard · CI run

Updated for commit ddba5cb.

@ald3b
ald3b force-pushed the feat/unread-mark-message branch from 16a838b to e2848dd Compare October 23, 2026 07:43
Comment thread apps/mobile/app/thread.tsx Outdated
@ald3b
ald3b force-pushed the feat/unread-mark-message branch from e2848dd to 77e700e Compare October 23, 2026 07:58
Comment thread apps/web/src/pages/Shell.tsx Outdated
Marking a whole chat unread brings its dot back but not the place to
resume. A message's menu now has "Mark as unread", right after Reply: the
chat's dot returns, it stays unread for the rest of the visit, and the next
open puts the New divider above the first reply from that message on. The
user's own messages offer it too; live progress and subagent rows don't.

threads/markUnread takes an optional messageId and moves the read cursor
to just before that message. Mobile offers it in the native context menu,
the Android sheet and the VoiceOver actions; web in the message's More
menu, which now sizes to its content.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ald3b
ald3b force-pushed the feat/unread-mark-message branch from 77e700e to ddba5cb Compare October 23, 2026 08:12
@elie222
elie222 merged commit f6a1ef2 into elie222:main Oct 23, 2026
26 of 27 checks passed
tyleruenaka added a commit to tyleruenaka/rakazo that referenced this pull request Oct 24, 2026
Ported from upstream f6a1ef2 (elie222#1933). Any sent message, your own
included, can be marked unread: on the web from the hover More menu, on the
phone from the long-press menu. The chat turns unread in the sidebar and the
next open shows New above the first reply from that message.

The server takes an optional message id on threads.markUnread and moves the
chat's read position to just before it. A group marked unread by hand stays
unread until it is opened again, like a bot chat already did.

Fitted to the fork: the item sits in Tyler's More menu above Copy with the
same icon size, and in his phone message sheet right under Reply. Live
progress and subagent rows don't offer it.
tyleruenaka added a commit to tyleruenaka/rakazo that referenced this pull request Oct 24, 2026
)

* feat: show a New divider above the replies you haven't seen

Ported from upstream b5325c4 (elie222#1789). Opening a chat that had unread
replies puts a New line above the first reply you hadn't seen, on web and
on the phone. Replies you watch arrive live never get one.

The server keeps a read position per chat (new lastReadSeq column; existing
chats start as read up to their latest message, so no divider shows for old
history). threads.markRead now answers with where the user had read up to.

Fitted to the fork: the web transcript lives in Shell.tsx here, so the
divider is wired there; the divider uses the time separator's size and
weight instead of upstream's small caps; on the phone it breaks a run of
joined bubbles the way a time separator does. Tyler's click-to-dictate
voice test is kept as is.

* feat: mark a message as unread from its menu

Ported from upstream f6a1ef2 (elie222#1933). Any sent message, your own
included, can be marked unread: on the web from the hover More menu, on the
phone from the long-press menu. The chat turns unread in the sidebar and the
next open shows New above the first reply from that message.

The server takes an optional message id on threads.markUnread and moves the
chat's read position to just before it. A group marked unread by hand stays
unread until it is opened again, like a bot chat already did.

Fitted to the fork: the item sits in Tyler's More menu above Copy with the
same icon size, and in his phone message sheet right under Reply. Live
progress and subagent rows don't offer it.

* fix: show the New divider above grouped bot-to-bot rows too

The fork folds back-to-back bot-to-bot markers into one row, which upstream
doesn't have. When the first unread message sat inside that row, no divider
was drawn at all. Draw it above the row on web and phone, like a voice call.

* test: cover the New divider inside a grouped bot-to-bot row; make the read cursor migration safe to re-run

- A browser test opens a chat whose first unread message is the second one in a
  grouped bot-to-bot row and checks New sits above the row. It fails without the
  previous commit's fix. A core test checks the same thing for the shared logic
  that web and phone both use.
- The migration now adds the column only if it's missing and only fills in chats
  still at the default, so running it again never overwrites a read position.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants