Skip to content

Conversation

@laevandus
Copy link
Contributor

@laevandus laevandus commented Nov 28, 2025

🔗 Issue Links

Fixes: IOS-1277

🎯 Goal

Mark messages as unread using timestamp

📝 Summary

  • Add new public methods where the from argument is Date
    • ChatChannelController.markUnread(from:completion:)
    • Chat.markUnread(from:)
  • Internally use MarkUnreadCriteria enum for managing the difference of message id and message timestamp in existing methods
  • Add demo app command for triggering mark as unread using a timestamp
  • Fix ChatChannelVC not updating the unread message separator (existing mark unread using a message id explicitly calls the update separator method)

🛠 Implementation

Use the existing mark unread endpoint, just that we pass message_timestamp instead of message_id. Backend can throw errors when passing in random timestamps and there are no messages to match against.

🎨 Showcase

Demo
img

🧪 Manual Testing Notes

  1. Open a channel
  2. Use channel debug menu to mark channel unread using a timestamp, it just asks number of days (basic)

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

Summary by CodeRabbit

  • New Features

    • Mark channels as unread from a timestamp (in addition to by-message); APIs accept unified criteria/payload.
  • UI

    • Demo app: "Mark channel unread with timestamp" action added.
  • Bug Fixes

    • Unread-related UI now refreshes on notification-based unread events.
  • Tests

    • Expanded tests covering timestamp-based unread scenarios and payload variations.

✏️ Tip: You can customize this high-level summary in your review settings.

@laevandus laevandus requested a review from a team as a code owner November 28, 2025 09:13
@laevandus laevandus added ✅ Feature An issue or PR related to a feature 🤞 Ready For QA A PR that is Ready for QA labels Nov 28, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

Walkthrough

Adds timestamp-based "mark unread" support: new MarkUnreadCriteria/MarkUnreadPayload, endpoint now accepts a payload, public APIs to mark unread by Date, database/message lookup updated for timestamp criteria, plumbing through repositories/controllers/workers, UI event handling, tests and mocks updated, and project "Profile" build configs added.

Changes

Cohort / File(s) Summary
Public API & Controllers
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift, Sources/StreamChat/StateLayer/Chat.swift
Added markUnread(from: Date) APIs; existing message-id variants now wrap ids as .messageId(...) when forwarded.
API Payloads & Endpoints
Sources/StreamChat/APIClient/Endpoints/Payloads/MarkUnreadPayload.swift, Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift, Tests/.../APIClient/Endpoints/ChannelEndpoints_Tests.swift
Added MarkUnreadCriteria and MarkUnreadPayload; endpoint signature changed to markUnread(cid:payload:); tests updated for messageId and timestamp payloads.
Database: message lookup & unread logic
Sources/StreamChat/Database/DTOs/MessageDTO.swift, Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift, Sources/StreamChat/Database/DatabaseSession.swift
markChannelAsUnread now accepts MarkUnreadCriteria; added loadMessage(beforeOrEqual:...); counting and lookup updated to support message-id or timestamp criteria and optional exclusion of the resolved message.
Repositories & Message fetching
Sources/StreamChat/Repositories/ChannelRepository.swift, Sources/StreamChat/Repositories/MessageRepository.swift
Repository signatures accept MarkUnreadCriteria; endpoint payloads built from criteria; message lookup branches on .messageId vs .messageTimestamp.
Workers & Read-state plumbing
Sources/StreamChat/Workers/ChannelUpdater.swift, Sources/StreamChat/Workers/ReadStateHandler.swift
Propagated MarkUnreadCriteria through ChannelUpdater and ReadStateHandler; async/sync variants updated accordingly.
WebSocket Middleware & UI
Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift, Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
Middleware now wraps ids as .messageId(...); ChatChannelVC handles NotificationMarkUnreadEvent to refresh unread UI when applicable.
Demo App
DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Added UI action to mark a channel unread by timestamp (prompts for days offset and calls controller.markUnread(from:)).
Mocks
TestTools/.../Database/DatabaseSession_Mock.swift, .../Repositories/ChannelRepository_Mock.swift, .../Workers/ChannelUpdater_Mock.swift
Updated mocks and stored test state to use MarkUnreadCriteria instead of raw MessageId; signatures adjusted.
Tests
multiple under Tests/... (Controllers, Database DTOs, Repositories, Workers, StateLayer)
Updated tests to wrap ids in .messageId(...), added timestamp-based test cases, adjusted expectations and helper calls.
Build Configuration
StreamChat.xcodeproj/project.pbxproj
Added "Profile" build configurations across project/target configuration lists.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant ChatChannelController
    participant ReadStateHandler
    participant MessageRepository
    participant ChannelUpdater
    participant ChannelRepository
    participant APIClient
    participant Database

    Client->>ChatChannelController: markUnread(from: Date)
    ChatChannelController->>ReadStateHandler: markUnread(from: .messageTimestamp(date), in: channel)
    ReadStateHandler->>MessageRepository: getMessage(before: .messageTimestamp(date))
    MessageRepository->>Database: loadMessage(beforeOrEqual: timestamp)
    Database-->>MessageRepository: MessageDTO? (found / nil)
    MessageRepository-->>ReadStateHandler: Result<MessageId?, Error>
    ReadStateHandler->>ChannelUpdater: markUnread(from: .messageTimestamp(date))
    ChannelUpdater->>ChannelRepository: markUnread(for: cid, from: .messageTimestamp(date))
    ChannelRepository->>APIClient: POST /channels/{type}/{id}/unread (body: MarkUnreadPayload)
    APIClient-->>ChannelRepository: EmptyResponse
    ChannelRepository->>Database: markChannelAsUnread(for: cid, from: .messageTimestamp)
    Database-->>ChannelRepository: updated ChatChannel
    ChannelRepository-->>ChannelUpdater: Result<ChatChannel, Error>
    ChannelUpdater-->>ReadStateHandler: Result<ChatChannel, Error>
    ReadStateHandler-->>ChatChannelController: Result<ChatChannel, Error>
    ChatChannelController-->>Client: completion(Result<ChatChannel, Error>)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay extra attention to:
    • MarkUnreadPayload.encode(to:) — keys, encoding branching and order.
    • MessageDTO.loadMessage(beforeOrEqual:...) — predicate for createdAt <= timestamp, visibility flags, sort and fetchLimit.
    • ChannelReadDTO.markChannelAsUnread(...) — message resolution, excludesMessageId behavior, edge cases when message not found.
    • Repository → API payload construction and endpoint change in ChannelEndpoints.
    • Tests exercising timestamp paths (ChannelReadDTO_Tests, MessageRepository_Tests, Chat_Tests).

Suggested labels

🌐 SDK: StreamChat (LLC)

Suggested reviewers

  • martinmitrevski

Poem

🐰 I hopped through code both near and deep,

Timestamps wake messages once put to sleep.
IDs or dates — unread finds its cue,
Tests and mocks hopped in to see it through.
A tiny carrot for CI to chew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Mark messages as unread using timestamp' is clear and specific, accurately summarizing the main change introduced in the PR.
Linked Issues check ✅ Passed All objectives from IOS-1277 are met: timestamp-based mark-unread APIs added to ChatChannelController and Chat; MarkUnreadCriteria enum introduced; backend endpoint reused with message_timestamp; UI updated in ChatChannelVC; unit tests added.
Out of Scope Changes check ✅ Passed All changes are directly related to IOS-1277 objectives. Minor additions include: Profile build configurations (infrastructure), demo app command (in-scope for testing), EventMiddleware updates (supporting new criteria), and mock/test updates (required for testing).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/mark-unread-with-timestamp

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c33202a and 96dc2c9.

📒 Files selected for processing (3)
  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift (6 hunks)
  • Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift (4 hunks)
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift
  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)

1303-1377: markUnread controller APIs are correct; tighten timestamp doc wording

  • The message-id overload now correctly wraps messageId in .messageId(...) before delegating to readStateHandler, reusing the existing guards and callback pattern.
  • The new markUnread(from timestamp: Date, ...) overload reuses the same guards and delegates via .messageTimestamp(timestamp), which aligns with ReadStateHandler/ChannelReadDTO’s use of MarkUnreadCriteria.

The doc comment for the timestamp overload says it:

finds the first message with a creation timestamp greater than the provided timestamp

but the DB layer actually:

  • resolves a message at or before the timestamp (via loadMessage(beforeOrEqual:timestamp, ...)), and
  • treats messages after that resolved message as unread, doing nothing if no such message can be found.

To avoid confusion in edge cases (e.g. timestamp earlier than the oldest message), consider rephrasing along the lines of:

This method resolves the last message with a creation timestamp less than or equal to the provided timestamp, and marks all later messages as unread. If no such message can be found, the operation completes without error and leaves the unread state unchanged.

Also, since this is a new public API on ChatChannelController, ensure there is a corresponding CHANGELOG entry and migration note.

Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (1)

4135-4328: Mark-unread tests align with new criteria-based flow; fix misleading comment and consider timestamp coverage

The updated mark‑unread tests correctly drive controller.markUnread(from: MessageId) and assert that ChannelUpdater_Mock receives markUnread_criteria == .messageId(messageId), which matches the new MarkUnreadCriteria‑based pipeline. The various error/no‑error branches (missing channel, read events disabled, marking read in progress, missing current user) remain consistent with ReadStateHandler.markUnread.

One small issue: in test_markUnread_whenIsNotMarkingAsRead_andCurrentUserIdIsPresent_whenThereAreNoMessages_whenUpdaterSucceeds, the comment saying “we fallback to the passed messageId as lastReadMessageId” contradicts the assertion XCTAssertNil(updater.markUnread_lastReadMessageId). Please update or remove the comment to reflect actual behaviour.

If there isn’t already a separate test exercising ChatChannelController.markUnread(from timestamp: Date) (possibly in another suite), it would be good to add one for parity with the message‑ID path.

🧹 Nitpick comments (7)
Sources/StreamChat/StateLayer/Chat.swift (1)

937-950: Async Chat.markUnread overloads are wired correctly; add release notes

Both markUnread(from messageId:) and the new markUnread(from timestamp:) correctly delegate via MarkUnreadCriteria to readStateHandler and reuse the existing ChannelNotCreatedYet error path. This keeps the async state-layer API consistent with the controller layer.

Given this is a new public API surface in StreamChat, make sure the CHANGELOG and migration docs call out:

  • the new Chat.markUnread(from: Date) API, and
  • the semantic equivalence between the message-id and timestamp variants.
Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift (1)

425-630: Timestamp-based partial unread tests cover main paths; consider one more edge case

The new messageTimestamp tests for:

  • messages present (expecting correct lastReadAt, unreadMessageCount, and lastReadMessageId), and
  • explicit lastReadAt / unreadMessagesCount overrides,

mirror the existing message-id tests and validate that timestamp criteria behave identically at the DTO level.

If you want to harden behavior further, you could add a test where the timestamp is before the earliest message in the channel, asserting that the unread state stays unchanged (matching the current guard let message = findMessageDTO() early return).

Sources/StreamChat/Repositories/MessageRepository.swift (1)

341-371: MarkUnreadCriteria handling in repository looks correct; duplication is acceptable but could be reduced

The new getMessage(before unreadCriteria: MarkUnreadCriteria, in:cid, ...) correctly routes .messageId to the existing loadMessage(before: ...) path and .messageTimestamp to loadMessage(beforeOrEqual: ...), reusing shared config and preserving error handling. If you touch this again, consider extracting a tiny helper to avoid repeating the fetch+.id pattern in both switch cases, but it’s fine as-is.

Sources/StreamChat/Database/DTOs/MessageDTO.swift (1)

610-625: Timestamp-based loadMessage looks correct; consider aligning with other fetch helpers

The predicate and sort (latest channel message with createdAt <= timestamp) match the intended semantics and mirror the existing loadMessage(before:id:cid:...) path. For consistency with other fetch helpers, you might consider also calling MessageDTO.applyPrefetchingState(to:) and/or reusing the load(request,context:) helper, but this is optional given the single-object fetch.

Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (1)

141-175: Unread-by-timestamp resolution looks correct; consider unifying client-config handling

The refactor cleanly supports both .messageId and .messageTimestamp, and the timestamp branch correctly reuses MessageDTO.loadMessage(beforeOrEqual:cid:deletedMessagesVisibility:shouldShowShadowedMessages:context:) so unread is anchored to the latest visible message at/before the timestamp.

One small consistency tweak to consider: for the timestamp branch you currently fall back to .alwaysVisible and false when chatClientConfig is nil, whereas the existing loadMessage(before:id:cid:) helper guards on a non-nil config and returns nil. Aligning these behaviors (e.g., early-return when config is missing) would make the criteria paths behave more uniformly.

Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift (1)

112-137: Repository markUnread tests match new payload shape; consider adding a timestamp-criteria case

The updated tests correctly assert that ChannelRepository.markUnread passes .messageId(messageId) via MarkUnreadPayload(criteria:userId:) and hits the expected .markUnread(cid:payload:) endpoint.

Since the feature now also supports .messageTimestamp, it would be useful to add a companion test that calls markUnread(from: .messageTimestamp(...)) and asserts the encoded payload’s criteria is .messageTimestamp(...), to lock in both branches of the new enum.

Also applies to: 139-160

Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift (1)

1727-1739: Updater markUnread tests validate id-based criteria; add a timestamp variant for full coverage

These tests correctly ensure that ChannelUpdater.markUnread forwards .messageId(messageId) and lastReadMessageId to ChannelRepository_Mock and that success/error results are propagated.

To fully exercise the new MarkUnreadCriteria surface, consider an additional test that calls markUnread(from: .messageTimestamp(timestamp)) and asserts channelRepository.markUnreadCriteria is .messageTimestamp(timestamp) and the completion semantics remain identical. This would complement the existing id-based checks.

Also applies to: 1741-1768

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3f1c281 and 205b37e.

📒 Files selected for processing (25)
  • CHANGELOG.md (1 hunks)
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MarkUnreadPayload.swift (1 hunks)
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1 hunks)
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (1 hunks)
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift (1 hunks)
  • Sources/StreamChat/Database/DatabaseSession.swift (1 hunks)
  • Sources/StreamChat/Repositories/ChannelRepository.swift (2 hunks)
  • Sources/StreamChat/Repositories/MessageRepository.swift (2 hunks)
  • Sources/StreamChat/StateLayer/Chat.swift (1 hunks)
  • Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift (1 hunks)
  • Sources/StreamChat/Workers/ChannelUpdater.swift (1 hunks)
  • Sources/StreamChat/Workers/ReadStateHandler.swift (3 hunks)
  • Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift (1 hunks)
  • StreamChat.xcodeproj/project.pbxproj (5 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (1 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (2 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (3 hunks)
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift (2 hunks)
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (7 hunks)
  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift (6 hunks)
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift (3 hunks)
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

**/*.swift: Write Swift code compatible with iOS deployment targets specified in Package.swift and podspec files; do not lower deployment targets without approval
Use SwiftLint with strict mode and respect .swiftlint.yml rules; justify and scope any exceptions rather than suppressing rules broadly
Run SwiftFormat for code formatting and respect repository-specific style conventions
Never commit API keys or customer data; use obvious placeholders (e.g., YOUR_STREAM_KEY) in example code
Follow the project's zero-warnings policy: fix new warnings and avoid introducing any

Files:

  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Sources/StreamChat/Workers/ChannelUpdater.swift
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
  • Sources/StreamChat/Repositories/ChannelRepository.swift
  • Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MarkUnreadPayload.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift
  • Sources/StreamChat/Repositories/MessageRepository.swift
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift
  • Sources/StreamChat/Workers/ReadStateHandler.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift
  • Sources/StreamChat/StateLayer/Chat.swift
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo

Files:

  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
Sources/StreamChat/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Files:

  • Sources/StreamChat/Workers/ChannelUpdater.swift
  • Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
  • Sources/StreamChat/Repositories/ChannelRepository.swift
  • Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MarkUnreadPayload.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • Sources/StreamChat/Repositories/MessageRepository.swift
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift
  • Sources/StreamChat/Workers/ReadStateHandler.swift
  • Sources/StreamChat/StateLayer/Chat.swift
CHANGELOG*

📄 CodeRabbit inference engine (AGENTS.md)

Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI

Files:

  • CHANGELOG.md
Sources/StreamChatUI/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Ensure public API changes in StreamChatUI include inline documentation and migration notes

Files:

  • Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to CHANGELOG* : Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Tests/**/*.swift : Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo

Applied to files:

  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • StreamChat.xcodeproj/project.pbxproj
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Applied to files:

  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Sources/StreamChat/Workers/ChannelUpdater.swift
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
  • Sources/StreamChat/Repositories/ChannelRepository.swift
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
  • CHANGELOG.md
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • StreamChat.xcodeproj/project.pbxproj
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift
  • Sources/StreamChat/Repositories/MessageRepository.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift
  • Sources/StreamChat/StateLayer/Chat.swift
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Maintain high test coverage when changing code in the Stream iOS Chat SDK

Applied to files:

  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes

Applied to files:

  • Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift
  • Sources/StreamChat/Workers/ChannelUpdater.swift
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
  • Sources/StreamChat/Repositories/ChannelRepository.swift
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
  • CHANGELOG.md
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • StreamChat.xcodeproj/project.pbxproj
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
  • Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift
  • Sources/StreamChat/Repositories/MessageRepository.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift
  • Sources/StreamChat/StateLayer/Chat.swift
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to CHANGELOG* : Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI

Applied to files:

  • Sources/StreamChat/Workers/ChannelUpdater.swift
  • Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
  • Sources/StreamChat/Repositories/ChannelRepository.swift
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
  • CHANGELOG.md
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
  • Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift
  • Sources/StreamChat/StateLayer/Chat.swift
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Prioritize backwards compatibility and API stability when changing code in the Stream iOS Chat SDK

Applied to files:

  • CHANGELOG.md
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
🧬 Code graph analysis (20)
Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift (2)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (2)
  • markChannelAsUnread (138-175)
  • markChannelAsUnread (177-181)
TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (5)
  • markChannelAsUnread (358-361)
  • markChannelAsUnread (363-372)
  • message (291-293)
  • user (100-102)
  • channel (403-405)
Sources/StreamChat/Workers/ChannelUpdater.swift (6)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (3)
  • markUnread (1308-1337)
  • markUnread (1348-1377)
  • lastReadMessageId (2264-2276)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)
  • markUnread (39-49)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (1)
  • markUnread (515-522)
Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (1)
Sources/StreamChat/Repositories/MessageRepository.swift (2)
  • getMessage (304-338)
  • getMessage (341-376)
Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift (1)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)
  • messageId (2152-2154)
Sources/StreamChat/Repositories/ChannelRepository.swift (6)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (3)
  • markUnread (1308-1337)
  • markUnread (1348-1377)
  • lastReadMessageId (2264-2276)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)
  • markUnread (39-49)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (3)
TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (2)
  • loadChannelRead (374-376)
  • message (291-293)
Sources/StreamChat/Database/DTOs/MessageDTO.swift (5)
  • message (457-463)
  • message (1369-1369)
  • loadMessage (589-608)
  • loadMessage (610-625)
  • loadMessage (1587-1599)
Sources/StreamChat/Database/DataStore.swift (1)
  • message (68-70)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (5)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
Sources/StreamChat/APIClient/Endpoints/Payloads/MarkUnreadPayload.swift (3)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)
  • messageId (2152-2154)
TestTools/StreamChatTestMockServer/MockServer/MockServerAttributes.swift (1)
  • userId (200-202)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • encode (416-420)
Sources/StreamChat/Database/DatabaseSession.swift (2)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (2)
  • markChannelAsUnread (138-175)
  • markChannelAsUnread (177-181)
Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift (1)
  • markChannelAsUnread (114-131)
TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (2)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (2)
  • markChannelAsUnread (138-175)
  • markChannelAsUnread (177-181)
Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift (1)
  • markChannelAsUnread (114-131)
Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (4)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (3)
  • markUnread (1308-1337)
  • markUnread (1348-1377)
  • messageId (2152-2154)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift (2)
Sources/StreamChat/WebSocketClient/Events/EventPayload.swift (1)
  • event (208-210)
Sources/StreamChat/WebSocketClient/Events/EventType.swift (1)
  • event (186-267)
Sources/StreamChat/Repositories/MessageRepository.swift (1)
Sources/StreamChat/Database/DTOs/MessageDTO.swift (3)
  • loadMessage (589-608)
  • loadMessage (610-625)
  • loadMessage (1587-1599)
Sources/StreamChat/Database/DTOs/MessageDTO.swift (1)
Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift (1)
  • fetch (181-199)
Sources/StreamChat/Workers/ReadStateHandler.swift (3)
Sources/StreamChat/Repositories/MessageRepository.swift (2)
  • getMessage (304-338)
  • getMessage (341-376)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)
  • markUnread (39-49)
Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift (5)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (4)
  • markUnread (1308-1337)
  • markUnread (1348-1377)
  • messageId (2152-2154)
  • lastReadMessageId (2264-2276)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (4)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (1)
  • markUnread (515-522)
Sources/StreamChat/StateLayer/Chat.swift (2)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (3)
  • markUnread (1308-1337)
  • markUnread (1348-1377)
  • messageId (2152-2154)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift (3)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (4)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/StateLayer/Chat.swift (2)
  • markUnread (937-940)
  • markUnread (947-950)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)
  • markUnread (39-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Metrics
🔇 Additional comments (20)
StreamChat.xcodeproj/project.pbxproj (5)

321-322: PBXBuildFile entries for MarkUnreadPayload.swift are consistent across targets

Both build files (4FC7B3F0 and 4FC7B3F1) reference the same fileRef and are each consumed in separate Sources lists later on, which matches the usual multi-target wiring pattern. No structural issues here.


3365-3365: PBXFileReference for MarkUnreadPayload.swift is correctly defined

The new file reference uses the expected sourcecode.swift type and path, consistent with neighboring Swift files. This will let Xcode resolve the file cleanly.


5979-5983: Payloads group inclusion looks correct

MarkUnreadPayload.swift is added under the existing Payloads PBXGroup alongside other payload DTOs, which keeps project structure coherent.


11918-11918: Main target Sources phase wiring is correct

4FC7B3F1 /* MarkUnreadPayload.swift in Sources */ is added to a Sources list with other core SDK files, so the main target will compile the new payload as expected.


13094-13094: Additional target Sources phase wiring is correct

4FC7B3F0 /* MarkUnreadPayload.swift in Sources */ appears in another Sources list (likely a second target such as tests or a demo), which is the standard pattern for sharing the same file across targets.

Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (2)

422-448: LGTM! Test updated correctly for new API.

The existing test has been properly updated to use the new MarkUnreadCriteria.messageId wrapper while maintaining the same test logic.


450-478: Excellent test coverage for timestamp-based marking.

The new test properly validates the timestamp-based unread marking functionality with a clear scenario: using a timestamp between two messages to retrieve the correct message.

TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (1)

363-372: LGTM! Mock updated correctly.

The mock signature has been properly updated to use MarkUnreadCriteria and correctly forwards the parameter to the underlying session.

Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift (1)

114-131: LGTM! Correctly wraps messageId in criteria enum.

The call to markChannelAsUnread has been properly updated to wrap the messageId parameter in the new MarkUnreadCriteria.messageId case.

DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift (1)

466-483: LGTM! Demo action properly implements timestamp-based marking.

The new demo action correctly prompts for a day offset, computes the date, and calls the new markUnread(from: Date) API with appropriate error handling.

Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift (1)

601-604: LGTM! Fixes unread UI update for timestamp-based marking.

This correctly handles NotificationMarkUnreadEvent by updating all unread-related UI components when the channel is marked unread via timestamp. This ensures the unread message separator and other UI elements are refreshed, addressing the issue mentioned in the PR description.

TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)

17-49: LGTM! Mock repository updated correctly.

The mock has been properly updated to use MarkUnreadCriteria and stores the criteria for verification in tests.

Sources/StreamChat/Workers/ReadStateHandler.swift (1)

48-94: LGTM! ReadStateHandler consistently updated.

Both the completion-based and async variants of markUnread have been properly updated to accept and forward MarkUnreadCriteria through the call chain. The implementation correctly propagates the criteria to both messageRepository and channelUpdater.

Sources/StreamChat/Workers/ChannelUpdater.swift (1)

567-583: Updater–repository wiring for MarkUnreadCriteria looks good

The ChannelUpdater.markUnread signature and doc are correctly updated to take MarkUnreadCriteria and simply forward it to ChannelRepository.markUnread, preserving behavior while enabling timestamp criteria.

Sources/StreamChat/Repositories/ChannelRepository.swift (1)

61-103: Repository markUnread refactor is consistent with new payload/criteria types

The repository correctly:

  • accepts from unreadCriteria: MarkUnreadCriteria,
  • builds the MarkUnreadPayload for the endpoint, and
  • passes the same criteria into session.markChannelAsUnread.

Error propagation and the ChannelNotCreatedYet fallback remain intact.

Tests/StreamChatTests/Database/DTOs/ChannelReadDTO_Tests.swift (1)

316-424: Nice symmetry tests for partial unread when read/message are missing

The updated tests:

  • switch existing partial-unread cases to from: .messageId(...), and
  • add *_messageTimestamp variants for “no read” and “no message” scenarios,

which together validate that markChannelAsUnread becomes a no-op (while still going through a single write) when either the read row or the target message cannot be resolved, for both criteria types.

Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)

272-279: Endpoint.markUnread now correctly uses the typed MarkUnreadPayload

Switching markUnread to accept a MarkUnreadPayload and using it as the request body keeps the HTTP contract (path/method) intact while aligning the endpoint with the new criteria/payload model and improving type safety at call sites.

Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift (1)

452-494: Endpoint tests cover both message-id and timestamp mark-unread variants correctly

The new markUnread endpoint tests look good: they use MarkUnreadPayload with .messageId and .messageTimestamp criteria, assert full AnyEndpoint equality (path, method, requiresConnectionId, body), and confirm the expected channels/{type}/{id}/unread path. This gives solid coverage of the new payload-based API.

Sources/StreamChat/Database/DatabaseSession.swift (1)

399-408: Channel read protocol update is consistent with criteria-based API

Switching from to MarkUnreadCriteria and updating the docstring to mention id or timestamp keeps the protocol aligned with the new unread model; no issues here.

TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (1)

103-109: Mock wiring for criteria-based markUnread looks consistent

Tracking markUnread_criteria instead of a bare message id, resetting it in cleanUp(), and assigning it in the overridden markUnread matches the new production API and keeps the mock introspection aligned with test expectations.

Also applies to: 250-256, 515-522

Comment on lines +602 to +604
if let event = event as? NotificationMarkUnreadEvent, let channel = channelController.channel, event.cid == channelController.cid, !messages.isEmpty {
updateAllUnreadMessagesRelatedComponents(channel: channel)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The existing mark unread message action is implemented in ChatChannelVC and it explicitly calls this method in the completion handler. Since I added demo app debug command then this did not get called and unread separator did not appear.
Should I clean up the explicit call? At the moment I did not touch it.

case is MarkUnreadActionItem:
            dismiss(animated: true) { [weak self] in
                self?.channelController.markUnread(from: message.id) { result in
                    if case let .success(channel) = result {
                        self?.updateAllUnreadMessagesRelatedComponents(channel: channel)
                    }
                }
            }

Copy link
Contributor

Choose a reason for hiding this comment

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

That's ok imo, we have one sample implementation (customers can change/decide which one to use).

@github-actions
Copy link

github-actions bot commented Nov 28, 2025

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@Stream-SDK-Bot
Copy link
Collaborator

SDK Performance

target metric benchmark branch performance status
MessageList Hitches total duration 10 ms 3.34 ms 66.6% 🔼 🟢
Duration 2.6 s 2.56 s 1.54% 🔼 🟢
Hitch time ratio 4 ms per s 1.3 ms per s 67.5% 🔼 🟢
Frame rate 75 fps 78.47 fps 4.63% 🔼 🟢
Number of hitches 1 0.4 60.0% 🔼 🟢

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

Looks good, but the code coverage can be improved

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
Tests/StreamChatTests/StateLayer/Chat_Tests.swift (1)

1311-1347: Verify lastMessage selection in new timestamp-based markUnread test

The new test correctly exercises chat.markUnread(from: Date) and mirrors the ID-based variant, but lastMessage is initialized from messages.first (same as firstMessage), which is likely a copy-paste/semantic slip—messages.last would better represent the latest message and aligns with how lastMessageAt is set in the markRead test above. If this is unintentional, consider switching to messages.last here (and in the existing ID-based markUnread test) so the setup more realistically models "all messages read, then mark unread from the first message."

Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (1)

4330-4408: Timestamp-based markUnread tests are good; consider adding one assertion on the criteria wiring

The new markUnread(from: Date) tests nicely mirror the messageId variants for:

  • channel not created
  • read events disabled
  • marking-as-read in progress
  • no current user id

They validate surface behavior (error vs no error), but they don’t yet assert that the controller passes MarkUnreadCriteria.messageTimestamp(...) through to ChannelUpdater_Mock, or that lastReadMessageId is derived consistently with the messageId-based path.

If you want tighter coverage of the new timestamp path, consider adding a positive test similar to test_markUnread_whenIsNotMarkingAsRead_andCurrentUserIdIsPresent_whenThereAreOtherMessages_whenUpdaterSucceeds, but using a message’s createdAt as the timestamp and asserting:

  • updater.markUnread_criteria == .messageTimestamp(timestamp)
  • updater.markUnread_lastReadMessageId matches the message immediately preceding that timestamp

This would exercise the new enum case end-to-end without changing production code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fd9bbb3 and 095fb8a.

📒 Files selected for processing (2)
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (7 hunks)
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

**/*.swift: Write Swift code compatible with iOS deployment targets specified in Package.swift and podspec files; do not lower deployment targets without approval
Use SwiftLint with strict mode and respect .swiftlint.yml rules; justify and scope any exceptions rather than suppressing rules broadly
Run SwiftFormat for code formatting and respect repository-specific style conventions
Never commit API keys or customer data; use obvious placeholders (e.g., YOUR_STREAM_KEY) in example code
Follow the project's zero-warnings policy: fix new warnings and avoid introducing any

Files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo

Files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to CHANGELOG* : Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Tests/**/*.swift : Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo

Applied to files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes

Applied to files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Applied to files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Maintain high test coverage when changing code in the Stream iOS Chat SDK

Applied to files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
🔇 Additional comments (1)
Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (1)

4135-4328: Mark-unread-by-messageId tests correctly updated to new API and criteria wiring

The updated tests now call controller.markUnread(from: MessageId.unique) and validate updater.markUnread_criteria == .messageId(messageId) in the success paths. This keeps existing behavior intact while exercising the new MarkUnreadCriteria plumbing, and the scenarios (channel missing, feature disabled, current user present/absent, updater error/success) remain well covered. No issues here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)

1345-1383: Clarify markUnread(from timestamp:) behavior and tighten documentation

The wiring into readStateHandler.markUnread(from: .messageTimestamp(timestamp), in: channel) looks correct, but the doc comment is a bit too specific and may be misleading:

  • The comment states that “If no message is found after the timestamp, the operation completes without error but no messages are marked as unread.” In practice, the call ultimately goes through ChannelRepository.markUnread, which propagates backend errors directly to the caller. Given the PR objective notes that the backend may return errors when timestamps do not match any messages, this should be reflected here so integrators don’t assume a silent no-op.
  • The phrase “creation timestamp greater than to the provided timestamp” is grammatically off and also leaves ambiguity about whether the boundary is > vs >=. It would be safer to say something like “starting from the first message whose createdAt is equal to or later than the given timestamp” (or whatever matches the actual backend semantics).

I suggest updating the doc comment along these lines, for example:

-    /// This method finds the first message with a creation timestamp greater than to the provided timestamp,
-    /// and marks all messages from that point forward as unread. If no message is found after the timestamp,
-    /// the operation completes without error but no messages are marked as unread.
+    /// This method marks messages as unread starting from the first message whose `createdAt`
+    /// is at or after the provided timestamp. If the timestamp does not correspond to any
+    /// message in the channel, the backend may return an error, which is propagated via
+    /// the `completion` result.

Also, markUnread(from messageId:) and markUnread(from timestamp:) share identical guard/error logic; if this grows further, consider a small private helper that takes a MarkUnreadCriteria to avoid duplication, though this is optional.

Finally, since this is a new public API in Sources/StreamChat, please ensure it’s also covered in the module’s migration notes and CHANGELOG entry for this release.

🧹 Nitpick comments (1)
Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (1)

4330-4408: Consider adding a success-path assertion for timestamp-based MarkUnreadCriteria

The new markUnread(from: Date) tests nicely mirror the existing messageId flows for channel-not-created, read-events-disabled, and current-user/markRead combinations. To fully exercise the new timestamp-based API, consider adding at least one success-path test (similar to the messageId variants above) that:

  • Uses a fixed Date value (stored in a local timestamp variable), and
  • Asserts that env.channelUpdater?.markUnread_criteria (or the appropriate mock) is set to something like .messageTimestamp(timestamp) when the operation succeeds.

This would align coverage between ID- and timestamp-based criteria and more directly guard the new enum case wiring.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 095fb8a and a7f68db.

📒 Files selected for processing (7)
  • CHANGELOG.md (1 hunks)
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift (1 hunks)
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1 hunks)
  • Sources/StreamChat/StateLayer/Chat.swift (1 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (3 hunks)
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (7 hunks)
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
  • Sources/StreamChat/StateLayer/Chat.swift
  • CHANGELOG.md
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
🧰 Additional context used
📓 Path-based instructions (3)
**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

**/*.swift: Write Swift code compatible with iOS deployment targets specified in Package.swift and podspec files; do not lower deployment targets without approval
Use SwiftLint with strict mode and respect .swiftlint.yml rules; justify and scope any exceptions rather than suppressing rules broadly
Run SwiftFormat for code formatting and respect repository-specific style conventions
Never commit API keys or customer data; use obvious placeholders (e.g., YOUR_STREAM_KEY) in example code
Follow the project's zero-warnings policy: fix new warnings and avoid introducing any

Files:

  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
Sources/StreamChat/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Files:

  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo

Files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to CHANGELOG* : Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Tests/**/*.swift : Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Prioritize backwards compatibility and API stability when changing code in the Stream iOS Chat SDK
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Maintain high test coverage when changing code in the Stream iOS Chat SDK
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes

Applied to files:

  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Applied to files:

  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to CHANGELOG* : Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI

Applied to files:

  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Tests/**/*.swift : Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo

Applied to files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Maintain high test coverage when changing code in the Stream iOS Chat SDK

Applied to files:

  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Prioritize backwards compatibility and API stability when changing code in the Stream iOS Chat SDK

Applied to files:

  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
🧬 Code graph analysis (3)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (4)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (4)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (3)
  • markUnread (1314-1343)
  • markUnread (1354-1383)
  • messageId (2158-2160)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (1)
  • markUnread (525-532)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)
  • markUnread (39-49)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (5)
Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)
  • markUnread (272-280)
Sources/StreamChat/Repositories/ChannelRepository.swift (1)
  • markUnread (69-103)
Sources/StreamChat/Workers/ReadStateHandler.swift (2)
  • markUnread (48-80)
  • markUnread (82-94)
TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift (1)
  • markUnread (39-49)
Sources/StreamChat/Workers/ChannelUpdater.swift (1)
  • markUnread (570-584)
🔇 Additional comments (5)
TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (3)

105-111: Mark-unread mock now tracks full MarkUnreadCriteria

Using markUnread_criteria: MarkUnreadCriteria? instead of a bare message ID keeps the mock aligned with the new API and lets tests assert both ID- and timestamp-based flows cleanly.
Based on learnings, this helps maintain high test coverage around the new unread behavior.


258-263: Clean-up correctly resets markUnread_criteria

Resetting markUnread_criteria to nil alongside the other mark-unread fields avoids state leakage between tests and matches the existing cleanup pattern.


525-531: Updated markUnread override matches criteria-based API

The overridden markUnread now accepts from unreadCriteria: MarkUnreadCriteria and records it into markUnread_criteria, mirroring the production ChannelUpdater/ChannelRepository contract and ensuring tests can verify the exact criteria passed through. Completion handling remains consistent with other mock methods.

Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (1)

4135-4328: Mark-unread (messageId) tests correctly updated to use MarkUnreadCriteria

The updated calls to controller.markUnread(from: MessageId.unique) and the expectations on updater.markUnread_criteria == .messageId(messageId) are consistent with the new MarkUnreadCriteria API and keep existing behavior intact. No further changes needed here.

Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)

1335-1342: Mark-unread-by-messageId correctly delegates via MarkUnreadCriteria

Wrapping messageId in .messageId(messageId) when calling readStateHandler.markUnread cleanly aligns this API with the new MarkUnreadCriteria model and keeps behavior unchanged from the caller’s perspective. No issues here.

@testableapple testableapple added 🧪 QAing and removed 🤞 Ready For QA A PR that is Ready for QA labels Dec 1, 2025
@laevandus laevandus enabled auto-merge (squash) December 2, 2025 09:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a7f68db and c33202a.

📒 Files selected for processing (3)
  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (1 hunks)
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift (2 hunks)
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • Tests/StreamChatTests/StateLayer/Chat_Tests.swift
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift
🧰 Additional context used
📓 Path-based instructions (2)
**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

**/*.swift: Write Swift code compatible with iOS deployment targets specified in Package.swift and podspec files; do not lower deployment targets without approval
Use SwiftLint with strict mode and respect .swiftlint.yml rules; justify and scope any exceptions rather than suppressing rules broadly
Run SwiftFormat for code formatting and respect repository-specific style conventions
Never commit API keys or customer data; use obvious placeholders (e.g., YOUR_STREAM_KEY) in example code
Follow the project's zero-warnings policy: fix new warnings and avoid introducing any

Files:

  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
Sources/StreamChat/**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Files:

  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to CHANGELOG* : Update CHANGELOG entries for user-visible SDK changes in StreamChat and StreamChatUI
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Tests/**/*.swift : Add or extend tests in the matching module's Tests/ folder for changes to StreamChat or StreamChatUI code, covering core models, API surfaces, and view controller behaviors; use fakes/mocks from test helpers provided by the repo
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChatUI/**/*.swift : Ensure public API changes in StreamChatUI include inline documentation and migration notes

Applied to files:

  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
📚 Learning: 2025-11-25T12:17:18.459Z
Learnt from: CR
Repo: GetStream/stream-chat-swift PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T12:17:18.459Z
Learning: Applies to Sources/StreamChat/**/*.swift : Ensure public API changes in StreamChat and StreamChatUI include inline documentation and migration notes

Applied to files:

  • Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift
🧬 Code graph analysis (1)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (1)
Sources/StreamChat/Database/DTOs/MessageDTO.swift (6)
  • message (457-463)
  • message (1373-1373)
  • loadMessage (593-612)
  • loadMessage (614-629)
  • loadMessage (1591-1603)
  • countOtherUserMessages (558-578)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Test App and Frameworks
  • GitHub Check: Test LLC (Debug)
  • GitHub Check: Build LLC + UI (Xcode 15)
  • GitHub Check: Automated Code Review
  • GitHub Check: Metrics
🔇 Additional comments (3)
Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift (3)

141-141: LGTM! Signature change supports unified criteria handling.

The parameter change from messageId to unreadCriteria enables the method to handle both ID-based and timestamp-based unread marking, aligning with the PR's objective to introduce MarkUnreadCriteria.


157-158: The fallback defaults in lines 157-158 are correct and consistent with the codebase. Both .alwaysVisible for deletedMessagesVisibility and false for shouldShowShadowedMessages match the documented defaults in ChatClientConfig (lines 181 and 185) and are used identically in MessageRepository.swift (lines 349-350) for the same nil-coalescing pattern. These defaults are also consistently validated across test files. For timestamp-based unread marking, these values are appropriate: showing deleted messages allows the operation to find the correct message boundary, and filtering shadowed messages prevents incorrect unread marking.


169-178: The asymmetric counting behavior is intentional and correct. The git history shows commit c33202a5 titled "Exclude current message from unread count calculation," and the logic reflects the different semantics of the two criteria types: when marking unread by message ID, the message is included in the unread count; when marking by timestamp, the count includes only messages created after that timestamp. This distinction appropriately handles the two use cases.

Comment on lines +148 to 162
let findMessageDTO: () -> MessageDTO? = {
switch unreadCriteria {
case .messageId(let messageId):
return self.message(id: messageId)
case .messageTimestamp(let messageTimestamp):
let clientConfig = self.chatClientConfig
return try? MessageDTO.loadMessage(
beforeOrEqual: messageTimestamp,
cid: cid.rawValue,
deletedMessagesVisibility: clientConfig?.deletedMessagesVisibility ?? .alwaysVisible,
shouldShowShadowedMessages: clientConfig?.shouldShowShadowedMessages ?? false,
context: self
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Log errors instead of silently discarding them.

The try? on line 154 silently swallows any errors thrown by MessageDTO.loadMessage. If the fetch fails due to a database error, context issue, or other problem, the function returns early at line 163 without any indication of what went wrong, making debugging difficult.

Apply this diff to log errors:

 case .messageTimestamp(let messageTimestamp):
     let clientConfig = self.chatClientConfig
-    return try? MessageDTO.loadMessage(
-        beforeOrEqual: messageTimestamp,
-        cid: cid.rawValue,
-        deletedMessagesVisibility: clientConfig?.deletedMessagesVisibility ?? .alwaysVisible,
-        shouldShowShadowedMessages: clientConfig?.shouldShowShadowedMessages ?? false,
-        context: self
-    )
+    do {
+        return try MessageDTO.loadMessage(
+            beforeOrEqual: messageTimestamp,
+            cid: cid.rawValue,
+            deletedMessagesVisibility: clientConfig?.deletedMessagesVisibility ?? .alwaysVisible,
+            shouldShowShadowedMessages: clientConfig?.shouldShowShadowedMessages ?? false,
+            context: self
+        )
+    } catch {
+        log.error("Failed to load message by timestamp for marking unread: \(error)")
+        return nil
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let findMessageDTO: () -> MessageDTO? = {
switch unreadCriteria {
case .messageId(let messageId):
return self.message(id: messageId)
case .messageTimestamp(let messageTimestamp):
let clientConfig = self.chatClientConfig
return try? MessageDTO.loadMessage(
beforeOrEqual: messageTimestamp,
cid: cid.rawValue,
deletedMessagesVisibility: clientConfig?.deletedMessagesVisibility ?? .alwaysVisible,
shouldShowShadowedMessages: clientConfig?.shouldShowShadowedMessages ?? false,
context: self
)
}
}
let findMessageDTO: () -> MessageDTO? = {
switch unreadCriteria {
case .messageId(let messageId):
return self.message(id: messageId)
case .messageTimestamp(let messageTimestamp):
let clientConfig = self.chatClientConfig
do {
return try MessageDTO.loadMessage(
beforeOrEqual: messageTimestamp,
cid: cid.rawValue,
deletedMessagesVisibility: clientConfig?.deletedMessagesVisibility ?? .alwaysVisible,
shouldShowShadowedMessages: clientConfig?.shouldShowShadowedMessages ?? false,
context: self
)
} catch {
log.error("Failed to load message by timestamp for marking unread: \(error)")
return nil
}
}
}
🤖 Prompt for AI Agents
In Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift around lines 148 to
162, the use of `try?` when calling MessageDTO.loadMessage silently swallows
errors; replace it with a do-catch block that calls MessageDTO.loadMessage with
`try`, and in the catch log the caught error (using the existing logger in this
scope; if none exists, use a minimal fallback like print) including context
(cid/rawValue and the timestamp), then return nil to preserve current behavior.

@laevandus laevandus disabled auto-merge December 2, 2025 09:52
@github-actions
Copy link

github-actions bot commented Dec 2, 2025

Public Interface

 public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider  
-   public func loadChannelReads(pagination: Pagination? = nil,completion: @escaping (Error?) -> Void)
+   public func markUnread(from timestamp: Date,completion: ((Result<ChatChannel, Error>) -> Void)? = nil)
-   public func loadMoreChannelReads(limit: Int? = nil,completion: @escaping (Error?) -> Void)
+   public func loadChannelReads(pagination: Pagination? = nil,completion: @escaping (Error?) -> Void)
-   public func enableSlowMode(cooldownDuration: Int,completion: ((Error?) -> Void)? = nil)
+   public func loadMoreChannelReads(limit: Int? = nil,completion: @escaping (Error?) -> Void)
-   public func disableSlowMode(completion: ((Error?) -> Void)? = nil)
+   public func enableSlowMode(cooldownDuration: Int,completion: ((Error?) -> Void)? = nil)
-   public func startWatching(isInRecoveryMode: Bool,completion: ((Error?) -> Void)? = nil)
+   public func disableSlowMode(completion: ((Error?) -> Void)? = nil)
-   public func stopWatching(completion: ((Error?) -> Void)? = nil)
+   public func startWatching(isInRecoveryMode: Bool,completion: ((Error?) -> Void)? = nil)
-   public func freezeChannel(completion: ((Error?) -> Void)? = nil)
+   public func stopWatching(completion: ((Error?) -> Void)? = nil)
-   public func unfreezeChannel(completion: ((Error?) -> Void)? = nil)
+   public func freezeChannel(completion: ((Error?) -> Void)? = nil)
-   public func pin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
+   public func unfreezeChannel(completion: ((Error?) -> Void)? = nil)
-   public func unpin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
+   public func pin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
-   public func uploadAttachment(localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil,completion: @escaping ((Result<UploadedAttachment, Error>) -> Void))
+   public func unpin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
-   public func enrichUrl(_ url: URL,completion: @escaping (Result<LinkAttachmentPayload, Error>) -> Void)
+   public func uploadAttachment(localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil,completion: @escaping ((Result<UploadedAttachment, Error>) -> Void))
-   public func loadPinnedMessages(pageSize: Int = .messagesPageSize,sorting: [Sorting<PinnedMessagesSortingKey>] = [],pagination: PinnedMessagesPagination? = nil,completion: @escaping (Result<[ChatMessage], Error>) -> Void)
+   public func enrichUrl(_ url: URL,completion: @escaping (Result<LinkAttachmentPayload, Error>) -> Void)
-   public func currentCooldownTime()-> Int
+   public func loadPinnedMessages(pageSize: Int = .messagesPageSize,sorting: [Sorting<PinnedMessagesSortingKey>] = [],pagination: PinnedMessagesPagination? = nil,completion: @escaping (Result<[ChatMessage], Error>) -> Void)
-   public func deleteFile(url: String,completion: ((Error?) -> Void)? = nil)
+   public func currentCooldownTime()-> Int
-   public func deleteImage(url: String,completion: ((Error?) -> Void)? = nil)
+   public func deleteFile(url: String,completion: ((Error?) -> Void)? = nil)
-   public func getFirstUnreadMessageId(for channel: ChatChannel)-> MessageId?
+   public func deleteImage(url: String,completion: ((Error?) -> Void)? = nil)
-   public func setPushPreference(level: PushPreferenceLevel,completion: ((Result<PushPreference, Error>) -> Void)? = nil)
+   public func getFirstUnreadMessageId(for channel: ChatChannel)-> MessageId?
-   public func snoozePushNotifications(until date: Date,completion: ((Result<PushPreference, Error>) -> Void)? = nil)
+   public func setPushPreference(level: PushPreferenceLevel,completion: ((Result<PushPreference, Error>) -> Void)? = nil)
+   public func snoozePushNotifications(until date: Date,completion: ((Result<PushPreference, Error>) -> Void)? = nil)

 public class Chat  
-   @discardableResult public func reply(to parentMessageId: MessageId,text: String,showReplyInChannel: Bool = false,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],pinning: MessagePinning? = nil,extraData: [String: RawJSON] = [:],silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,messageId: MessageId? = nil)async throws -> ChatMessage
+   public func markUnread(from timestamp: Date)async throws 
-   @discardableResult public func loadReplies(for messageId: MessageId,pagination: MessagesPagination)async throws -> [ChatMessage]
+   @discardableResult public func reply(to parentMessageId: MessageId,text: String,showReplyInChannel: Bool = false,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],pinning: MessagePinning? = nil,extraData: [String: RawJSON] = [:],silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,messageId: MessageId? = nil)async throws -> ChatMessage
-   public func loadReplies(before replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   @discardableResult public func loadReplies(for messageId: MessageId,pagination: MessagesPagination)async throws -> [ChatMessage]
-   public func loadReplies(after replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadReplies(before replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func loadReplies(around replyId: MessageId,for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadReplies(after replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func loadOlderReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadReplies(around replyId: MessageId,for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func loadNewerReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadOlderReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   @discardableResult public func translateMessage(_ messageId: MessageId,to language: TranslationLanguage)async throws -> ChatMessage
+   public func loadNewerReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func mute(expiration: Int? = nil)async throws 
+   @discardableResult public func translateMessage(_ messageId: MessageId,to language: TranslationLanguage)async throws -> ChatMessage
-   public func unmute()async throws 
+   public func mute(expiration: Int? = nil)async throws 
-   public func hide(clearHistory: Bool = false)async throws 
+   public func unmute()async throws 
-   public func show()async throws 
+   public func hide(clearHistory: Bool = false)async throws 
-   public func pin(scope: ChannelPinningScope = .me)async throws 
+   public func show()async throws 
-   public func unpin(scope: ChannelPinningScope = .me)async throws 
+   public func pin(scope: ChannelPinningScope = .me)async throws 
-   public func subscribe(toEvent event: E.Type,handler: @escaping (E) -> Void)-> AnyCancellable
+   public func unpin(scope: ChannelPinningScope = .me)async throws 
-   public func subscribe(_ handler: @escaping (Event) -> Void)-> AnyCancellable
+   public func subscribe(toEvent event: E.Type,handler: @escaping (E) -> Void)-> AnyCancellable
-   public func sendEvent(_ payload: EventPayload)async throws 
+   public func subscribe(_ handler: @escaping (Event) -> Void)-> AnyCancellable
-   public func enableSlowMode(cooldownDuration: Int)async throws 
+   public func sendEvent(_ payload: EventPayload)async throws 
-   public func disableSlowMode()async throws 
+   public func enableSlowMode(cooldownDuration: Int)async throws 
-   public func truncate(systemMessage: String? = nil,hardDelete: Bool = true,skipPush: Bool = false)async throws 
+   public func disableSlowMode()async throws 
-   public func keystroke(parentMessageId: MessageId? = nil)async throws 
+   public func truncate(systemMessage: String? = nil,hardDelete: Bool = true,skipPush: Bool = false)async throws 
-   public func stopTyping(parentMessageId: MessageId? = nil)async throws 
+   public func keystroke(parentMessageId: MessageId? = nil)async throws 
-   public func update(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:])async throws 
+   public func stopTyping(parentMessageId: MessageId? = nil)async throws 
-   public func updatePartial(name: String? = nil,imageURL: URL? = nil,team: String? = nil,members: [UserId] = [],invites: [UserId] = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [])async throws 
+   public func update(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:])async throws 
-   public func deleteFile(at url: URL)async throws 
+   public func updatePartial(name: String? = nil,imageURL: URL? = nil,team: String? = nil,members: [UserId] = [],invites: [UserId] = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [])async throws 
-   public func deleteImage(at url: URL)async throws 
+   public func deleteFile(at url: URL)async throws 
-   public func uploadAttachment(with localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil)async throws -> UploadedAttachment
+   public func deleteImage(at url: URL)async throws 
-   @discardableResult public func loadWatchers(with pagination: Pagination)async throws -> [ChatUser]
+   public func uploadAttachment(with localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil)async throws -> UploadedAttachment
-   @discardableResult public func loadMoreWatchers(limit: Int? = nil)async throws -> [ChatUser]
+   @discardableResult public func loadWatchers(with pagination: Pagination)async throws -> [ChatUser]
+   @discardableResult public func loadMoreWatchers(limit: Int? = nil)async throws -> [ChatUser]

@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title develop branch diff status
StreamChat 7.26 MB 7.28 MB +18 KB 🟢
StreamChatUI 4.89 MB 4.89 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Collaborator

StreamChat XCSize

Object Diff (bytes)
MarkUnreadPayload.o +7822
ChannelController.o +4868
Chat.o +2580
MessageDTO.o +2300
ReadStateHandler.o +1148
Show 7 more objects
Object Diff (bytes)
ChannelReadDTO.o +964
ChannelRepository.o +764
MessageRepository.o +338
ChatMessage.o +264
ChannelReadUpdaterMiddleware.o +212
ReminderPayloads.o +130
ChannelListQuery.o +68

@Stream-SDK-Bot
Copy link
Collaborator

StreamChatUI XCSize

Object Diff (bytes)
ChatChannelVC.o +1172

@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 2, 2025

@laevandus laevandus added 🟢 QAed A PR that was QAed and removed 🧪 QAing labels Dec 2, 2025
@laevandus laevandus merged commit 955f978 into develop Dec 2, 2025
14 checks passed
@laevandus laevandus deleted the feature/mark-unread-with-timestamp branch December 2, 2025 11:17
@Stream-SDK-Bot Stream-SDK-Bot mentioned this pull request Dec 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✅ Feature An issue or PR related to a feature 🟢 QAed A PR that was QAed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants