Conversation
# 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
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
|
WalkthroughThis pull request enhances thread query functionality by adding a Changes
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
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()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorUpdate participant recency on every inserted reply, not only first-time participants.
threadParticipantsrecency is refreshed only whenisInsertis true, so existing participants can keep stale order after new replies. Also, Line 92 should use optimistic-first timestamp selection.Based on learnings: In the Stream Chat Android SDK, `createdLocallyAt` should be prioritized over `createdAt` so optimistic updates immediately affect ordering.💡 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, + )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 | 🟡 MinorAvoid reapplying the same
modifiertoThreads.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
@OptInannotations 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 explicitlastThreadMessageAtvalues 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.Based on learnings: Applies to **/src/test/**/*.kt : Use deterministic tests with `runTest` + virtual time for concurrency-sensitive logic (uploads, sync, message state).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), + )🤖 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 publicloadingErrorKDoc.Line 53 documents state semantics well, but the API doc should also state thread/collection expectations explicitly for this
StateFlow.As per coding guidelines, "Document public APIs with KDoc, including thread expectations and state notes".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>🤖 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 defaultinglastThreadMessageAttonullfor 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
upsertReplyimplementation 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 bylastThreadMessageAt.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 (viasortedByLastReply()).🤖 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 withcontextas well.
ThreadTimestampFormatter.format(...)is locale/resource-sensitive; caching only byupdatedAtcan 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.
⛔ Files ignored due to path filters (9)
stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListBannerTest_error_state.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListBannerTest_loading_state.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListBannerTest_unread_threads.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_empty_threads.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_threads.pngis excluded by!**/*.png
📒 Files selected for processing (47)
stream-chat-android-client/api/stream-chat-android-client.apistream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryThreadsState.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/ThreadDtos.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/database/internal/ChatDatabase.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/threads/internal/ThreadMapper.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/threads/internal/ThreadParticipantEntity.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsLogic.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsStateLogic.ktstream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querythreads/internal/QueryThreadsMutableState.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/ThreadExtensionsTests.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/Mother.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/domain/threads/internal/ThreadMapperTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsLogicTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querythreads/internal/QueryThreadsStateLogicTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/querythreads/internal/QueryThreadsMutableStateTest.ktstream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ThreadDtoTestData.ktstream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ShimmerProgressIndicator.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadList.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListBanner.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListLoadingItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatter.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/UnreadThreadsBanner.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.ktstream-chat-android-compose/src/main/res/drawable/stream_compose_ic_exclamation_circle.xmlstream-chat-android-compose/src/main/res/drawable/stream_compose_ic_threads_empty.xmlstream-chat-android-compose/src/main/res/values/strings.xmlstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListBannerTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatterTest.ktstream-chat-android-core/api/stream-chat-android-core.apistream-chat-android-core/src/main/java/io/getstream/chat/android/models/ThreadParticipant.ktstream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.ktstream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewThreadData.ktstream-chat-android-ui-common/api/stream-chat-android-ui-common.apistream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/threads/ThreadListState.ktstream-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
...roid-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt
Outdated
Show resolved
Hide resolved
...hat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt
Outdated
Show resolved
Hide resolved
...mpose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatter.kt
Show resolved
Hide resolved
...src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadTimestampFormatterTest.kt
Show resolved
Hide resolved
| } | ||
|
|
||
| /** Full locale-aware day name (e.g. "Monday", "Montag", "lundi"). */ | ||
| private fun dayOfWeek(date: Date): String = |
There was a problem hiding this comment.
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.



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
lastThreadMessageAtfield onThreadParticipant.Implementation
Model layer (
stream-chat-android-client,stream-chat-android-core)ThreadParticipant.lastThreadMessageAt— a nullableDateparsed from the API'slast_thread_message_atfield, persisted in Room viaThreadParticipantEntity.sortedByLastReply()), so the avatar stack in each thread item shows the most active participants first.ThreadListStategains aloadingError: Booleanfield to distinguish failed loads from empty states.QueryThreadsLogic/QueryThreadsMutableStatepropagate the new error state.Compose UI (
stream-chat-android-compose)ThreadItem— completely redesigned: horizontalRowlayout 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 verticalColumnlayout with icon + "replied to:" prefix. Removed the slot-basedtitleContent/replyToContent/unreadCountContent/latestReplyContentparameters in favour of a simpler, opinionated design.ThreadListLoadingItem(new) — skeleton shimmer placeholder that mirrors ThreadItem's layout. ReusesShimmerProgressIndicator(which now accepts optionalbaseColor/highlightColorparams).ThreadListBanner(new, replacesUnreadThreadsBanner) — supports three states:UnreadThreads,Loading, andError, driven by a sealedThreadListBannerStateinterface.ThreadTimestampFormatter(new) — relative timestamp formatting: "Just now", "Today at …", "Yesterday at …", " at …", " at …".DefaultThreadListLoadingContent— replacedLoadingIndicator(spinner) with aLazyColumnof 7 skeletonThreadListLoadingItems.DefaultThreadListEmptyContent— updated icon, copy ("Reply to a message to start a thread"), and sizing to match Figma.ChatComponentFactory— removedThreadListItemTitle,ThreadListItemReplyToContent,ThreadListItemUnreadCountContent,ThreadListItemLatestReplyContentslot methods; addedThreadListBanner.stream_compose_ic_exclamation_circlefor error banner.Tests
ThreadTimestampFormatterTestwith comprehensive coverage of all formatting branches.ThreadListBannerTest(renamed fromUnreadThreadsBannerTest) with Paparazzi snapshots for all three banner states.ThreadListTestPaparazzi snapshots for the new loading/empty/loaded designs.ChatsScreenTest— thread loading assertion uses test tag instead of progress bar semantics.DomainMappingTest,ThreadMapperTest,QueryThreadsLogicTest,QueryThreadsStateLogicTest,QueryThreadsMutableStateTest) for the newlastThreadMessageAtfield and error state.ThreadParticipantEntitycolumn.🎨 UI Changes
thread-list.mp4
thread-list-error.mp4
Testing
ThreadTimestampFormatterTestcovers all timestamp formatting branches (just now, today, yesterday, day-of-week, older dates). Client-layer tests updated forlastThreadMessageAtparsing, mapping, and state propagation.ThreadItem,ThreadListBanner(all 3 states),ThreadList(loading, empty, loaded, loading-more, unread banner). Run./gradlew :stream-chat-android-compose:verifyPaparazziDebugto verify.ChatsScreenTestverifies the skeleton loading content renders correctly in threads mode.Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Improvements