Skip to content

feat: count unread thread messages - #977

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

feat: count unread thread messages#977
AchoArnold wants to merge 12 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.

AchoArnold and others added 12 commits August 21, 2026 12:41
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
@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 on lines 244 to 247
webhookChannel.bind('message.phone.received', () => {
if (!loadingMessages.value) {
void markCurrentThreadRead(true)
void resetCurrentThreadUnreadCount(true)
loadMessages(false, false)

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.

P1 Inbound events reset wrong thread

When an SMS or missed-call event for conversation B arrives while conversation A is open, this user-wide handler clears A's unread count without checking the event's conversation, causing A to be marked read while B's badge remains stale until another reload.

@codacy-production

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 261 complexity · 138 duplication

Metric Results
Complexity 261
Duplication 138

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.

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.

1 participant