Skip to content

Redesign Thread List screen in Compose#6196

Open
VelikovPetar wants to merge 16 commits intov7from
feature/AND-1097_redesign_thread_list
Open

Redesign Thread List screen in Compose#6196
VelikovPetar wants to merge 16 commits intov7from
feature/AND-1097_redesign_thread_list

Conversation

@VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Feb 27, 2026

Goal

Redesign the Thread List screen in the Compose UI kit to match the updated Figma design system specifications. The new design improves visual hierarchy, introduces skeleton loading, adds error/loading banners, and surfaces participant recency through a new lastThreadMessageAt field on ThreadParticipant.

Implementation

Model layer (stream-chat-android-client, stream-chat-android-core)

  • Added ThreadParticipant.lastThreadMessageAt — a nullable Date parsed from the API's last_thread_message_at field, persisted in Room via ThreadParticipantEntity.
  • Thread participants are now sorted by most-recent reply (sortedByLastReply()), so the avatar stack in each thread item shows the most active participants first.
  • ThreadListState gains a loadingError: Boolean field to distinguish failed loads from empty states.
  • QueryThreadsLogic / QueryThreadsMutableState propagate the new error state.

Compose UI (stream-chat-android-compose)

  • ThreadItem — completely redesigned: horizontal Row layout with parent-message author avatar, channel title, parent message preview, a reply footer (participant avatar stack, reply count, relative timestamp), and an unread badge. Replaced the previous vertical Column layout with icon + "replied to:" prefix. Removed the slot-based titleContent/replyToContent/unreadCountContent/latestReplyContent parameters in favour of a simpler, opinionated design.
  • ThreadListLoadingItem (new) — skeleton shimmer placeholder that mirrors ThreadItem's layout. Reuses ShimmerProgressIndicator (which now accepts optional baseColor/highlightColor params).
  • ThreadListBanner (new, replaces UnreadThreadsBanner) — supports three states: UnreadThreads, Loading, and Error, driven by a sealed ThreadListBannerState interface.
  • ThreadTimestampFormatter (new) — relative timestamp formatting: "Just now", "Today at …", "Yesterday at …", " at …", " at …".
  • DefaultThreadListLoadingContent — replaced LoadingIndicator (spinner) with a LazyColumn of 7 skeleton ThreadListLoadingItems.
  • DefaultThreadListEmptyContent — updated icon, copy ("Reply to a message to start a thread"), and sizing to match Figma.
  • ChatComponentFactory — removed ThreadListItemTitle, ThreadListItemReplyToContent, ThreadListItemUnreadCountContent, ThreadListItemLatestReplyContent slot methods; added ThreadListBanner.
  • New string resources for banner states, reply counts, and relative timestamp labels.
  • New vector drawable stream_compose_ic_exclamation_circle for error banner.

Tests

  • New ThreadTimestampFormatterTest with comprehensive coverage of all formatting branches.
  • ThreadListBannerTest (renamed from UnreadThreadsBannerTest) with Paparazzi snapshots for all three banner states.
  • Updated ThreadListTest Paparazzi snapshots for the new loading/empty/loaded designs.
  • Updated ChatsScreenTest — thread loading assertion uses test tag instead of progress bar semantics.
  • Updated client-layer tests (DomainMappingTest, ThreadMapperTest, QueryThreadsLogicTest, QueryThreadsStateLogicTest, QueryThreadsMutableStateTest) for the new lastThreadMessageAt field and error state.
  • Room DB version bump for the new ThreadParticipantEntity column.

🎨 UI Changes

Loading Error / reload
thread-list.mp4
thread-list-error.mp4

Testing

  • Unit tests: ThreadTimestampFormatterTest covers all timestamp formatting branches (just now, today, yesterday, day-of-week, older dates). Client-layer tests updated for lastThreadMessageAt parsing, mapping, and state propagation.
  • Snapshot tests: Paparazzi snapshots updated/added for ThreadItem, ThreadListBanner (all 3 states), ThreadList (loading, empty, loaded, loading-more, unread banner). Run ./gradlew :stream-chat-android-compose:verifyPaparazziDebug to verify.
  • Integration test: ChatsScreenTest verifies the skeleton loading content renders correctly in threads mode.
  • Manual testing: Open the Compose sample → navigate to Threads tab. Verify skeleton loading on initial load, thread items render with avatar + channel name + message preview + participant stack + reply count + relative timestamp + unread badge. Verify banner states by triggering refresh (pull or unseen count), error (disconnect network), and loading (force refresh with existing threads).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a new banner in the thread list to display loading status, error state, and unread thread count with improved visual feedback.
    • Thread participants are now sorted by most recent activity.
  • Bug Fixes

    • Enhanced error handling and feedback when thread loading fails.
    • Improved deleted message preview formatting.
  • Improvements

    • Redesigned thread list and thread item UI for better clarity and usability.

# Conflicts:
#	stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png
#	stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png
#	stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png
#	stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png
@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 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.

@VelikovPetar VelikovPetar changed the title Thread List re-desing Redesign Thread List screen in Compose UI kit to match Figma specifications Feb 27, 2026
@VelikovPetar VelikovPetar added pr:breaking-change Breaking change pr:new-feature New feature labels Feb 27, 2026
@VelikovPetar VelikovPetar changed the title Redesign Thread List screen in Compose UI kit to match Figma specifications Redesign Thread List screen in Compose Feb 27, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.69 MB 0.44 MB 🟡
stream-chat-android-ui-components 10.60 MB 10.97 MB 0.37 MB 🟡
stream-chat-android-compose 12.81 MB 11.96 MB -0.85 MB 🚀

@VelikovPetar VelikovPetar marked this pull request as ready for review February 27, 2026 15:07
@VelikovPetar VelikovPetar requested a review from a team as a code owner February 27, 2026 15:07
@sonarqubecloud
Copy link

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Walkthrough

This pull request enhances thread query functionality by adding a loadingError state to track initial/refresh load failures, introduces timestamp-based sorting of thread participants via a new lastThreadMessageAt field, refactors the thread list UI with a generalized banner system, and updates the database schema and related mappings accordingly.

Changes

Cohort / File(s) Summary
Thread Query State API
stream-chat-android-client/api/stream-chat-android-client.api, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryThreadsState.kt
Added public loadingError: StateFlow<Boolean> property to QueryThreadsState interface to expose loading error state for initial/refresh operations.
Thread Query Logic & State Mutation
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsLogic.kt, QueryThreadsStateLogic.kt, QueryThreadsMutableState.kt
Modified query thread request/result handling to clear loading errors on new queries and set error state on failures; added setLoadingError() method to state mutation API; introduced _loadingError backing flow with public accessor.
Thread Participant Timestamp Tracking
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/ThreadDtos.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.kt, ThreadParticipantEntity.kt
Added lastThreadMessageAt: Date? field to DownstreamThreadParticipantDto and ThreadParticipantEntity; introduced sortedByLastReply() extension to sort participants by last message timestamp in descending order; updated participant upsert logic to initialize timestamp.
Domain Mapping Updates
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/threads/internal/ThreadMapper.kt
Updated DTO-to-domain mappings to apply sortedByLastReply() to thread participants and propagate lastThreadMessageAt field across mapping layers; bumped database version from 100 to 101.
Core Domain Model
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ThreadParticipant.kt, stream-chat-android-core/api/stream-chat-android-core.api
Added lastThreadMessageAt: Date? property to ThreadParticipant data class with corresponding copy, component, and getter methods.
UI Common State
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/threads/ThreadListState.kt, ThreadListController.kt
Added loadingError: Boolean = false field to ThreadListState data class; updated controller to pass loading error from query state to UI state.
Thread List Banner Refactor
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListBanner.kt, ThreadList.kt, ChatComponentFactory.kt
Introduced new ThreadListBanner composable with sealed ThreadListBannerState (UnreadThreads, Loading, Error); replaced old UnreadThreadsBanner and per-section slot props with unified banner parameter throughout ThreadList API.
Thread Item UI Rebuild
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt
Removed public slot parameters (titleContent, replyToContent, unreadCountContent, latestReplyContent); refactored internal layout with modular sub-composables (ThreadItemTitle, ThreadItemParentMessage, ThreadItemContentContainer, ThreadRepliesFooter, ThreadItemParticipants, etc.); added bottom border and standardized sizing.
Thread List Loading UI
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListLoadingItem.kt, ShimmerProgressIndicator.kt
Added new ThreadListLoadingItem composable for skeleton loading placeholder; enhanced ShimmerProgressIndicator with customizable baseColor and highlightColor parameters.
Thread Timestamp Formatting
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatter.kt
Introduced new ThreadTimestampFormatter object to format thread timestamps with locale-aware, proximity-based display rules (just now, today, yesterday, day-of-week, full date).
Message Preview & Icon Updates
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt, MessagePreviewIconFactory.kt, MessageUtils.kt
Simplified deleted message preview to always show sender name with DELETED icon; added DELETED inline icon mapping; changed own message prefix logic in DMs to consistently display "You" instead of null.
UI Resources & Drawable Updates
stream-chat-android-compose/src/main/res/values/strings.xml, stream_compose_ic_threads_empty.xml, stream_compose_ic_exclamation_circle.xml
Added banner loading/error strings and thread timestamp format strings; updated thread list empty icon from 137×136dp to 32×32dp with stroke styling; added new exclamation-circle icon drawable.
Test Infrastructure & Helpers
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/internal/offline/Mother.kt, stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt, stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewThreadData.kt
Added randomDownstreamThreadParticipantDto(), randomThreadParticipantEntity() helpers; updated randomThreadParticipant() signature to include lastThreadMessageAt parameter; extended PreviewThreadData with participant3 and thread3 entities.
Test Updates Across Modules
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt, ThreadExtensionsTests.kt, ThreadMapperTest.kt, QueryThreadsLogicTest.kt, QueryThreadsStateLogicTest.kt, QueryThreadsMutableStateTest.kt, ThreadDtoTestData.kt
Updated tests to use new helper functions; modified assertions to verify loadingError state transitions; adjusted test data to include lastThreadMessageAt timestamps; refactored test expectations around participant sorting and reload behavior.
Compose UI Tests
stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListBannerTest.kt, ThreadListTest.kt, ThreadTimestampFormatterTest.kt, ChatsScreenTest.kt
Renamed/refactored banner tests from UnreadThreadsBannerTest to ThreadListBannerTest with new state-based API; updated ThreadList test invocations to use onBannerClick; added comprehensive ThreadTimestampFormatterTest covering all time proximity scenarios; replaced progress bar assertion with tag-based check.
UI Common State Tests
stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt
Added assertions verifying loadingError: false in initial state and test scenarios; introduced test for loadingError state propagation from query state to list state.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant QueryThreadsLogic
    participant QueryThreadsState
    participant QueryThreadsMutableState
    participant Network
    
    Client->>QueryThreadsLogic: onQueryThreadsRequest (not pagination)
    QueryThreadsLogic->>QueryThreadsMutableState: setLoadingError(false)
    QueryThreadsMutableState->>QueryThreadsState: Update loadingError StateFlow
    
    alt Unseen threads exist
        QueryThreadsLogic->>QueryThreadsMutableState: clearUnseenThreadIds()
    else No unseen threads and no loaded threads
        QueryThreadsLogic->>Network: queryThreadsOffline()
    end
    
    Network-->>QueryThreadsLogic: Result (success/failure)
    
    alt Query failed
        QueryThreadsLogic->>QueryThreadsMutableState: setLoadingError(true)
        QueryThreadsMutableState->>QueryThreadsState: Update loadingError = true
    else Query succeeded
        QueryThreadsLogic->>QueryThreadsMutableState: Update thread list
        QueryThreadsLogic->>QueryThreadsMutableState: setLoadingError(false)
    end
    
    QueryThreadsState-->>Client: Emit new state with loadingError status
Loading
sequenceDiagram
    participant ThreadList as ThreadList Composable
    participant ThreadListViewModel
    participant QueryThreadsState
    participant ThreadListBanner
    participant Threads
    
    ThreadList->>ThreadListViewModel: Load initial state
    ThreadListViewModel->>QueryThreadsState: Collect state
    
    alt Loading state
        QueryThreadsState-->>ThreadList: bannerState = ThreadListBannerState.Loading
        ThreadList->>ThreadListBanner: Render(Loading state)
        ThreadList->>Threads: Show DefaultThreadListLoadingContent
    else Error state (loadingError = true)
        QueryThreadsState-->>ThreadList: bannerState = ThreadListBannerState.Error
        ThreadList->>ThreadListBanner: Render(Error state)
        ThreadList->>Threads: Show previous threads or empty
    else Success with unseenCount > 0
        QueryThreadsState-->>ThreadList: bannerState = ThreadListBannerState.UnreadThreads(count)
        ThreadList->>ThreadListBanner: Render(UnreadThreads state)
        ThreadList->>Threads: Show thread list
    else Success with no unseen
        QueryThreadsState-->>ThreadList: bannerState = null
        ThreadList->>Threads: Show thread list
    end
    
    ThreadListBanner->>ThreadList: onBannerClick
    ThreadList->>ThreadListViewModel: load()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Threads now sorted by their reply time's grace,
Errors tracked in state, giving feedback its place,
Banners bloom with loading and care,
UI components dance, refactored fair,
Timestamps guide, participants wear their best face! 🎀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.38% 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 'Redesign Thread List screen in Compose' accurately describes the primary change—a comprehensive redesign of the thread list UI component in the Compose module.
Description check ✅ Passed The description comprehensively covers all required sections: Goal explains the redesign rationale, Implementation details model/client/compose changes, UI Changes shows videos, Testing explains unit/snapshot/integration/manual approaches, and all contributor and reviewer checklist items are present.

✏️ 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 feature/AND-1097_redesign_thread_list

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

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-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.kt (1)

88-98: ⚠️ Potential issue | 🟠 Major

Update participant recency on every inserted reply, not only first-time participants.

threadParticipants recency is refreshed only when isInsert is true, so existing participants can keep stale order after new replies. Also, Line 92 should use optimistic-first timestamp selection.

💡 Proposed fix
-    val threadParticipants = if (isInsert) {
-        upsertThreadParticipantInList(
-            newParticipant = ThreadParticipant(
-                user = reply.user,
-                lastThreadMessageAt = reply.createdAt ?: reply.createdLocallyAt,
-            ),
-            participants = this.threadParticipants,
-        )
-    } else {
-        this.threadParticipants
-    }
+    val threadParticipants = upsertThreadParticipantInList(
+        newParticipant = ThreadParticipant(
+            user = reply.user,
+            lastThreadMessageAt = reply.getCreatedAtOrNull(),
+        ),
+        participants = this.threadParticipants,
+    )
Based on learnings: In the Stream Chat Android SDK, `createdLocallyAt` should be prioritized over `createdAt` so optimistic updates immediately affect ordering.

Also applies to: 90-93

🤖 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/extensions/internal/Thread.kt`
around lines 88 - 98, threadParticipants recency is only updated when isInsert
is true and it uses createdAt first; change it so upsertThreadParticipantInList
is called for every new reply to refresh existing participant order and use
optimistic-first timestamp selection (createdLocallyAt ?: createdAt) when
building the ThreadParticipant; update the block that creates ThreadParticipant
(and the assignment to threadParticipants) to always call
upsertThreadParticipantInList with ThreadParticipant(user = reply.user,
lastThreadMessageAt = reply.createdLocallyAt ?: reply.createdAt) instead of only
on isInsert.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadList.kt (1)

184-193: ⚠️ Potential issue | 🟡 Minor

Avoid reapplying the same modifier to Threads.

Line 184 applies the caller's modifier to the content container, and line 192 forwards it again to Threads (line 234), which double-applies padding, background, and test tags.

♻️ Proposed fix
                     else -> Threads(
                         threads = state.threads,
                         isLoading = state.isLoading,
                         isLoadingMore = state.isLoadingMore,
-                        modifier = modifier,
+                        modifier = Modifier,
                         onLoadMore = onLoadMore,
                         itemContent = itemContent,
                         loadingMoreContent = loadingMoreContent,
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadList.kt`
around lines 184 - 193, The Box already applies the incoming modifier (modifier)
with padding, but the same modifier is being forwarded again into Threads
causing duplicate padding/background/test tags; update the Threads call to pass
Modifier instead of the incoming modifier (or a cleaned modifier) so Threads
receives its own composed modifier (e.g., use Modifier or Modifier.fillMaxSize()
as appropriate) and keep the incoming modifier only on Box; adjust the Threads
invocation in ThreadList (where state.threads/isLoading checks occur) to remove
forwarding of the original modifier while preserving other parameters like
threads, isLoading, isLoadingMore, onLoadMore.
🧹 Nitpick comments (7)
stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewThreadData.kt (1)

30-31: Consider documenting the suppression annotation.

Per coding guidelines, suppressions should be documented. A brief inline comment explaining why magic numbers are acceptable here (preview data with fixed timestamps for UI demos) would satisfy this requirement.

+// Magic numbers are intentional for fixed preview timestamps demonstrating participant ordering
 `@Suppress`("MagicNumber")
 public object PreviewThreadData {

As per coding guidelines: "Use @OptIn annotations explicitly; avoid suppressions unless documented."

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

In
`@stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewThreadData.kt`
around lines 30 - 31, Add a brief inline comment immediately above the
`@Suppress`("MagicNumber") on the PreviewThreadData object explaining why magic
numbers are acceptable here (these values are fixed timestamps/constants used
solely for preview/demo UI data), and note that this suppression is intentional
per coding guidelines; reference the PreviewThreadData object and the
`@Suppress`("MagicNumber") annotation so reviewers can see the justification
without removing the suppression.
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/ThreadExtensionsTests.kt (1)

51-52: Use explicit lastThreadMessageAt values in test fixtures to avoid randomness.

Lines 51-52 currently inherit random lastThreadMessageAt, which can create unnecessary variability now that participant recency affects behavior. Prefer fixed timestamps in this test setup.

Proposed deterministic fixture update
-    private val threadParticipant1 = randomThreadParticipant(user = user1)
-    private val threadParticipant2 = randomThreadParticipant(user = user2)
+    private val threadParticipant1 = randomThreadParticipant(
+        user = user1,
+        lastThreadMessageAt = now,
+    )
+    private val threadParticipant2 = randomThreadParticipant(
+        user = user2,
+        lastThreadMessageAt = Date(now.time - 1_000),
+    )
Based on learnings: Applies to **/src/test/**/*.kt : Use deterministic tests with `runTest` + virtual time for concurrency-sensitive logic (uploads, sync, message state).
🤖 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/extensions/internal/ThreadExtensionsTests.kt`
around lines 51 - 52, The test fixtures use randomThreadParticipant to build
threadParticipant1 and threadParticipant2 which leaves lastThreadMessageAt
randomized; change the test setup to pass explicit deterministic timestamps into
randomThreadParticipant (e.g., fixed Instant/Date values) for the
lastThreadMessageAt field for both threadParticipant1 and threadParticipant2 so
participant recency is stable; locate usages of randomThreadParticipant in
ThreadExtensionsTests.kt and update its call sites (threadParticipant1,
threadParticipant2) to provide a fixed lastThreadMessageAt parameter (and adjust
any helper signature if needed) to remove randomness from the tests.
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryThreadsState.kt (1)

53-54: Add thread-expectation details to the new public loadingError KDoc.

Line 53 documents state semantics well, but the API doc should also state thread/collection expectations explicitly for this StateFlow.

Suggested KDoc refinement
-    /** Indicates that the last initial or refresh load failed. Not set for pagination failures. */
+    /**
+     * Indicates that the last initial or refresh load failed. Not set for pagination failures.
+     *
+     * Threading: safe to collect from any coroutine context.
+     */
     public val loadingError: StateFlow<Boolean>
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/api/state/QueryThreadsState.kt`
around lines 53 - 54, Update the public KDoc for QueryThreadsState.loadingError
to include thread/collection expectations and state notes: state explicitly
emits whether the most recent initial or refresh load failed (and does not
reflect pagination failures), is a hot StateFlow that can be safely collected
from any thread/coroutine context (UI or background), and will replay the latest
Boolean to new collectors; reference QueryThreadsState.loadingError in the
comment and keep the existing semantic note about initial/refresh vs pagination
failures.
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ThreadParticipant.kt (1)

31-31: Consider defaulting lastThreadMessageAt to null for smoother API migration.

The field is nullable; a default reduces Kotlin call-site churn when constructing ThreadParticipant.

Proposed tweak
 public data class ThreadParticipant(
     override val user: User,
-    val lastThreadMessageAt: Date?,
+    val lastThreadMessageAt: Date? = null,
 ) : UserEntity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ThreadParticipant.kt`
at line 31, The ThreadParticipant data class property lastThreadMessageAt should
have a default value of null to ease API migration; change the declaration of
lastThreadMessageAt in ThreadParticipant to provide a default (= null) and then
update any constructors, factory methods or call sites (builders/tests) that
explicitly pass this parameter so they can omit it safely.
stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt (1)

1044-1050: Add KDoc for the updated public fixture helper.

Line 1046 introduces a new public parameter (lastThreadMessageAt) but the function is undocumented.

📝 Proposed fix
+/**
+ * Creates a random [ThreadParticipant] fixture.
+ *
+ * `@param` user The participant user.
+ * `@param` lastThreadMessageAt Timestamp of the participant's latest thread reply, if available.
+ */
 public fun randomThreadParticipant(
     user: User = randomUser(),
     lastThreadMessageAt: Date? = randomDateOrNull(),
 ): ThreadParticipant = ThreadParticipant(

As per coding guidelines **/*.kt: 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-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt`
around lines 1044 - 1050, Add KDoc for the public fixture helper function
randomThreadParticipant(user: User = randomUser(), lastThreadMessageAt: Date? =
randomDateOrNull()) describing what the function returns (a ThreadParticipant),
documenting the parameters (user and nullable lastThreadMessageAt), and noting
any thread-related expectations/state (e.g., that lastThreadMessageAt represents
the timestamp of the last message in the thread or may be null) and that this is
a test fixture helper; place the KDoc immediately above the
randomThreadParticipant declaration.
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsStateLogicTest.kt (1)

476-515: Add a test case verifying participant re-ordering when an existing non-first participant sends a reply.

The current test only verifies updating an existing first-position participant. However, the upsertReply implementation only re-sorts participants when a NEW participant is added (isInsert = true). When an existing participant sends a reply (isInsert = false), they retain their position in the list without re-sorting by lastThreadMessageAt.

To verify the expected behavior: if usrId1 (not first) sends a reply after usrId2, should usrId1 move to the first position due to the updated lastThreadMessageAt? Currently, the code would not re-sort them. A test case covering this scenario would clarify whether re-sorting should happen in state logic or be deferred to the UI layer (via sortedByLastReply()).

🤖 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/logic/querythreads/internal/QueryThreadsStateLogicTest.kt`
around lines 476 - 515, Add a test that verifies participant re-ordering when an
existing non-first participant sends a reply: create a mutableState mock with
threadList where Thread.threadParticipants has usrId2 as first and usrId1
second, instantiate QueryThreadsStateLogic and call upsertReply with a reply
from User(id="usrId1"), then verify QueryThreadsMutableState.upsertThreads is
called with a Thread whose threadParticipants list has been re-sorted so usrId1
is now first (compare using ThreadParticipant.user.id and lastThreadMessageAt)
to assert participants are moved to reflect the updated lastThreadMessageAt;
reference QueryThreadsStateLogic.upsertReply,
QueryThreadsMutableState.upsertThreads, ThreadParticipant and threadList to
locate the code under test.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt (1)

214-216: Consider keying timestamp cache with context as well.

ThreadTimestampFormatter.format(...) is locale/resource-sensitive; caching only by updatedAt can retain stale localized text in configuration changes.

♻️ Proposed refactor
-    val timestamp = remember(updatedAt) {
+    val timestamp = remember(updatedAt, context) {
         ThreadTimestampFormatter.format(updatedAt, context)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt`
around lines 214 - 216, The cached timestamp in ThreadItem (`val timestamp =
remember(updatedAt) { ThreadTimestampFormatter.format(updatedAt, context) }`) is
only keyed by `updatedAt`, so locale/config changes can leave stale localized
text; update the remember key to also include the current context (or its
configuration) so the cache invalidates on configuration/locale changes (e.g.,
change the remember call to use `updatedAt` and
`context`/`context.resources.configuration` as keys) while keeping the
formatting call to ThreadTimestampFormatter.format(updatedAt, context).
🤖 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/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt`:
- Around line 747-760: The test currently feeds threadParticipants in
newest-first order so it doesn't validate sorting; update it to provide
participants in non-sorted order (e.g., put participant2Dto before
participant1Dto in the threadParticipants list) and then assert that the mapper
returns participants sorted by lastThreadMessageAt descending. Change the
downstreamThreadDto construction to list(threadParticipants =
listOf(participant2Dto, participant1Dto)) and keep the assertion that the mapper
output orders by newest-first (use the same participant1Dto/participant2Dto
identifiers and the mapper under test).

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt`:
- Around line 154-156: The remember call that computes text uses message and
currentUser as keys but omits isOneToOneChannel and formatter, causing stale
previews when channel context or formatter changes; update the remember
invocation for text to include isOneToOneChannel and formatter in its keys so
formatter.formatMessagePreview(message, currentUser, isOneToOneChannel)
recomputes whenever the channel type or formatter reference changes.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatter.kt`:
- Around line 79-80: The current formatTime(Date) uses a hardcoded
SimpleDateFormat("HH:mm") which forces 24-hour output; change it to use
android.text.format.DateFormat.getTimeFormat(...) so the time respects the
device 12/24 setting. Update formatTime to obtain a java.text.DateFormat from
DateFormat.getTimeFormat(context) (or receive a Context/Resources/Locale-aware
formatter via the ThreadTimestampFormatter constructor) and call its
format(date); replace references to the old formatTime(Date) accordingly so
timestamps follow the device setting.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatterTest.kt`:
- Around line 151-163: The assertions hardcode years; instead build the expected
string dynamically in ThreadTimestampFormatterTest by calling
DateUtils.formatDateTime(...) for the date portion using the flags
FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH and for the time portion using
FORMAT_SHOW_TIME (or a single call combining both flags), passing the timestamp
from nowOffset(...) and the test Context, then assert that format(date) equals
the concatenation (e.g., "<formattedDate> at <formattedTime>"); update the two
tests referencing format(date) to compute expected via DateUtils.formatDateTime
rather than hardcoded literals.

---

Outside diff comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.kt`:
- Around line 88-98: threadParticipants recency is only updated when isInsert is
true and it uses createdAt first; change it so upsertThreadParticipantInList is
called for every new reply to refresh existing participant order and use
optimistic-first timestamp selection (createdLocallyAt ?: createdAt) when
building the ThreadParticipant; update the block that creates ThreadParticipant
(and the assignment to threadParticipants) to always call
upsertThreadParticipantInList with ThreadParticipant(user = reply.user,
lastThreadMessageAt = reply.createdLocallyAt ?: reply.createdAt) instead of only
on isInsert.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadList.kt`:
- Around line 184-193: The Box already applies the incoming modifier (modifier)
with padding, but the same modifier is being forwarded again into Threads
causing duplicate padding/background/test tags; update the Threads call to pass
Modifier instead of the incoming modifier (or a cleaned modifier) so Threads
receives its own composed modifier (e.g., use Modifier or Modifier.fillMaxSize()
as appropriate) and keep the incoming modifier only on Box; adjust the Threads
invocation in ThreadList (where state.threads/isLoading checks occur) to remove
forwarding of the original modifier while preserving other parameters like
threads, isLoading, isLoadingMore, onLoadMore.

---

Nitpick comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryThreadsState.kt`:
- Around line 53-54: Update the public KDoc for QueryThreadsState.loadingError
to include thread/collection expectations and state notes: state explicitly
emits whether the most recent initial or refresh load failed (and does not
reflect pagination failures), is a hot StateFlow that can be safely collected
from any thread/coroutine context (UI or background), and will replay the latest
Boolean to new collectors; reference QueryThreadsState.loadingError in the
comment and keep the existing semantic note about initial/refresh vs pagination
failures.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/ThreadExtensionsTests.kt`:
- Around line 51-52: The test fixtures use randomThreadParticipant to build
threadParticipant1 and threadParticipant2 which leaves lastThreadMessageAt
randomized; change the test setup to pass explicit deterministic timestamps into
randomThreadParticipant (e.g., fixed Instant/Date values) for the
lastThreadMessageAt field for both threadParticipant1 and threadParticipant2 so
participant recency is stable; locate usages of randomThreadParticipant in
ThreadExtensionsTests.kt and update its call sites (threadParticipant1,
threadParticipant2) to provide a fixed lastThreadMessageAt parameter (and adjust
any helper signature if needed) to remove randomness from the tests.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsStateLogicTest.kt`:
- Around line 476-515: Add a test that verifies participant re-ordering when an
existing non-first participant sends a reply: create a mutableState mock with
threadList where Thread.threadParticipants has usrId2 as first and usrId1
second, instantiate QueryThreadsStateLogic and call upsertReply with a reply
from User(id="usrId1"), then verify QueryThreadsMutableState.upsertThreads is
called with a Thread whose threadParticipants list has been re-sorted so usrId1
is now first (compare using ThreadParticipant.user.id and lastThreadMessageAt)
to assert participants are moved to reflect the updated lastThreadMessageAt;
reference QueryThreadsStateLogic.upsertReply,
QueryThreadsMutableState.upsertThreads, ThreadParticipant and threadList to
locate the code under test.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt`:
- Around line 214-216: The cached timestamp in ThreadItem (`val timestamp =
remember(updatedAt) { ThreadTimestampFormatter.format(updatedAt, context) }`) is
only keyed by `updatedAt`, so locale/config changes can leave stale localized
text; update the remember key to also include the current context (or its
configuration) so the cache invalidates on configuration/locale changes (e.g.,
change the remember call to use `updatedAt` and
`context`/`context.resources.configuration` as keys) while keeping the
formatting call to ThreadTimestampFormatter.format(updatedAt, context).

In
`@stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ThreadParticipant.kt`:
- Line 31: The ThreadParticipant data class property lastThreadMessageAt should
have a default value of null to ease API migration; change the declaration of
lastThreadMessageAt in ThreadParticipant to provide a default (= null) and then
update any constructors, factory methods or call sites (builders/tests) that
explicitly pass this parameter so they can omit it safely.

In
`@stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt`:
- Around line 1044-1050: Add KDoc for the public fixture helper function
randomThreadParticipant(user: User = randomUser(), lastThreadMessageAt: Date? =
randomDateOrNull()) describing what the function returns (a ThreadParticipant),
documenting the parameters (user and nullable lastThreadMessageAt), and noting
any thread-related expectations/state (e.g., that lastThreadMessageAt represents
the timestamp of the last message in the thread or may be null) and that this is
a test fixture helper; place the KDoc immediately above the
randomThreadParticipant declaration.

In
`@stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewThreadData.kt`:
- Around line 30-31: Add a brief inline comment immediately above the
`@Suppress`("MagicNumber") on the PreviewThreadData object explaining why magic
numbers are acceptable here (these values are fixed timestamps/constants used
solely for preview/demo UI data), and note that this suppression is intentional
per coding guidelines; reference the PreviewThreadData object and the
`@Suppress`("MagicNumber") annotation so reviewers can see the justification
without removing the suppression.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cf481f5 and 95cb7f3.

⛔ Files ignored due to path filters (9)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListBannerTest_error_state.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListBannerTest_loading_state.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListBannerTest_unread_threads.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_empty_threads.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_threads.png is excluded by !**/*.png
📒 Files selected for processing (47)
  • stream-chat-android-client/api/stream-chat-android-client.api
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryThreadsState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/ThreadDtos.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.kt
  • 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/threads/internal/ThreadMapper.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/threads/internal/ThreadParticipantEntity.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsLogic.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsStateLogic.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querythreads/internal/QueryThreadsMutableState.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt
  • 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/extensions/internal/ThreadExtensionsTests.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/Mother.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/domain/threads/internal/ThreadMapperTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsLogicTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsStateLogicTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/querythreads/internal/QueryThreadsMutableStateTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ThreadDtoTestData.kt
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ShimmerProgressIndicator.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadList.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListBanner.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListLoadingItem.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatter.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/UnreadThreadsBanner.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt
  • stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_exclamation_circle.xml
  • stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_threads_empty.xml
  • stream-chat-android-compose/src/main/res/values/strings.xml
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListBannerTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatterTest.kt
  • stream-chat-android-core/api/stream-chat-android-core.api
  • stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ThreadParticipant.kt
  • stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt
  • stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewThreadData.kt
  • stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/threads/ThreadListState.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt
💤 Files with no reviewable changes (1)
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/UnreadThreadsBanner.kt

}

/** Full locale-aware day name (e.g. "Monday", "Montag", "lundi"). */
private fun dayOfWeek(date: Date): String =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not entirely convinced how should handle localisation here. We could either use the SimpleDateFormat("EEEE", Locale.getDefault()).format(date) to get the localised name based on the device of the language, but we might face the issue when the device language, is not a language in which the SDK is localised. We might get weird language mix like:

<month-in-spanish> at (English) 12:00
<day-of-week-in-spanish> at (English) 12:00

Perhaps we should localise each Month and Day of the week to ensure we don't encounter similar issues.

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

Labels

pr:breaking-change Breaking change pr:new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant