Skip to content

feat: count unread thread messages - #977

Open
AchoArnold wants to merge 15 commits into
mainfrom
feat/unread-message-count
Open

feat: count unread thread messages#977
AchoArnold wants to merge 15 commits into
mainfrom
feat/unread-message-count

Conversation

@AchoArnold

Copy link
Copy Markdown
Member

Summary

  • replace binary message-thread is_read state with a server-owned unread_count
  • count received SMS and missed calls idempotently with retry-safe ledger/deletion markers
  • reset counts through the existing thread update endpoint and show numeric 99+ badges in the web UI
  • migrate existing unread threads to count 1 and publish regenerated Swagger/TypeScript contracts

Concurrency and migration safety

  • serialize activity, reset, deletion, and create-conflict decisions under row locks
  • use CockroachDB retry-aware transactions
  • prevent duplicate/replayed and deleted events from restoring counts or thread previews
  • enforce one thread per user/owner/contact and fail migration explicitly when legacy duplicates require manual resolution

Validation

  • cd api && go test -count=1 ./...
  • cd web && pnpm lint && pnpm run generate
  • cd tests && go test -count=1 -run '^$' ./...
  • final whole-branch review: merge-ready with no findings

Docker is unavailable in the local environment, so the live integration stack test is left to CI.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR replaces message-thread read booleans with server-owned unread counts and adds retry-safe ledgers for received, deleted, and replayed activity.

  • Migrates existing unread threads and adds a unique conversation identity.
  • Serializes count increments, resets, deletion handling, and create conflicts through repository transactions.
  • Exposes numeric unread counts through the API and displays capped badges in the web UI.
  • Adds generated contracts and broad migration, repository, service, listener, and request validation coverage.

Confidence Score: 4/5

The 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

Filename Overview
api/pkg/repositories/gorm_message_thread_repository.go Adds transactional unread ledgers, row locking, retry handling, reset, deletion, and create-conflict reconciliation; no publishable repository defect remained.
api/pkg/migrations/message_thread_unread_count.go Migrates the schema, backfills legacy unread state, rejects duplicate conversations, and creates the uniqueness constraint.
web/app/pages/threads/[id]/index.vue Resets the currently open thread for every user-wide inbound event, even when the event belongs to another conversation.
web/app/stores/threads.ts Adds the unread-count reset request and local thread replacement used by the affected page event handlers.
web/app/components/MessageThread.vue Replaces the binary unread indicator with a numeric badge capped at 99+.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "fix(api): preserve deleted message marke..." | Re-trigger Greptile

Comment thread web/app/pages/threads/[id]/index.vue Outdated
@codacy-production

codacy-production Bot commented Aug 23, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 66 minor

Alerts:
⚠ 66 issues (≤ 0 issues of at least minor severity)

Results:
66 new issues

Category Results
CodeStyle 66 minor

View in Codacy

🟢 Metrics 110 complexity · 111 duplication

Metric Results
Complexity 110
Duplication 111

View in Codacy

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.

@AchoArnold
AchoArnold force-pushed the feat/unread-message-count branch from a09ef6d to 8408915 Compare August 26, 2026 07:31
AchoArnold and others added 12 commits August 28, 2026 08:11
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/pkg/services/message_thread_service_test.go
Comment thread api/pkg/handlers/message_thread_handler_test.go
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
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