feat: count unread thread messages - #977
Conversation
Greptile SummaryThe PR replaces message-thread read booleans with server-owned unread counts and adds retry-safe ledgers for received, deleted, and replayed activity.
Confidence Score: 4/5The PR should not merge until user-wide inbound events stop clearing the unread count of an unrelated currently open conversation. The backend sends received and missed-call notifications on a shared user channel without conversation identity, while the page handles every notification by persisting unread_count=0 for the current route's thread. Files Needing Attention: web/app/pages/threads/[id]/index.vue, api/pkg/listeners/websocket_listener.go Important Files Changed
Sequence DiagramsequenceDiagram
participant B as Backend event
participant P as User Pusher channel
participant A as Open thread A page
participant API as Thread API
B->>P: message.phone.received for thread B (event ID only)
P->>A: user-wide event
A->>API: "PUT /message-threads/A {unread_count: 0}"
API-->>A: thread A marked read
Note over A,API: Thread B's badge is not refreshed
Reviews (1): Last reviewed commit: "fix(api): preserve deleted message marke..." | Re-trigger Greptile |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 66 minor |
🟢 Metrics 110 complexity · 111 duplication
Metric Results Complexity 110 Duplication 111
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
a09ef6d to
8408915
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce unread_count and unread-item ledger schema plus an idempotent startup migration that backfills legacy unread threads before dropping message_threads.is_read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Retain unread tombstones, generate read watermarks under lock, and retry all counter transactions on CockroachDB serialization failures. Refuse duplicate conversations before destructive migration, then create the unique identity needed for concurrent first-message fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 332cfd7b-8a60-4e28-9ea8-61854aa712c3
8408915 to
33fe141
Compare
There was a problem hiding this comment.
Pull request overview
This PR replaces the legacy binary is_read message-thread state with a server-owned unread_count, backed by an unread-item ledger and deletion markers in the API, and updates the web UI + integration coverage to display/reset numeric unread badges (with 99+ behavior) via the existing thread update endpoint.
Changes:
- API: Introduce
unread_count+ ledger/tombstones, update repository/service/listener flows for idempotent counting/reset/deletion, and add an idempotent migration (including unique conversation index preflight). - Contracts/UI: Regenerate Swagger + web types; update web store/components/pages to reset counts and render numeric badges.
- Tests/Docs: Extend integration coverage for increment/reset/decrement behavior; add design + implementation plan documents.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| web/shared/types/api.ts | Regenerated API types: remove is_read, add unread_count + reset-only constraint. |
| web/app/stores/threads.ts | Rename mark-read action to reset unread count and send { unread_count: 0 }. |
| web/app/pages/threads/[id]/index.vue | Update thread open/realtime flows to reset unread count instead of mark-read. |
| web/app/components/MessageThread.vue | Render numeric unread badge (99+) and bold styling based on unread_count. |
| tests/README.md | Update E2E coverage description to unread-count semantics. |
| tests/read_receipts_test.go | Update integration assertions for increment/reset/decrement and delete behavior. |
| docs/superpowers/specs/2026-08-21-unread-message-count-design.md | Add design spec for unread-count + ledger approach. |
| docs/superpowers/plans/2026-08-21-unread-message-count.md | Add detailed implementation plan for unread-count feature. |
| docs/superpowers/plans/2026-08-21-unread-count-concurrency-fixes.md | Add follow-up plan for Cockroach retries, tombstones, conflict handling, and migration hardening. |
| api/pkg/entities/message_thread.go | Replace IsRead with UnreadCount in the public entity. |
| api/pkg/entities/message_thread_unread_item.go | Add unread ledger entity with Counted tombstone state. |
| api/pkg/entities/message_thread_deleted_item.go | Add deleted-message marker entity to block replay/restoration. |
| api/pkg/entities/message_thread_test.go | Update entity reflection tests for unread fields/ledger/deleted marker. |
| api/pkg/migrations/message_thread_unread_count.go | Add idempotent migration: schema + legacy backfill/drop + unique conversation index preflight. |
| api/pkg/migrations/message_thread_unread_count_test.go | Add migration tests for idempotency, ordering, duplicates, and schema failures. |
| api/pkg/repositories/message_thread_repository.go | Update repository contracts for unread intent, reset params, and delete params. |
| api/pkg/repositories/gorm_message_thread_repository.go | Implement retry-safe (Cockroach) locked transactions for count/reset/delete + replay guards. |
| api/pkg/repositories/gorm_message_thread_repository_test.go | Expand repository tests for retry, locking/order, idempotency, deletion replay, and reset semantics. |
| api/pkg/services/message_thread_service.go | Route CountAsUnread, store params, reset-only status updates, and delegate delete decisions to repository. |
| api/pkg/services/message_thread_service_test.go | Update service tests for new contracts, unarchive behavior, and delete delegation. |
| api/pkg/listeners/read_receipts_test_helpers_test.go | Update listener test repository stubs for new repository interfaces. |
| api/pkg/listeners/message_thread_listener.go | Mark inbound SMS/missed calls as CountAsUnread=true. |
| api/pkg/listeners/message_thread_listener_test.go | Update listener tests to assert unread intent + delete delegation payloads. |
| api/pkg/requests/message_thread_update_request.go | Replace is_read with unread_count (min/max 0) and map to service params. |
| api/pkg/requests/message_thread_update_request_test.go | Add JSON pointer semantics + Swagger tag tests for exactly-zero constraint. |
| api/pkg/validators/message_thread_handler_validator.go | Require is_archived or unread_count, and reject nonzero counts. |
| api/pkg/validators/message_thread_handler_validator_test.go | Add validator tests for reset/invalid values and combined payloads. |
| api/pkg/handlers/message_thread_handler_test.go | Update handler tests for unread reset and legacy is_read rejection. |
| api/pkg/di/container.go | Wire new migration into startup instead of direct AutoMigrate for threads. |
| api/docs/swagger.yaml | Regenerated Swagger: remove is_read, add unread_count + min/max 0 on update. |
| api/docs/swagger.json | Regenerated Swagger JSON reflecting unread-count schema and constraints. |
| api/docs/docs.go | Regenerated embedded Swagger template reflecting unread-count schema and constraints. |
Files not reviewed (1)
- api/docs/docs.go: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Resolve inbound events to their authenticated message before clearing unread state so activity from another conversation cannot mark the open thread as read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b0e509-8bcb-4ace-82f4-3e64f6fcc0a6
Track unread state only on message_threads and use direct conditional updates without ledgers, row locks, or retry transactions. Migration removes the obsolete read watermark and unread/deleted item tables while preserving the legacy is_read backfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b0e509-8bcb-4ace-82f4-3e64f6fcc0a6
Store conversations with a single upsert and update thread activity with one conditional UPDATE RETURNING query. Unread counts increment or reset directly without auxiliary state or locking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b0e509-8bcb-4ace-82f4-3e64f6fcc0a6
Summary
is_readstate with a server-ownedunread_count99+badges in the web UI1and publish regenerated Swagger/TypeScript contractsConcurrency and migration safety
Validation
cd api && go test -count=1 ./...cd web && pnpm lint && pnpm run generatecd tests && go test -count=1 -run '^$' ./...Docker is unavailable in the local environment, so the live integration stack test is left to CI.