Skip to content

Introduce ThreadsScreen and ThreadListHeader components#6376

Merged
gpunto merged 2 commits intodevelopfrom
threads-header
Apr 20, 2026
Merged

Introduce ThreadsScreen and ThreadListHeader components#6376
gpunto merged 2 commits intodevelopfrom
threads-header

Conversation

@gpunto
Copy link
Copy Markdown
Contributor

@gpunto gpunto commented Apr 17, 2026

Goal

Add a header to the thread list screen, matching the Figma specs and the existing ChannelsScreen / ChannelListHeader pattern.

Same as #6342, which got closed when I deleted the v7 branch.

Implementation

  • Add ThreadListHeader composable that reuses ChannelListHeader internally with no trailing action button
  • Add ThreadsScreen composable that wraps ThreadListHeader + ThreadList, mirroring ChannelsScreen
  • Add ThreadListHeaderParams and ChatComponentFactory.ThreadListHeader for customization
  • Expose user and connectionState on ThreadListController and ThreadListViewModel
  • Update the sample app to use ThreadsScreen

🎨 UI Changes

Before After
Screenshot_20260408_150321 Screenshot_20260408_150207

Testing

Launch the sample and verify that the threads screen shows the header

Summary by CodeRabbit

  • New Features
    • Added dedicated threads screen with enhanced header displaying user avatar and connection status
    • Improved thread list header with refined visual layout and interaction handling
    • Network connectivity feedback in threads interface showing connected, connecting, and disconnected states

@gpunto gpunto added the pr:new-feature New feature label Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 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.

@gpunto
Copy link
Copy Markdown
Contributor Author

gpunto commented Apr 17, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

✅ Actions performed

Review triggered.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Walkthrough

The pull request introduces a new ThreadsScreen composable and refactors header layout logic into reusable components. A shared ListHeader component replaces inline header implementation in channel lists, while ThreadListHeader provides thread-specific header rendering. The ChannelsActivity now uses ThreadsScreen instead of inline ThreadList composition. ThreadListViewModel exposes additional reactive state for connection and user information.

Changes

Cohort / File(s) Summary
Activity and Sample Updates
stream-chat-android-compose-sample/.../ChannelsActivity.kt
Replaced ThreadList with ThreadsScreen, updated THREADS tab to use ThreadListHeader with avatar click handler that opens navigation drawer.
New Screen and Header Composables
stream-chat-android-compose/ui/threads/ThreadsScreen.kt, stream-chat-android-compose/ui/threads/ThreadListHeader.kt
Added ThreadsScreen entry point that creates ThreadListViewModel, observes user and connection state, and renders header with ThreadList. Added ThreadListHeader with configurable title, user avatar, connection state indicator, and avatar click callback.
Shared Header Component
stream-chat-android-compose/ui/components/ListHeader.kt
Introduced internal ListHeader with three content slots (leading, center, trailing), avatar rendering with click handler, and connection-aware center content (title when connected, loading indicator when connecting, disconnected text when offline).
Refactored Channel Header
stream-chat-android-compose/ui/channels/header/ChannelListHeader.kt
Simplified ChannelListHeader to delegate layout rendering to shared ListHeader; removed inline Surface, Column, spacing, and default content implementations.
Component Factory and Parameters
stream-chat-android-compose/ui/theme/ChatComponentFactory.kt, stream-chat-android-compose/ui/theme/ChatComponentFactoryParams.kt
Updated default header content implementations to use shared DefaultListHeaderLeadingContent and DefaultListHeaderCenterContent. Added new ThreadListHeader factory method and ThreadListHeaderParams data class with connection state, modifier, title, user, and avatar click callback fields.
ViewModel and Controller Enhancements
stream-chat-android-compose/viewmodel/threads/ThreadListViewModel.kt, stream-chat-android-ui-common/feature/threads/ThreadListController.kt
Exposed connectionState and user as StateFlow properties in both ThreadListViewModel and ThreadListController for reactive connection and authentication state updates.
Public API, Resources, and Tests
stream-chat-android-compose.api, stream-chat-android-compose/res/values/strings.xml, stream-chat-android-compose/ui/threads/ThreadListHeaderTest.kt, stream-chat-android-ui-common/feature/threads/ThreadListControllerTest.kt
Updated public API surface with new composables and ViewModel accessors. Added stream_compose_thread_list_header_title string resource. Added Paparazzi snapshot tests for ThreadListHeader variants (connected/connecting/offline with and without user). Updated ThreadListControllerTest fixture to mock ClientState.

Sequence Diagram

sequenceDiagram
    actor User as User
    participant Activity as ChannelsActivity
    participant Screen as ThreadsScreen
    participant VM as ThreadListViewModel
    participant Header as ThreadListHeader
    participant List as ThreadList
    participant Controller as ThreadListController
    participant ChatClient as ChatClient

    User->>Activity: Click THREADS tab
    Activity->>Screen: Render ThreadsScreen(factory, title, onHeaderAvatarClick)
    Screen->>VM: Create via ThreadsViewModelFactory
    Screen->>VM: collectAsState(user)
    Screen->>VM: collectAsState(connectionState)
    VM->>Controller: Access user & connectionState StateFlows
    Controller->>ChatClient: Get clientState.user & connectionState
    ChatClient-->>Controller: Emit user & connectionState
    Controller-->>VM: Provide StateFlows
    VM-->>Screen: Emit user & connectionState states
    
    Screen->>Header: Render ThreadListHeader(title, user, connectionState, onAvatarClick)
    Header->>Header: Show avatar (if user exists) or spacer
    Header->>Header: Show title (connected), loading indicator (connecting), or "Disconnected"
    
    Screen->>List: Render ThreadList(viewModel, onThreadClick)
    List->>VM: Access thread state
    VM->>Controller: Load threads via QueryThreadsRequest
    Controller-->>VM: Emit thread list
    VM-->>List: Provide threads state
    List-->>Screen: Render thread items
    
    User->>Header: Click avatar
    Header->>Activity: onHeaderAvatarClick()
    Activity->>Activity: drawerState.open()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 New headers bloom in Compose delight,
Threads now have their rightful screen!
Shared components, refactored tight,
ViewModel flows, connection keen,
The avatar hops where clicks convene! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% 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: introducing two new components (ThreadsScreen and ThreadListHeader) to the threads UI, which aligns with the primary objective of adding a header to the thread list screen.
Description check ✅ Passed The description follows the template structure with all key sections completed: Goal clearly states the purpose, Implementation lists all changes, UI Changes includes before/after screenshots, and Testing explains verification steps.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch threads-header

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
Copy Markdown

@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.

🧹 Nitpick comments (3)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.kt (1)

149-176: Optional: deduplicate connected/offline Text branches.

The Connected and Offline arms render the same Text with identical modifier/style, only the text differs. A tiny local helper or computing text first would reduce duplication.

♻️ Suggested refactor
-    when (connectionState) {
-        is ConnectionState.Connected -> {
-            Text(
-                modifier = Modifier
-                    .weight(1f)
-                    .wrapContentWidth()
-                    .padding(horizontal = StreamTokens.spacingMd),
-                text = title,
-                style = ChatTheme.typography.headingSmall,
-                maxLines = 1,
-                color = ChatTheme.colors.textPrimary,
-            )
-        }
-
-        is ConnectionState.Connecting -> NetworkLoadingIndicator(modifier = Modifier.weight(1f))
-        is ConnectionState.Offline -> {
-            Text(
-                modifier = Modifier
-                    .weight(1f)
-                    .wrapContentWidth()
-                    .padding(horizontal = StreamTokens.spacingMd),
-                text = stringResource(R.string.stream_compose_disconnected),
-                style = ChatTheme.typography.headingSmall,
-                maxLines = 1,
-                color = ChatTheme.colors.textPrimary,
-            )
-        }
-    }
+    when (connectionState) {
+        is ConnectionState.Connecting -> NetworkLoadingIndicator(modifier = Modifier.weight(1f))
+        is ConnectionState.Connected,
+        is ConnectionState.Offline,
+        -> Text(
+            modifier = Modifier
+                .weight(1f)
+                .wrapContentWidth()
+                .padding(horizontal = StreamTokens.spacingMd),
+            text = if (connectionState is ConnectionState.Connected) {
+                title
+            } else {
+                stringResource(R.string.stream_compose_disconnected)
+            },
+            style = ChatTheme.typography.headingSmall,
+            maxLines = 1,
+            color = ChatTheme.colors.textPrimary,
+        )
+    }
🤖 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/components/ListHeader.kt`
around lines 149 - 176, The Connected and Offline branches in ListHeader.kt
duplicate the same Text composable; compute the displayed string first (e.g.,
val headerText = when (connectionState) { is ConnectionState.Connected -> title;
is ConnectionState.Offline ->
stringResource(R.string.stream_compose_disconnected); else -> null }) or create
a small helper composable, then render a single Text with the existing Modifier
(.weight(1f).wrapContentWidth().padding(StreamTokens.spacingMd)), style
(ChatTheme.typography.headingSmall) and color (ChatTheme.colors.textPrimary);
keep the NetworkLoadingIndicator branch (ConnectionState.Connecting) as-is.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt (1)

67-69: Consider exposing a customizable trailing slot.

The trailing area is hardcoded to a Spacer sized to AvatarSize.ExtraLarge to balance the leading avatar so the centered title remains centered. That works for the default case, but it prevents consumers from customizing the trailing slot (e.g., adding a settings/search action) without replacing the entire composable — unlike ChannelListHeader which exposes leadingContent/centerContent/trailingContent parameters. If future customization is anticipated, mirroring that slot pattern (with the Spacer as the default) would be more consistent and extensible.

🤖 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/ThreadListHeader.kt`
around lines 67 - 69, ThreadListHeader currently hardcodes trailingContent to a
Spacer sized AvatarSize.ExtraLarge which prevents consumers from adding custom
trailing UI; change ThreadListHeader signature to accept an optional
trailingContent composable parameter (mirror ChannelListHeader's
leadingContent/centerContent/trailingContent pattern) and use the provided
trailingContent when non-null, falling back to the existing
Spacer(Modifier.size(AvatarSize.ExtraLarge)) default; update the call sites and
the composable parameter list (ThreadListHeader, trailingContent) so existing
behavior is preserved but consumers can pass custom content.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt (1)

57-72: Prefer Modifier.weight(1f) on ThreadList inside the Column.

Using Modifier.fillMaxSize() on a child inside a vertical Column is fragile: it works today because the header measures first and the remaining max height is passed down, but it disables intrinsic sizing and can cause layout oddities when the parent has unbounded height (e.g., inside a scrollable container). The idiomatic pattern is Modifier.weight(1f) for the flexible child so it takes the remaining space after the header. Also consider exposing a modifier: Modifier = Modifier parameter on ThreadsScreen for consistency with other screen composables and to allow callers to apply padding/insets.

♻️ Proposed change
 `@Composable`
 public fun ThreadsScreen(
+    modifier: Modifier = Modifier,
     viewModelFactory: ThreadsViewModelFactory = ThreadsViewModelFactory(query = QueryThreadsRequest()),
     title: String = stringResource(R.string.stream_compose_thread_list_header_title),
     onHeaderAvatarClick: () -> Unit = {},
     onThreadClick: (Thread) -> Unit = {},
 ) {
     val listViewModel: ThreadListViewModel = viewModel(factory = viewModelFactory)

     val user by listViewModel.user.collectAsState()
     val connectionState by listViewModel.connectionState.collectAsState()

-    Column(modifier = Modifier.fillMaxSize()) {
+    Column(modifier = modifier.fillMaxSize()) {
         ChatTheme.componentFactory.ThreadListHeader(
             params = ThreadListHeaderParams(
                 title = title,
                 currentUser = user,
                 connectionState = connectionState,
                 onAvatarClick = { onHeaderAvatarClick() },
             ),
         )

         ThreadList(
             viewModel = listViewModel,
-            modifier = Modifier.fillMaxSize(),
+            modifier = Modifier.weight(1f).fillMaxWidth(),
             onThreadClick = onThreadClick,
         )
     }
 }
🤖 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/ThreadsScreen.kt`
around lines 57 - 72, The ThreadList child currently uses Modifier.fillMaxSize()
inside the Column which is fragile; update ThreadsScreen so ThreadList uses
Modifier.weight(1f) (e.g., ThreadList(modifier = Modifier.weight(1f), ...)) so
it correctly takes remaining space after
ChatTheme.componentFactory.ThreadListHeader, and also add an optional parameter
modifier: Modifier = Modifier to ThreadsScreen and thread that through the
Column (Column(modifier = modifier.fillMaxSize())) to allow callers to apply
padding/insets consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.kt`:
- Around line 149-176: The Connected and Offline branches in ListHeader.kt
duplicate the same Text composable; compute the displayed string first (e.g.,
val headerText = when (connectionState) { is ConnectionState.Connected -> title;
is ConnectionState.Offline ->
stringResource(R.string.stream_compose_disconnected); else -> null }) or create
a small helper composable, then render a single Text with the existing Modifier
(.weight(1f).wrapContentWidth().padding(StreamTokens.spacingMd)), style
(ChatTheme.typography.headingSmall) and color (ChatTheme.colors.textPrimary);
keep the NetworkLoadingIndicator branch (ConnectionState.Connecting) as-is.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt`:
- Around line 67-69: ThreadListHeader currently hardcodes trailingContent to a
Spacer sized AvatarSize.ExtraLarge which prevents consumers from adding custom
trailing UI; change ThreadListHeader signature to accept an optional
trailingContent composable parameter (mirror ChannelListHeader's
leadingContent/centerContent/trailingContent pattern) and use the provided
trailingContent when non-null, falling back to the existing
Spacer(Modifier.size(AvatarSize.ExtraLarge)) default; update the call sites and
the composable parameter list (ThreadListHeader, trailingContent) so existing
behavior is preserved but consumers can pass custom content.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt`:
- Around line 57-72: The ThreadList child currently uses Modifier.fillMaxSize()
inside the Column which is fragile; update ThreadsScreen so ThreadList uses
Modifier.weight(1f) (e.g., ThreadList(modifier = Modifier.weight(1f), ...)) so
it correctly takes remaining space after
ChatTheme.componentFactory.ThreadListHeader, and also add an optional parameter
modifier: Modifier = Modifier to ThreadsScreen and thread that through the
Column (Column(modifier = modifier.fillMaxSize())) to allow callers to apply
padding/insets consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c454c58b-0a0c-4585-9772-39ff1b012487

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1993d and 6520893.

⛔ Files ignored due to path filters (6)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connected,_no_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connected,_with_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connecting,_no_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connecting,_with_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_offline,_no_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_offline,_with_user.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.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/theme/ChatComponentFactoryParams.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel.kt
  • stream-chat-android-compose/src/main/res/values/strings.xml
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListHeaderTest.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt

@github-actions
Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.82 MB 5.82 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.95 MB 10.95 MB 0.00 MB 🟢
stream-chat-android-compose 12.23 MB 12.23 MB 0.00 MB 🟢

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
62.3% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@gpunto gpunto marked this pull request as ready for review April 17, 2026 13:14
@gpunto gpunto requested a review from a team as a code owner April 17, 2026 13:14
@gpunto gpunto merged commit 3ac468c into develop Apr 20, 2026
27 of 32 checks passed
@gpunto gpunto deleted the threads-header branch April 20, 2026 09:00
@stream-public-bot stream-public-bot added the released Included in a release label Apr 24, 2026
@stream-public-bot
Copy link
Copy Markdown
Contributor

🚀 Available in v7.0.1

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

Labels

pr:new-feature New feature released Included in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants