Skip to content

Improve handling of pending and local-only messages#6252

Open
VelikovPetar wants to merge 30 commits intov7from
bug/handle_local_only_messages
Open

Improve handling of pending and local-only messages#6252
VelikovPetar wants to merge 30 commits intov7from
bug/handle_local_only_messages

Conversation

@VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Mar 16, 2026

Goal

Improve the handling of pending and local-only messages (ephemeral, error, failed, in-progress) so they are never lost when a server response replaces the active message window, and so that the pagination state accurately tracks the currently loaded range to filter these messages correctly.

Note: Review after #6234 is merged.

Implementation

Local-only message preservation

Introduces the concept of "local-only messages" — messages that are only known to the client and are never returned by the server after the initial send attempt. This covers:

  • Messages with sync status SYNC_NEEDED, IN_PROGRESS, AWAITING_ATTACHMENTS, FAILED_PERMANENTLY
  • Messages with type ephemeral (e.g. Giphy previews) or error

A new selectLocalOnlyMessagesForChannel(cid) method is added to MessageRepository, backed by a new DAO query (selectBySyncStatusOrTypeForChannel) and a database migration to version 102. When updateStateFromDatabase is called, these messages are loaded from the DB and stored separately in ChannelStateImpl.localOnlyMessages. They are filtered by the current pagination window (floor/ceiling) before being merged into the public messages flow, alongside regular messages.

Pagination state refactoring (MessagesPaginationManager)

Replaces the five ad-hoc MutableStateFlow fields for loading/end-of-page state in ChannelStateImpl (_loading, _loadingOlderMessages, _loadingNewerMessages, _endOfOlderMessages, _endOfNewerMessages) with a single MessagesPaginationState data class managed by a new MessagesPaginationManager interface (MessagesPaginationManagerImpl). The manager tracks:

  • oldestMessage / newestMessage — the current pagination window (floor and ceiling)
  • hasLoadedAllPreviousMessages / hasLoadedAllNextMessages — whether the ends have been reached
  • isLoadingPreviousMessages / isLoadingNextMessages / isLoadingMiddleMessages — loading indicators
  • isInWindow(message) — helper to check if a message falls within the currently loaded range

begin(query) and end(query, result) are called around each paginated channel query to atomically transition loading states. ChannelLogicImpl.setPaginationDirection and onQueryChannelResult are updated to delegate to the manager. The removed PendingMessagesManager date-range logic (advanceOldestLoadedDate, setNewestLoadedDate, advanceNewestLoadedDate) is superseded by the manager's window tracking.

Call-order fix in QueryChannelListenerState

setPaginationDirection is now called before updateStateFromDatabase, ensuring the pagination state (oldest message floor) is initialized from the DB before the window filter is applied to local-only messages.

🎨 UI Changes

Add relevant videos

Before After
local-only-before.mp4
local-only-after.mp4

Testing

The changes are covered by unit tests across three test classes:

  • ChannelStateImplLocalOnlyMessagesTest (new) — 14 tests covering visibility, floor/ceiling window filtering, upsert (ephemeral path), and delete cleanup for local-only messages.
  • ChannelStateImplMessagesTest — extended with tests for upsertCachedLatestMessages and the gap-detection / updateDataForChannel scenarios (refresh, contiguous upsert, gap → cache, insideSearch → cache).
  • ChannelLogicImplTest — updated to mock MessagesPaginationManager and verify delegation; added two new tests for updateStateFromDatabase covering setOldestMessage with messages and setOldestMessage(null) for empty results.

🎉 GIF

Please provide a suitable gif that describes your work on this pull request

Summary by CodeRabbit

  • New Features

    • Enhanced message pagination with improved state tracking and loading indicators for older and newer messages.
    • Added support for preserving local-only messages across connection state changes.
  • Bug Fixes

    • Improved message synchronization when loading channel history with better gap detection.
  • Refactor

    • Redesigned internal message state management to consolidate pagination and message lifecycle handling.
    • Updated database schema for optimized message querying.

VelikovPetar and others added 24 commits March 10, 2026 12:07
Co-Authored-By: Claude <noreply@anthropic.com>
…fter_coming_back_from_background' into bug/AND-1113_fix_channel_jumps_after_coming_back_from_background
- 8 @disabled @test stubs covering all SyncStatus values (SYNC_NEEDED,
  IN_PROGRESS, AWAITING_ATTACHMENTS, FAILED_PERMANENTLY) plus type-based
  cases (ephemeral/error with COMPLETED) and negative cases (COMPLETED/system)
- All stubs disabled with "Wave 1 — implement isLocalOnly() first" message
- Satisfies PRES-05 Wave 0 requirement: test class resolvable by Gradle
  --tests "*.MessageIsLocalOnlyTest" without "no tests found" failure
…reservingLocalOnly()

- 12 @disabled @test stubs covering survival cases (FAILED_PERMANENTLY,
  ephemeral, AWAITING_ATTACHMENTS, pending edit SYNC_NEEDED), collision
  (server wins), window floor (below-floor excluded, boundary included),
  empty page (null floor = all local-only), no-DB fallback, DB+state union,
  COMPLETED exclusion, and DB seed full-replace semantics
- Extends ChannelStateImplTestBase to reuse channelState fixture
- All stubs disabled with "Wave 2 — implement setMessagesPreservingLocalOnly() first"
- Satisfies PRES-01/PRES-04 Wave 0 requirement: test class resolvable by
  Gradle --tests "*.ChannelStateImplPreservationTest" without build failure
- Create MessageLocalOnlyExt.kt with internal fun Message.isLocalOnly()
- Covers SYNC_NEEDED, IN_PROGRESS, AWAITING_ATTACHMENTS, FAILED_PERMANENTLY,
  type==ephemeral, and type==error; returns false for COMPLETED+regular/system
- Remove @disabled from all 8 MessageIsLocalOnlyTest stubs and fill real assertions
- All 8 tests pass
- Add selectLocalOnlyMessagesForChannel to MessageRepository interface
- NoOpMessageRepository overrides it returning emptyList()
- MessageDao.selectLocalOnlyForChannel @query without default args (Room constraint)
- DatabaseMessageRepository implements with explicit SyncStatus int constants
  and MessageType string constants, maps entities via existing toMessage()
- RepositoryFacade picks up new method automatically via 'by messageRepository' delegation
…n to 102

- ChannelEntity gains oldestLoadedDate: Date? = null as last field
  (floor for local-only message window; written in Phase 1, read in Phase 2)
- ChatDatabase version bumped 101 -> 102
- fallbackToDestructiveMigration() already in place — no migration script needed
- No public API surface changes; no legacy path touched
- 12 test cases covering all preservation scenarios
- Cases: FAILED_PERMANENTLY/ephemeral/AWAITING_ATTACHMENTS survival, ID collision (server wins),
  window floor filtering (below excluded, above included, boundary included),
  null floor (all local-only included), no-DB fallback, state+DB union dedup,
  COMPLETED not preserved, setMessages full-replace semantics unchanged
- Tests fail to compile until implementation is added (TDD RED phase)
…Impl

- Adds internal fun setMessagesPreservingLocalOnly(incoming, localOnlyFromDb, windowFloor)
  immediately after setMessages in ChannelStateImpl
- Uses _messages.update { } (CAS atomic read-modify-write, no value assignment)
- Step 1: gathers local-only from current in-memory state via isLocalOnly() predicate
- Step 2: unions with localOnlyFromDb, deduplicates by ID
- Step 3: server wins on ID collision (local-only with matching incoming ID dropped)
- Step 4: applies windowFloor filter (null = include all; createdAt < floor = exclude)
- Step 5: filters incoming through existing shouldIgnoreUpsertion() guard
- Step 6: merges and sorts with existing MESSAGE_COMPARATOR
- Step 7: syncs quotedMessagesMap and poll indexes (mirrors setMessages post-loop)
- setMessages is NOT modified (retains full-replace semantics for DB-seed path)
- All 12 ChannelStateImplPreservationTest cases pass; all 8 MessageIsLocalOnlyTest pass
…ry layer

- ChannelDao: add updateOldestLoadedDate @query targeting oldestLoadedDate column
- ChannelRepository: add updateOldestLoadedDateForChannel interface method
- NoOpChannelRepository: add no-op override returning Unit
- DatabaseChannelRepository: implement via channelDao.updateOldestLoadedDate
- RepositoryFacade picks up new method automatically via ChannelRepository delegation
…l call sites

- updateMessages: change signature to suspend, add localOnlyFromDb and windowFloor params
- Three setMessages call sites in updateMessages replaced with setMessagesPreservingLocalOnly
- DB-seed path (updateDataForChannel/shouldRefreshMessages=true) unchanged at state.setMessages
- onQueryChannelResult: prefetch localOnlyFromDb and derive windowFloor in coroutineScope.launch
- windowFloor derived from min(channel.messages.createdAt); null when incoming is empty
- Floor persistence: repository.updateOldestLoadedDateForChannel called when windowFloor != null
- ChannelLogicImplTest: add PreservationCallSites nested class with 4 new test cases
- ChannelLogicImplTest: update 3 existing call-site tests to verify setMessagesPreservingLocalOnly
- ChannelLogicLegacyImpl untouched
- Add single-column SELECT @query for oldestLoadedDate column
- Method returns Date? matching ChannelEntity field nullability
- Placed immediately after updateOldestLoadedDate for co-location
- Add interface method to ChannelRepository returning Date?
- Add no-op override returning null in NoOpChannelRepository
- Add DB implementation delegating to channelDao.selectOldestLoadedDate
- RepositoryFacade picks up via ChannelRepository by channelsRepository delegation
- Updated filteringOlderMessages and isFilteringNewerMessages tests to verify setMessagesPreservingLocalOnly replaces upsertMessages
- Added new tests for endReached=true path (null floor), reconnect path (isChannelsStateUpdate=false), and else-upsert branch in updateDataForChannel
- Updated DB-seed tests to pass isChannelsStateUpdate=true explicitly
- Added isNull import for Mockito

RED phase: 8 tests failing as expected
…ches

- filteringOlderMessages branch: replace upsertMessages with setMessagesPreservingLocalOnly(messages, localOnlyFromDb, windowFloor)
- isFilteringNewerMessages endReached=false: replace upsertMessages with setMessagesPreservingLocalOnly(messages, localOnlyFromDb, windowFloor)
- isFilteringNewerMessages endReached=true: replace upsertMessages with setMessagesPreservingLocalOnly(messages, localOnlyFromDb, null) — null floor signals no-ceiling (reached latest messages)
- trimOldestMessages / trimNewestMessages calls preserved in both branches

GREEN: pagination preservation tests pass (Task 1 complete)
- Prefetch localOnlyFromDb and persistedFloor from repository at top of messageLimit > 0 block
- shouldRefreshMessages branch: isChannelsStateUpdate=true uses setMessages (DB-seed; local-only already in DB), isChannelsStateUpdate=false uses setMessagesPreservingLocalOnly (SyncManager reconnect)
- else upsert branch: replaced upsertMessages with setMessagesPreservingLocalOnly using persistedFloor
- insideSearch and hasGap branches unchanged (route to cache, not active messages)
- Removed TODO(Phase 2) comment from updateMessages — floor is now read in updateDataForChannel

GREEN: all 92 tests pass — full trigger coverage complete
- 4 tests verifying filteringOlderMessages and isFilteringNewerMessages
  branches call setMessagesPreservingLocalOnly (not upsertMessages)
- end-reached test verifies null windowFloor (isNull())
- mid-page test verifies advanceNewestLoadedDate still called
- 3 tests covering updateDataForChannel reconnect path, DB-seed path,
  and else-upsert branch
- reconnect (isChannelsStateUpdate=false) verifies setMessagesPreservingLocalOnly
- DB-seed (isChannelsStateUpdate=true) verifies setMessages full-replace (regression guard)
- else-upsert branch verifies setMessagesPreservingLocalOnly, never upsertMessages
- Full testDebugUnitTest suite: BUILD SUCCESSFUL, zero failures
…anches

Pagination branches (filteringOlderMessages, isFilteringNewerMessages) were
incorrectly calling setMessagesPreservingLocalOnly which *replaces* the entire
message list with only the new page. Instead they must *merge* the new page
into existing state so prior pagination results are preserved.

Add upsertMessagesPreservingLocalOnly to ChannelStateImpl — identical to
setMessagesPreservingLocalOnly except step 6 keeps existingServer messages
alongside incoming + local-only, preventing page drops on pagination.

Also fix endReached branch to pass windowFloor instead of null: the floor
must not reset when we reach the newest end; it stays at the oldest loaded
point across the full pagination session.

Tests updated to assert upsertMessagesPreservingLocalOnly for pagination
paths and setMessagesPreservingLocalOnly for set/replace paths only.
Fix flaky else-branch test to use ID overlap for deterministic hasGap=false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@VelikovPetar VelikovPetar added pr:bug Bug fix pr:improvement Improvement labels Mar 16, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Contributor

DB Entities have been updated. Do we need to upgrade DB Version?
Modified Entities :

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/channel/internal/ChannelEntity.kt

@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.70 MB 0.45 MB 🟡
stream-chat-android-ui-components 10.60 MB 11.01 MB 0.41 MB 🟡
stream-chat-android-compose 12.81 MB 12.04 MB -0.78 MB 🚀

@VelikovPetar VelikovPetar marked this pull request as ready for review March 16, 2026 16:39
@VelikovPetar VelikovPetar requested a review from a team as a code owner March 16, 2026 16:39
@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

Walkthrough

This pull request refactors the message pagination and state management system. It introduces a new MessagesPaginationManager to centralize pagination logic, adds local-only message handling, removes the legacy PendingMessagesManager, updates database schema to version 102, and refactors ChannelLogicImpl and ChannelStateImpl to use the new pagination patterns throughout.

Changes

Cohort / File(s) Summary
Database Layer
api/stream-chat-android-client.api, src/.../offline/repository/database/internal/ChatDatabase.kt, src/.../offline/repository/domain/message/internal/MessageDao.kt, src/.../offline/repository/domain/message/internal/DatabaseMessageRepository.kt
Database schema bumped from version 101 to 102; new DAO method selectBySyncStatusOrTypeForChannel added for querying messages by sync status or type within a channel; DatabaseMessageRepository implements new selectLocalOnlyMessagesForChannel method.
Pagination System (New)
src/.../state/plugin/state/channel/internal/MessagesPaginationState.kt
Introduces comprehensive pagination management: MessagesPaginationState data class tracks oldest/newest message bounds and loading flags; MessagesPaginationManager interface and MessagesPaginationManagerImpl provide lifecycle methods (begin, end, setters, reset) for three pagination modes (loading older/newer/around).
Message Utilities & Local-Only Support
src/.../utils/message/MessageUtils.kt, src/.../persistance/repository/MessageRepository.kt
Added Message.isLocalOnly() extension function; introduced LocalOnlySyncStatuses (SYNC_NEEDED, IN_PROGRESS, AWAITING_ATTACHMENTS, FAILED_PERMANENTLY) and LocalOnlyMessageTypes (EPHEMERAL, ERROR) collections to identify messages that must persist across server updates.
State Management Refactoring
src/.../state/plugin/state/channel/internal/ChannelStateImpl.kt
Major refactoring: integrated paginationManager dependency; introduced localOnlyMessages and pendingMessages state tracking with in-range computed views; replaced direct loading flags with pagination-manager-derived flags; added new APIs (setLocalOnlyMessages, setPendingMessages, removePendingMessage, upsertCachedLatestMessages); enhanced message upsertion/deletion to handle ephemeral and local-only messages.
Channel Logic Refactoring
src/.../state/plugin/logic/channel/internal/ChannelLogicImpl.kt, src/.../state/plugin/listener/internal/QueryChannelListenerState.kt
Refactored fetchOfflineChannel to retrieve and set local-only messages; delegated pagination to paginationManager (setPaginationDirection now calls paginationManager.begin); onQueryChannelResult now calls paginationManager.end; added hasGap helper for detecting non-overlapping pages; reordered state update sequencing in QueryChannelListenerState.
Repository Implementations
src/.../persistance/repository/noop/NoOpChannelRepository.kt, src/.../persistance/repository/noop/NoOpMessageRepository.kt
Removed evictChannel and clear methods from NoOpChannelRepository; added no-op selectLocalOnlyMessagesForChannel implementation to NoOpMessageRepository.
Core Pagination Tests
src/test/.../internal/MessagesPaginationManagerImplTest.kt
Comprehensive test suite covering all pagination modes, state transitions, edge cases, and error handling for the new MessagesPaginationManagerImpl; validates loading flag isolation, message bound updates, and reset behavior.
State Management Tests
src/test/.../internal/ChannelStateImplLocalOnlyMessagesTest.kt, src/test/.../internal/ChannelStateImplMessagesTest.kt, src/test/.../internal/ChannelStateImplNonChannelStatesTest.kt, src/test/.../internal/ChannelStateImplPendingMessagesTest.kt, src/test/.../internal/ChannelStateImplTestBase.kt
Updated test infrastructure: new ChannelStateImplLocalOnlyMessagesTest for local-only message scenarios; refactored ChannelStateImplMessagesTest with base class inheritance and nested test suites; updated ChannelStateImplNonChannelStatesTest and ChannelStateImplPendingMessagesTest to use paginationManager API; added createLocalOnlyMessage helper to test base.
Logic & Integration Tests
src/test/.../internal/ChannelLogicImplTest.kt, src/test/.../attachment/UploadAttachmentsIntegrationTests.kt, src/test/.../utils/message/MessageUtilsTest.kt
Updated ChannelLogicImplTest to wire paginationManager mock and verify pagination interactions instead of direct state mutations; added tests for offline database loading behavior; updated UploadAttachmentsIntegrationTests mock; added eight new isLocalOnly test cases in MessageUtilsTest.
Removed Components
src/.../state/plugin/state/channel/internal/PendingMessagesManager.kt, src/test/.../internal/PendingMessagesManagerTest.kt
Deleted entire PendingMessagesManager class (state tracking, filtering, date-range logic, lifecycle methods) and its corresponding 402-line test suite; functionality is now integrated into ChannelStateImpl and managed by paginationManager.

Sequence Diagram

sequenceDiagram
    participant Client
    participant QueryListener as QueryChannelListener
    participant Logic as ChannelLogicImpl
    participant Manager as MessagesPaginationManager
    participant State as ChannelStateImpl
    participant Database as MessageDatabase
    participant Server as Server API

    Client->>QueryListener: onQueryChannelRequest()
    QueryListener->>Logic: updateStateFromDatabase()
    Logic->>Database: selectLocalOnlyMessagesForChannel()
    Database-->>Logic: local-only messages
    Logic->>State: setLocalOnlyMessages()
    Logic->>State: setOldestMessage()
    
    QueryListener->>Manager: begin(query)
    Manager->>Manager: determine pagination mode<br/>(LESS_THAN/GREATER_THAN/AROUND_ID)
    Manager->>Manager: update loading flags
    
    Logic->>Server: queryChannel(request)
    Server-->>Logic: Result<Channel>
    
    Logic->>Manager: end(query, result)
    Manager->>Manager: update pagination bounds<br/>(oldestMessage/newestMessage)
    Manager->>Manager: set hasLoadedAll flags
    
    Logic->>State: updateDataForChannel()<br/>(with new messages)
    State->>State: merge messages from three sources<br/>(server + local-only + pending)
    State->>State: filter by pagination bounds
    State-->>Client: emit updated messages flow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops with glee through pagination's lane,
Local-only messages, no server strain!
PaginationManager orchestrates with care,
State and logic dancing—fair and square!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve handling of pending and local-only messages' clearly and concisely summarizes the main objective of the changeset.
Description check ✅ Passed The PR description is comprehensive, covering Goal, Implementation, Testing, and UI Changes sections with sufficient technical detail about the changes made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug/handle_local_only_messages
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

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: 4

🧹 Nitpick comments (2)
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplTestBase.kt (1)

92-104: Make createLocalOnlyMessage flexible for both local-only paths.

Right now the helper is local-only only because of type = EPHEMERAL, while syncStatus = COMPLETED. Making status/type configurable improves test clarity and coverage for sync-status-based local-only behavior.

🧪 Suggested refactor
-protected fun createLocalOnlyMessage(index: Int, timestamp: Long): Message =
+protected fun createLocalOnlyMessage(
+    index: Int,
+    timestamp: Long,
+    syncStatus: SyncStatus = SyncStatus.IN_PROGRESS,
+    type: MessageType = MessageType.EPHEMERAL,
+): Message =
     randomMessage(
         id = "local_only_$index",
         cid = CID,
         createdAt = Date(timestamp),
         createdLocallyAt = null,
-        syncStatus = SyncStatus.COMPLETED,
-        type = MessageType.EPHEMERAL,
+        syncStatus = syncStatus,
+        type = type,
         parentId = null,
         showInChannel = true,
         shadowed = false,
         deletedAt = null,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplTestBase.kt`
around lines 92 - 104, The helper createLocalOnlyMessage is hardcoded to use
type = MessageType.EPHEMERAL and syncStatus = SyncStatus.COMPLETED, which
prevents testing the other local-only path; update createLocalOnlyMessage to
accept optional parameters for message type and sync status (e.g., messageType:
MessageType = MessageType.EPHEMERAL, syncStatus: SyncStatus =
SyncStatus.COMPLETED) and pass them through to the call to randomMessage so
tests can construct messages that are local-only via either type or syncStatus.
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/MessageRepository.kt (1)

203-212: Add thread-expectation notes to the new public API KDoc.

The new API KDoc is helpful, but it should also state threading/dispatcher expectations for callers.

✍️ Suggested KDoc update
 /**
  * Returns all messages for [cid] that are local-only: syncStatus is one of
  * SYNC_NEEDED, IN_PROGRESS, AWAITING_ATTACHMENTS, FAILED_PERMANENTLY, or type is
  * "ephemeral" or "error". Used by the preservation mechanism before a server response
  * replaces the active message window.
+ *
+ * Threading: safe to call from any coroutine context; implementations may hit local storage.
+ * State notes: result may include messages not present in the active in-memory window.
  *
  * `@param` cid The channel ID (format "type:id").
  * `@return` List of local-only messages, unordered.
  */

As per coding guidelines: "Document public APIs with KDoc, including thread expectations and state notes".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/MessageRepository.kt`
around lines 203 - 212, Update the KDoc for the public suspend function
selectLocalOnlyMessagesForChannel to include thread/dispatcher expectations and
state notes: explicitly state that although it is suspendable it performs local
database access and should be called from a background coroutine (e.g.,
Dispatchers.IO or a non-main thread), whether it is safe to call from main/UI,
and that it returns an unordered snapshot of local-only messages (no network
calls, no mutation of state). Place this note in the function KDoc so callers
know the expected coroutine context and the function’s side-effect and ordering
guarantees.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/database/internal/ChatDatabase.kt`:
- Line 91: The Room database was bumped to version = 102 but getDatabase() still
relies on fallbackToDestructiveMigration, which will erase user data on upgrade;
add an explicit Migration object for 101→102 and register it via
addMigrations(...) on the Room.databaseBuilder in getDatabase() (or remove
fallbackToDestructiveMigration and supply all needed migrations), ensuring the
Migration handles the new draft message & pagination bound schema changes
declared in ChatDatabase (version = 102).

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/MessagesPaginationState.kt`:
- Around line 193-200: When updating pagination state in MessagesPaginationState
(inside the block that computes hasLoadedAllPreviousMessages and calls
current.copy), avoid clearing the existing floor when the older-page response is
an empty list: compute a newOldestMessage that uses current.oldestMessage if
messages.isEmpty(), otherwise use the response oldestMessage, and pass that
newOldestMessage into current.copy (instead of unconditionally setting
oldestMessage to the response value). Keep the existing
hasLoadedAllPreviousMessages logic (messages.size < query.messagesLimit()) and
leave the other flags (isLoadingNextMessages, isLoadingPreviousMessages,
isLoadingMiddleMessages) unchanged.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImplTest.kt`:
- Around line 686-699: The test fixture returns messages in ascending order but
the real MessageDao.messagesForChannel returns messages in DESC order, so change
the stubbed messages used in ChannelLogicImplTest (the list passed to
whenever(repository.selectMessagesForChannel(...))) to be ordered DESC (newest
first) so that paginationManager.setOldestMessage is asserted against the
correct item; locate the stub in the test that sets
whenever(repository.selectMessagesForChannel(any(), any())) and reverse or
reorder the messages list (used with randomMessage id "m1"/"m2") so
messages.first() represents the newest and messages.last() the oldest to match
MessageDao.messagesForChannel behavior referenced by updateStateFromDatabase and
paginationManager.setOldestMessage.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplPendingMessagesTest.kt`:
- Line 145: In ChannelStateImplPendingMessagesTest, the fixture named ceiling is
created with id = "floor" which is confusing; update the call to randomMessage
that constructs the ceiling variable to use a matching id (e.g., id = "ceiling")
so the variable name and message id align for clearer test output and easier
debugging when assertions fail.

---

Nitpick comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/MessageRepository.kt`:
- Around line 203-212: Update the KDoc for the public suspend function
selectLocalOnlyMessagesForChannel to include thread/dispatcher expectations and
state notes: explicitly state that although it is suspendable it performs local
database access and should be called from a background coroutine (e.g.,
Dispatchers.IO or a non-main thread), whether it is safe to call from main/UI,
and that it returns an unordered snapshot of local-only messages (no network
calls, no mutation of state). Place this note in the function KDoc so callers
know the expected coroutine context and the function’s side-effect and ordering
guarantees.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplTestBase.kt`:
- Around line 92-104: The helper createLocalOnlyMessage is hardcoded to use type
= MessageType.EPHEMERAL and syncStatus = SyncStatus.COMPLETED, which prevents
testing the other local-only path; update createLocalOnlyMessage to accept
optional parameters for message type and sync status (e.g., messageType:
MessageType = MessageType.EPHEMERAL, syncStatus: SyncStatus =
SyncStatus.COMPLETED) and pass them through to the call to randomMessage so
tests can construct messages that are local-only via either type or syncStatus.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 95d85e40-860e-4ea3-95ad-7ad586800194

📥 Commits

Reviewing files that changed from the base of the PR and between 8660d8d and 9ec95e2.

📒 Files selected for processing (23)
  • stream-chat-android-client/api/stream-chat-android-client.api
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/database/internal/ChatDatabase.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/message/internal/DatabaseMessageRepository.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/message/internal/MessageDao.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryChannelListenerState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImpl.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImpl.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/MessagesPaginationState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/PendingMessagesManager.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/MessageRepository.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/noop/NoOpChannelRepository.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/noop/NoOpMessageRepository.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/channel/controller/attachment/UploadAttachmentsIntegrationTests.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImplTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplLocalOnlyMessagesTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplMessagesTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplNonChannelStatesTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplPendingMessagesTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplTestBase.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/MessagesPaginationManagerImplTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/PendingMessagesManagerTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/utils/message/MessageUtilsTest.kt
💤 Files with no reviewable changes (3)
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/persistance/repository/noop/NoOpChannelRepository.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/PendingMessagesManager.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/PendingMessagesManagerTest.kt

- Fix ChannelLogicImplTest: stub messages now ordered DESC (newest first)
  to match MessageDao.messagesForChannel behaviour; assertion updated to
  verify setOldestMessage receives the oldest message (m1/1000ms).
- Fix ChannelStateImplPendingMessagesTest: ceiling fixture id corrected
  from "floor" to "ceiling" to avoid confusion when assertions fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Bug fix pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant