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

Explain how this change can be tested manually, if applicable.

☑️ 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 specific date/timestamp (in addition to per-message unread), enabling bulk unread marking from a chosen point in time.
  • UI

    • Demo app: new "Mark channel unread with timestamp" action to try timestamp-based unread marking.
  • Bug Fixes

    • Unread-related UI now refreshes when notification-based unread events arrive.

✏️ 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: introduces MarkUnreadCriteria and MarkUnreadPayload, new public APIs to mark unread from a Date, updates endpoint payloads, repository/database/worker/controller wiring, tests, mocks, UI event handling, and Xcode "Profile" build configs.

Changes

Cohort / File(s) Summary
Public API Additions
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift, Sources/StreamChat/StateLayer/Chat.swift
Added markUnread(from: Date) APIs and updated message-id variant to wrap as .messageId(...).
Payload & Endpoint
Sources/StreamChat/APIClient/Endpoints/Payloads/MarkUnreadPayload.swift, Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift, Tests/.../APIClient/Endpoints/ChannelEndpoints_Tests.swift
Added MarkUnreadCriteria and MarkUnreadPayload; changed endpoint to markUnread(cid:payload:) using payload for .messageId or .messageTimestamp.
Database: message lookup & unread logic
Sources/StreamChat/Database/DTOs/MessageDTO.swift, Sources/StreamChat/Database/DTOs/ChannelReadDTO.swift, Sources/StreamChat/Database/DatabaseSession.swift
Added loadMessage(beforeOrEqual:...); markChannelAsUnread now accepts MarkUnreadCriteria and resolves message by id or timestamp.
Repositories & State
Sources/StreamChat/Repositories/ChannelRepository.swift, Sources/StreamChat/Repositories/MessageRepository.swift, Sources/StreamChat/StateLayer/Chat.swift
markUnread/getMessage signatures now accept MarkUnreadCriteria; branching for .messageId vs .messageTimestamp.
Workers & Handlers
Sources/StreamChat/Workers/ChannelUpdater.swift, Sources/StreamChat/Workers/ReadStateHandler.swift
Propagated MarkUnreadCriteria through channel updater and read-state handler APIs and internals.
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.
Demo App
DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Added UI action to mark channel unread by timestamp (prompts for days offset).
Mocks
TestTools/.../Database/DatabaseSession_Mock.swift, .../Repositories/ChannelRepository_Mock.swift, .../Workers/ChannelUpdater_Mock.swift
Updated mocks/state to use MarkUnreadCriteria instead of raw MessageId.
Tests
Tests/.../Controllers/ChannelController_Tests.swift, Tests/.../Database/DTOs/ChannelReadDTO_Tests.swift, Tests/.../Repositories/ChannelRepository_Tests.swift, Tests/.../Repositories/MessageRepository_Tests.swift, Tests/.../Workers/ChannelUpdater_Tests.swift, Tests/.../StateLayer/Chat_Tests.swift
Updated/added tests to cover .messageId wrapping and .messageTimestamp flows and payload usage.
Build Configuration
StreamChat.xcodeproj/project.pbxproj
Added "Profile" build configurations across targets.

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?
    MessageRepository-->>ReadStateHandler: Result<MessageId?, Error>
    ReadStateHandler->>ChannelUpdater: markUnread(from: .messageTimestamp(date))
    ChannelUpdater->>ChannelRepository: markUnread(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) | ⏱️ ~20–30 minutes

  • Pay extra attention to:
    • MarkUnreadPayload.encode(to:) — correct keys and ordering for both criteria cases.
    • MessageDTO.loadMessage(beforeOrEqual:...) — predicate correctness for channel, deleted/shadowed visibility, and sort/limit.
    • ChannelReadDTO.markChannelAsUnread(...) — message resolution closure and edge-case handling when message not found.
    • Tests exercising timestamp paths in ChannelReadDTO_Tests, MessageRepository_Tests, and new Chat tests.

Possibly related PRs

Suggested labels

🌐 SDK: StreamChat (LLC), 🎨 SDK: StreamChatUI, 🪧 Demo App

Suggested reviewers

  • martinmitrevski

Poem

🐰 I hopped through lines and found the clue,

Timestamps now tell what unread should do.
Wrapped IDs or dates, both play a part—
The burrow's tidy, unread's an art! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% 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' accurately and clearly summarizes the main change—adding timestamp-based marking functionality alongside existing message ID-based approaches.
Linked Issues check ✅ Passed The PR successfully implements all coding objectives from IOS-1277: adds public APIs (ChatChannelController.markUnread(from:completion:) and Chat.markUnread(from:)) accepting Date parameters, introduces MarkUnreadCriteria enum for unified handling, reuses backend endpoint with message_timestamp, and updates ChatChannelVC UI handling.
Out of Scope Changes check ✅ Passed All changes are in scope: payload/endpoint refactoring supports timestamp feature, database/repository updates enable timestamp queries, mock updates maintain test infrastructure, and demo/UI updates showcase functionality per IOS-1277 requirements.
✨ 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

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

2 Warnings
⚠️ Big PR
⚠️ The changes should be manually QAed before the Pull Request will be merged

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.

@github-actions
Copy link

Public Interface

 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> = [],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] = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [])async throws 
+   public func update(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],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] = [],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]

 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)

@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 +2100
ReadStateHandler.o +1144
Show 7 more objects
Object Diff (bytes)
ChannelReadDTO.o +768
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

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 🤞 Ready For QA A PR that is Ready for QA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants