Skip to content

client: Count thread replies toward the slow mode cooldown#6505

Open
andremion wants to merge 2 commits into
developfrom
feature/and-1230-thread-replies-composer-cooldown
Open

client: Count thread replies toward the slow mode cooldown#6505
andremion wants to merge 2 commits into
developfrom
feature/and-1230-thread-replies-composer-cooldown

Conversation

@andremion

@andremion andremion commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Goal

When slow mode is active, sending a thread-only reply did not start the composer cooldown. The backend cooldown is channel-wide (per app, channel, user) and counts thread replies, so the user replied in a thread, saw no countdown, sent again, and the server rejected the message. iOS already counts thread replies; this brings Android (both Compose and XML) to the same behavior. Reported by the Flutter team.

Linear: AND-1230

Implementation

The composer cooldown is driven only by ChannelState.lastSentMessageDate, whose only consumer is the cooldown observer in the shared MessageComposerController (used by both the Compose and XML composers). That observer is not gated by message mode, so once lastSentMessageDate reflects thread replies, the countdown works in both channel mode and thread mode with no composer change.

lastSentMessageDate was derived from the channel message list filtered to the current user. A thread-only reply (parentId != null && showInChannel == false) never contributed, for different reasons in the two channel-state implementations:

  • New ChannelStateImpl (default): shouldIgnoreUpsertion drops thread-only replies at every write entry point, so they never enter the message list.
  • Legacy ChannelStateLegacyImpl: they enter the raw map but are filtered out before lastSentMessageDate reads the visible list.

This change keeps the existing channel-message derivation untouched and adds a thread-reply contribution, then exposes lastSentMessageDate as the later of the two. The contribution is a small per-channel value updated at the message write entry points with the current user's most recent thread-only reply date, and it only moves forward. The optimistic send path feeds it, so the countdown starts on send. The same change is applied to both state implementations, so behavior is consistent whether useLegacyChannelLogic is false (default) or true.

The visible message list, shouldIgnoreUpsertion, and lastMessageAt are unchanged, so no other consumer is affected. There is no public API change.

Cross-session resume needs no extra work: on channel init the offline load reads messages (including thread replies) and feeds the same value, which only moves forward, so it survives the later server refresh.

Parity note: this matches iOS, where the cooldown comes from channel.lastMessageFromCurrentUser (which counts thread replies) via currentCooldownTime(), used by both the UIKit and SwiftUI composers.

Testing

Manual steps (the Compose and XML sample apps behave the same):

  1. Enable slow mode on a channel from the dashboard (set a cooldown, for example 10s).
  2. Open the channel as a member without the skip-slow-mode capability.
  3. Open a thread on any message and send a reply.
  4. Expected: the composer shows the countdown and the send button is disabled until it reaches zero, the same as sending a message in the channel.
  5. While the countdown is active, go back to the channel. Expected: the channel composer also shows the remaining countdown.
  6. Open a different thread in the same channel. Expected: it also shows the remaining countdown.
  7. Force-stop and reopen the app while a countdown is active. Expected: the countdown resumes.

Automated: unit tests in ChannelStateImplLastSentMessageDateTest and the additions to ChannelStateLegacyImplTest cover a thread reply updating lastSentMessageDate (and not updating it for another user's reply), in both state implementations, plus the channel-wide and cross-refresh cases.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed tracking of the current user's most recent message timestamp to now properly include thread-only replies, ensuring the last sent message date reflects both channel messages and threaded conversations.
  • Tests

    • Added comprehensive test coverage for message timestamp tracking with thread replies.

The composer cooldown is driven by ChannelState.lastSentMessageDate, which
was derived only from the channel message list. Thread-only replies are
excluded from that list, so sending a thread reply during slow mode did not
start the countdown and the next send was rejected by the server.

Track the current user's most recent thread-only reply at the channel-state
write entry points and expose lastSentMessageDate as the later of the channel
message date and that thread-reply date. The cooldown is now channel-wide and
consistent between channel mode and thread mode, matching iOS. Applied to both
ChannelStateImpl and the legacy ChannelStateLegacyImpl.
@andremion andremion added the pr:bug Bug fix label Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@andremion

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

ChannelStateImpl and ChannelStateLegacyImpl each gain a new _lastSentThreadReplyDate StateFlow that tracks the current user's most recent thread-only reply date independently of the visible message list. lastSentMessageDate is refactored to combine this value with the existing channel-message date using a new latestOf helper, selecting whichever is more recent. New helper functions in LastSentMessageDate.kt support date extraction and reduction across message collections.

Changes

Thread-only reply date tracking in channel state

Layer / File(s) Summary
Thread reply date helper utilities
...internal/LastSentMessageDate.kt
Adds latestOf(Date?, Date?), Message.ownThreadReplyDate(currentUserId), and Collection<Message>.latestOwnThreadReplyDate(currentUserId) as internal helpers for extracting and comparing thread-only reply timestamps.
ChannelStateImpl: thread reply tracking
...internal/ChannelStateImpl.kt
Adds _lastSentThreadReplyDate StateFlow, splits lastSentMessageDate into lastSentChannelMessageDate combined via latestOf, and updates setMessages, upsertMessage, and upsertMessages to advance the thread-reply timestamp monotonically.
ChannelStateLegacyImpl: thread reply tracking
...internal/ChannelStateLegacyImpl.kt
Mirrors the same changes in the legacy implementation: new _lastSentThreadReplyDate field, refactored lastSentMessageDate derivation, and monotonic advancement in upsertMessages and setMessages.
Tests
...ChannelStateImplLastSentMessageDateTest.kt, ...ChannelStateLegacyImplTest.kt
New test class and additional test cases covering thread-reply-only updates, user-ownership rules, timestamp precedence between thread replies and channel messages, and persistence after message list refresh.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

pr:improvement

Poem

🐇 A thread reply hops by, unseen in the list,
But its timestamp is tracked — it won't be dismissed!
latestOf two dates, we pick the most late,
lastSentMessageDate now reflects every trait.
No reply shall be lost, says the bunny with glee! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: counting thread replies toward slow mode cooldown, which is the core objective of this PR.
Description check ✅ Passed The description is comprehensive and follows the template structure with Goal, Implementation, and Testing sections filled out with substantial detail. All critical sections are complete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/and-1230-thread-replies-composer-cooldown

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImpl.kt`:
- Around line 103-107: The `_lastSentThreadReplyDate` MutableStateFlow field
contributes to cooldown state tracking but is not being reset in the `destroy()`
method of the `ChannelStateImpl` class, which can leave stale cooldown
timestamps persisting across teardown and reuse cycles. Locate the `destroy()`
method and add a call to reset `_lastSentThreadReplyDate` to null, similar to
how other state fields are cleared during destruction, to ensure clean state
management when the channel is torn down or reused.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5233e7f1-ed09-4787-80e0-afb5e90e7dcc

📥 Commits

Reviewing files that changed from the base of the PR and between dcb8199 and 751026b.

📒 Files selected for processing (5)
  • 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/ChannelStateLegacyImpl.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/LastSentMessageDate.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplLastSentMessageDateTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateLegacyImplTest.kt

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.90 MB 5.90 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.14 MB 11.14 MB 0.00 MB 🟢
stream-chat-android-compose 12.59 MB 12.59 MB 0.00 MB 🟢

ChannelStateImpl.destroy() cleared every message-derived flow except the new
_lastSentThreadReplyDate, so lastSentMessageDate would still emit a stale
thread-reply date after teardown. Reset it alongside the message list.
@sonarqubecloud

Copy link
Copy Markdown

@andremion andremion marked this pull request as ready for review June 18, 2026 15:01
@andremion andremion requested a review from a team as a code owner June 18, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant