Skip to content

Comments

Channel.lastMessageAt as a computed property derived from the currently loaded messages#6118

Merged
aleksandar-apostolov merged 5 commits intodevelopfrom
feature/AND-1035_improve_channel_lastmessageat_calculation
Feb 4, 2026
Merged

Channel.lastMessageAt as a computed property derived from the currently loaded messages#6118
aleksandar-apostolov merged 5 commits intodevelopfrom
feature/AND-1035_improve_channel_lastmessageat_calculation

Conversation

@VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Feb 2, 2026

🎯 Goal

We are currently calculating Channel.lastMessageAt as a computed property derived from the currently loaded messages. This can be wrong, if for example we have a deleted message as the last one in the channel, and we only fetch the last message from the channel.

In this PR, this logic is reworked so that the lastMessageAt is initially populated from the QueryChannels call, and later updated by the relevant events (message.new, notification.message_new). This also aligns the logic with iOS.

🛠 Implementation details

  • Convert lastMessageAt from a computed property (derived from messages list) to a stored property on Channel and ChannelData
  • Add calculateNewLastMessageAt utility function with proper filtering logic that skips shadowed messages, system messages (when configured), and thread replies not shown in channel
  • Update ChannelStateLogic, ChannelEventHandler, and EventHandlerSequential to properly calculate and update lastMessageAt when messages change
  • Update domain mapping and repository layers to persist lastMessageAt
  • Add comprehensive unit tests for the new calculation logic

🎨 UI Changes

Observe the Other Group channel

Before After
before.mp4
after.mp4

🧪 Testing

  • Unit tests added for calculateNewLastMessageAt function
  • Existing tests updated to reflect the new property structure
  • Manual testing: verify that channel list sorting by lastMessageAt works correctly

Summary by CodeRabbit

  • Improvements

    • Channels now maintain an explicit last message timestamp that updates whenever new messages are sent or channels are modified, ensuring accurate sorting and ordering in channel lists.
  • Tests

    • Added comprehensive test coverage for last message timestamp calculations, including scenarios with system messages, thread replies, and timestamp edge cases.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.26 MB 5.26 MB 0.00 MB 🟢
stream-chat-android-offline 5.48 MB 5.48 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.63 MB 10.63 MB 0.00 MB 🟢
stream-chat-android-compose 12.84 MB 12.84 MB 0.00 MB 🟢

@VelikovPetar VelikovPetar marked this pull request as ready for review February 2, 2026 12:10
@VelikovPetar VelikovPetar requested a review from a team as a code owner February 2, 2026 12:10
@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Walkthrough

The PR converts Channel.lastMessageAt from a computed property into an explicitly stored field. It adds lastMessageAt as a parameter to the Channel and ChannelData constructors, updates mappings across the client and offline modules, integrates event handling to update the field when new messages arrive, and introduces a utility function to calculate the appropriate timestamp value.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md, stream-chat-android-core/api/stream-chat-android-core.api
Changelog entry and public API surface updates reflecting new lastMessageAt field addition to Channel and ChannelData constructors, copy methods, and builder methods.
Core Models
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Channel.kt, stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChannelData.kt
Added lastMessageAt: Date? as stored property to both Channel and ChannelData. Updated builders, copy constructors, and conversion methods to propagate the new field.
Mapping Layer
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt, stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/internal/ChannelMapper.kt
Updated DTO-to-domain and entity-to-domain mapping transformations to populate lastMessageAt from source objects during Channel instantiation.
Event Handling & State Logic
stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelEventHandler.kt, stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelStateLogic.kt, stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/listener/internal/SendAttachmentListenerState.kt
Added event handling to update lastMessageAt when new messages arrive or channels are truncated. New updateLastMessageAt() method in ChannelStateLogic computes and updates the timestamp.
Utilities
stream-chat-android-state/src/main/java/io/getstream/chat/android/state/utils/internal/ChannelUtils.kt, stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/EventHandlerSequential.kt
New calculateNewLastMessageAt() utility function determines the appropriate timestamp considering message shadowing, system message skip flag, and thread visibility.
Test Fixtures
stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt
Extended randomChannel() fixture to include optional lastMessageAt parameter for test data generation.
Test Coverage
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt, stream-chat-android-offline/src/test/java/io/getstream/chat/android/offline/repository/domain/channel/internal/ChannelMapperTest.kt, stream-chat-android-offline/src/test/java/io/getstream/chat/android/offline/PaginationTest.kt, stream-chat-android-state/src/test/java/io/getstream/chat/android/state/...
Added new mapping test for lastMessageAt, updated event arguments and pagination test data to use lastMessageAt directly instead of embedding messages, added comprehensive unit tests for calculateNewLastMessageAt() covering edge cases (shadowed messages, system messages, threads, null dates), and integration tests for updateLastMessageAt().

Sequence Diagram

sequenceDiagram
    actor EventSource as Message Event<br/>(NewMessageEvent)
    participant EH as ChannelEventHandler
    participant CSL as ChannelStateLogic
    participant Utils as ChannelUtils
    participant CM as ChannelData

    EventSource->>EH: Handle new message event
    EH->>CSL: updateLastMessageAt(message)
    CSL->>Utils: calculateNewLastMessageAt(message, currentLastMessageAt, skipFlag)
    Utils-->>CSL: Date? (computed timestamp)
    CSL->>CM: Update channelData.lastMessageAt
    CM-->>CSL: ChannelData updated
    CSL-->>EH: Complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • gpunto
  • andremion
  • aleksandar-apostolov

Poem

🐰 Hops of joy for timestamp cheer,
No more computed, stored right here!
When messages flow, lastMessageAt glows,
Channel state logic smoothly flows!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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 accurately describes the main change: converting Channel.lastMessageAt from a computed property to a stored property with improved calculation logic.
Description check ✅ Passed The description follows the template structure with Goal, Implementation details, UI Changes, and Testing sections; all required information is present and well-documented.

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

✨ 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/AND-1035_improve_channel_lastmessageat_calculation

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

Caution

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

⚠️ Outside diff range comments (2)
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChannelData.kt (1)

216-233: ⚠️ Potential issue | 🟡 Minor

Preserve existing lastMessageAt when event payload omits it.

Line 232 directly assigns that.lastMessageAt without null-checking, which overwrites a valid value if the event omits the field. This breaks channel ordering that depends on lastMessageAt. The function already handles this correctly for messageCount (line 231) using the Elvis operator—apply the same pattern here:

Suggested fix
-        lastMessageAt = that.lastMessageAt,
+        lastMessageAt = that.lastMessageAt ?: this.lastMessageAt,
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Channel.kt (1)

361-380: ⚠️ Potential issue | 🟡 Minor

Preserve existing lastMessageAt when merge payload is null.

Both messageCount and lastMessageAt are nullable fields, but messageCount already uses the Elvis operator to preserve the existing value when null (line 378). The lastMessageAt field should follow the same pattern to prevent data loss when event payloads omit this field.

💡 Suggested adjustment
-        lastMessageAt = that.lastMessageAt,
+        lastMessageAt = that.lastMessageAt ?: this.lastMessageAt,
🤖 Fix all issues with AI agents
In
`@stream-chat-android-state/src/main/java/io/getstream/chat/android/state/utils/internal/ChannelUtils.kt`:
- Around line 45-63: The calculateNewLastMessageAt function should prefer server
timestamps to avoid under-ordering: replace the current messageDate =
message.createdLocallyAt ?: message.createdAt ?: return currentLastMessageAt
with logic that uses message.createdAt when available (or the max of
message.createdAt and message.createdLocallyAt when both exist) so the chosen
messageDate uses the server timestamp if present; keep falling back to
createdLocallyAt only if createdAt is null and preserve the existing checks
around shadowed, system, and thread messages.
🧹 Nitpick comments (1)
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Channel.kt (1)

209-350: Add KDoc for the new builder setter.

withLastMessageAt is a new public API surface; please add a short KDoc to keep API docs consistent.

✍️ Suggested addition
+        /**
+         * Sets the last message timestamp for the channel.
+         */
         public fun withLastMessageAt(lastMessageAt: Date?): Builder = apply { this.lastMessageAt = lastMessageAt }

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

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 3, 2026

@aleksandar-apostolov aleksandar-apostolov merged commit 40119da into develop Feb 4, 2026
14 checks passed
@aleksandar-apostolov aleksandar-apostolov deleted the feature/AND-1035_improve_channel_lastmessageat_calculation branch February 4, 2026 08:11
@aleksandar-apostolov aleksandar-apostolov changed the title Improve Channel lastMessageAt calculation Channel.lastMessageAt as a computed property derived from the currently loaded messages Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants