From 53c03eee21a8f62c405b4944819885ae8e32dd64 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:33:28 +0200 Subject: [PATCH 1/3] Introduce ThreadsScreen and ThreadListHeader composables --- .../feature/channel/list/ChannelsActivity.kt | 24 ++-- .../api/stream-chat-android-compose.api | 41 ++++++ .../compose/ui/theme/ChatComponentFactory.kt | 17 +++ .../ui/theme/ChatComponentFactoryParams.kt | 17 +++ .../compose/ui/threads/ThreadListHeader.kt | 136 ++++++++++++++++++ .../compose/ui/threads/ThreadsScreen.kt | 73 ++++++++++ .../viewmodel/threads/ThreadListViewModel.kt | 12 ++ .../src/main/res/values/strings.xml | 1 + .../ui/threads/ThreadListHeaderTest.kt | 71 +++++++++ ...hreadListHeaderTest_connected,_no_user.png | Bin 0 -> 7231 bytes ...eadListHeaderTest_connected,_with_user.png | Bin 0 -> 10406 bytes ...readListHeaderTest_connecting,_no_user.png | Bin 0 -> 12601 bytes ...adListHeaderTest_connecting,_with_user.png | Bin 0 -> 15798 bytes ..._ThreadListHeaderTest_offline,_no_user.png | Bin 0 -> 9277 bytes ...hreadListHeaderTest_offline,_with_user.png | Bin 0 -> 12472 bytes .../feature/threads/ThreadListController.kt | 12 ++ .../threads/ThreadListControllerTest.kt | 11 +- 17 files changed, 403 insertions(+), 12 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt create mode 100644 stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListHeaderTest.kt create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connected,_no_user.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connected,_with_user.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connecting,_no_user.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connecting,_with_user.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_offline,_no_user.png create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_offline,_with_user.png diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt index b6efcaf7e34..16a145a30f6 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt @@ -86,12 +86,11 @@ import io.getstream.chat.android.compose.ui.components.SearchInput import io.getstream.chat.android.compose.ui.components.channels.buildDefaultChannelActions import io.getstream.chat.android.compose.ui.mentions.MentionList import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.ui.threads.ThreadList +import io.getstream.chat.android.compose.ui.threads.ThreadsScreen import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModelFactory import io.getstream.chat.android.compose.viewmodel.mentions.MentionListViewModel import io.getstream.chat.android.compose.viewmodel.mentions.MentionListViewModelFactory -import io.getstream.chat.android.compose.viewmodel.threads.ThreadListViewModel import io.getstream.chat.android.compose.viewmodel.threads.ThreadsViewModelFactory import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.Filters @@ -126,10 +125,7 @@ class ChannelsActivity : ComponentActivity() { private val channelsViewModel: ChannelListViewModel by viewModels { channelsViewModelFactory } private val mentionListViewModel: MentionListViewModel by viewModels { MentionListViewModelFactory() } - private val threadsViewModel: ThreadListViewModel by viewModels { - val query = QueryThreadsRequest() - ThreadsViewModelFactory(query) - } + private val threadsViewModelFactory = ThreadsViewModelFactory(query = QueryThreadsRequest()) @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { @@ -212,7 +208,13 @@ class ChannelsActivity : ComponentActivity() { ) AppBottomBarOption.MENTIONS -> MentionsContent() - AppBottomBarOption.THREADS -> ThreadsContent() + AppBottomBarOption.THREADS -> ThreadsContent( + onHeaderAvatarClick = { + coroutineScope.launch { + drawerState.open() + } + }, + ) } } } @@ -251,10 +253,10 @@ class ChannelsActivity : ComponentActivity() { } @Composable - private fun ThreadsContent() { - ThreadList( - viewModel = threadsViewModel, - modifier = Modifier.fillMaxSize(), + private fun ThreadsContent(onHeaderAvatarClick: () -> Unit) { + ThreadsScreen( + viewModelFactory = threadsViewModelFactory, + onHeaderAvatarClick = onHeaderAvatarClick, onThreadClick = ::openThread, ) } diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index f799d845cfe..f65383190ef 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -3474,6 +3474,7 @@ public abstract interface class io/getstream/chat/android/compose/ui/theme/ChatC public fun SwipeToReplyContent (Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/ui/theme/SwipeToReplyContentParams;Landroidx/compose/runtime/Composer;I)V public fun ThreadListBanner (Lio/getstream/chat/android/compose/ui/theme/ThreadListBannerParams;Landroidx/compose/runtime/Composer;I)V public fun ThreadListEmptyContent (Lio/getstream/chat/android/compose/ui/theme/ThreadListEmptyContentParams;Landroidx/compose/runtime/Composer;I)V + public fun ThreadListHeader (Lio/getstream/chat/android/compose/ui/theme/ThreadListHeaderParams;Landroidx/compose/runtime/Composer;I)V public fun ThreadListItem (Lio/getstream/chat/android/compose/ui/theme/ThreadListItemParams;Landroidx/compose/runtime/Composer;I)V public fun ThreadListLoadingContent (Lio/getstream/chat/android/compose/ui/theme/ThreadListLoadingContentParams;Landroidx/compose/runtime/Composer;I)V public fun ThreadListLoadingMoreContent (Lio/getstream/chat/android/compose/ui/theme/ThreadListLoadingMoreContentParams;Landroidx/compose/runtime/Composer;I)V @@ -3662,6 +3663,7 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatComponentFacto public static fun SwipeToReplyContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/ui/theme/SwipeToReplyContentParams;Landroidx/compose/runtime/Composer;I)V public static fun ThreadListBanner (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ThreadListBannerParams;Landroidx/compose/runtime/Composer;I)V public static fun ThreadListEmptyContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ThreadListEmptyContentParams;Landroidx/compose/runtime/Composer;I)V + public static fun ThreadListHeader (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ThreadListHeaderParams;Landroidx/compose/runtime/Composer;I)V public static fun ThreadListItem (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ThreadListItemParams;Landroidx/compose/runtime/Composer;I)V public static fun ThreadListLoadingContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ThreadListLoadingContentParams;Landroidx/compose/runtime/Composer;I)V public static fun ThreadListLoadingMoreContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/ui/theme/ThreadListLoadingMoreContentParams;Landroidx/compose/runtime/Composer;I)V @@ -6097,6 +6099,27 @@ public final class io/getstream/chat/android/compose/ui/theme/ThreadListEmptyCon public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/theme/ThreadListHeaderParams { + public static final field $stable I + public fun (Lio/getstream/chat/android/models/ConnectionState;Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/models/ConnectionState;Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/models/ConnectionState; + public final fun component2 ()Landroidx/compose/ui/Modifier; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/getstream/chat/android/models/User; + public final fun component5 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lio/getstream/chat/android/models/ConnectionState;Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/ui/theme/ThreadListHeaderParams; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ThreadListHeaderParams;Lio/getstream/chat/android/models/ConnectionState;Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ThreadListHeaderParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getConnectionState ()Lio/getstream/chat/android/models/ConnectionState; + public final fun getCurrentUser ()Lio/getstream/chat/android/models/User; + public final fun getModifier ()Landroidx/compose/ui/Modifier; + public final fun getOnAvatarClick ()Lkotlin/jvm/functions/Function1; + public final fun getTitle ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/compose/ui/theme/ThreadListItemParams { public static final field $stable I public fun (Lio/getstream/chat/android/models/Thread;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;)V @@ -6185,6 +6208,14 @@ public final class io/getstream/chat/android/compose/ui/threads/ComposableSingle public final fun getLambda$881034423$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListHeaderKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListHeaderKt; + public fun ()V + public final fun getLambda$-1674084940$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$-186248340$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-2127007699$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListKt; public fun ()V @@ -6241,11 +6272,19 @@ public final class io/getstream/chat/android/compose/ui/threads/ThreadListBanner public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/threads/ThreadListHeaderKt { + public static final fun ThreadListHeader-ws93vos (Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/ConnectionState;JLandroidx/compose/ui/graphics/Shape;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/threads/ThreadListKt { public static final fun ThreadList (Lio/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V public static final fun ThreadList (Lio/getstream/chat/android/ui/common/state/threads/ThreadListState;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } +public final class io/getstream/chat/android/compose/ui/threads/ThreadsScreenKt { + public static final fun ThreadsScreen (Lio/getstream/chat/android/compose/viewmodel/threads/ThreadsViewModelFactory;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/util/ChannelUtilsKt { public static final fun getLastMessage (Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;)Lio/getstream/chat/android/models/Message; public static final fun getMembersStatusText (Lio/getstream/chat/android/models/Channel;Landroid/content/Context;Lio/getstream/chat/android/models/User;)Ljava/lang/String; @@ -6814,7 +6853,9 @@ public final class io/getstream/chat/android/compose/viewmodel/pinned/PinnedMess public final class io/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel : androidx/lifecycle/ViewModel { public static final field $stable I public fun (Lio/getstream/chat/android/ui/common/feature/threads/ThreadListController;)V + public final fun getConnectionState ()Lkotlinx/coroutines/flow/StateFlow; public final fun getState ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; public final fun load ()V public final fun loadNextPage ()V } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt index 4ad01fbe3d2..aceaa979b95 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt @@ -2053,6 +2053,23 @@ public interface ChatComponentFactory { ) } + /** + * The default header shown above the thread list. + * Displays the user avatar and a title, typically "Threads", with no trailing action button. + * + * @param params Parameters for this component. + */ + @Composable + public fun ThreadListHeader(params: ThreadListHeaderParams) { + io.getstream.chat.android.compose.ui.threads.ThreadListHeader( + modifier = params.modifier, + title = params.title, + currentUser = params.currentUser, + connectionState = params.connectionState, + onAvatarClick = params.onAvatarClick, + ) + } + /** * The default thread list banner. * Shows unread thread count, a loading indicator during refresh, or an error prompt. diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt index ed9fe9033a6..13d4fd7af37 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt @@ -1609,6 +1609,23 @@ public data class MenuOptionItemParams( val horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, ) +/** + * Parameters for [ChatComponentFactory.ThreadListHeader]. + * + * @param connectionState The current connection state. + * @param modifier Modifier for styling. + * @param title The title to display in the header. + * @param currentUser The currently logged in user. + * @param onAvatarClick Action invoked when the avatar is clicked. + */ +public data class ThreadListHeaderParams( + val connectionState: ConnectionState, + val modifier: Modifier = Modifier, + val title: String = "", + val currentUser: User? = null, + val onAvatarClick: (User?) -> Unit = {}, +) + /** * Parameters for [ChatComponentFactory.ThreadListBanner]. * diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt new file mode 100644 index 00000000000..ca100456b9f --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.threads + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.channels.header.ChannelListHeader +import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.models.ConnectionState +import io.getstream.chat.android.models.User +import io.getstream.chat.android.previewdata.PreviewUserData + +/** + * A header composable for the thread list screen. + * Internally reuses [ChannelListHeader] with no trailing action button. + * + * @param modifier Modifier for styling. + * @param title The title to display, when the network is available. + * @param currentUser The currently logged in user, to load its image in the avatar. + * @param connectionState The state of WS connection used to switch between the title and the network loading view. + * @param color The color of the header. + * @param shape The shape of the header. + * @param elevation The elevation of the header. + * @param onAvatarClick Action handler when the user taps on an avatar. + */ +@Composable +public fun ThreadListHeader( + modifier: Modifier = Modifier, + title: String = stringResource(R.string.stream_compose_thread_list_header_title), + currentUser: User? = null, + connectionState: ConnectionState = ConnectionState.Connected, + color: Color = ChatTheme.colors.backgroundCoreElevation1, + shape: Shape = RectangleShape, + elevation: Dp = 0.dp, + onAvatarClick: (User?) -> Unit = {}, +) { + ChannelListHeader( + modifier = modifier, + title = title, + currentUser = currentUser, + connectionState = connectionState, + color = color, + shape = shape, + elevation = elevation, + onAvatarClick = onAvatarClick, + trailingContent = { + Spacer(modifier = Modifier.size(AvatarSize.ExtraLarge)) + }, + ) +} + +@Composable +internal fun ThreadListHeaderConnectedNoUser() { + ThreadListHeader( + connectionState = ConnectionState.Connected, + ) +} + +@Composable +internal fun ThreadListHeaderConnectedWithUser() { + ThreadListHeader( + currentUser = PreviewUserData.user1, + connectionState = ConnectionState.Connected, + ) +} + +@Composable +internal fun ThreadListHeaderConnectingNoUser() { + ThreadListHeader( + connectionState = ConnectionState.Connecting, + ) +} + +@Composable +internal fun ThreadListHeaderConnectingWithUser() { + ThreadListHeader( + currentUser = PreviewUserData.user1, + connectionState = ConnectionState.Connecting, + ) +} + +@Composable +internal fun ThreadListHeaderOfflineNoUser() { + ThreadListHeader( + connectionState = ConnectionState.Offline, + ) +} + +@Composable +internal fun ThreadListHeaderOfflineWithUser() { + ThreadListHeader( + currentUser = PreviewUserData.user1, + connectionState = ConnectionState.Offline, + ) +} + +@Preview +@Composable +private fun ThreadListHeaderConnectedPreview() { + ChatTheme { + ThreadListHeaderConnectedWithUser() + } +} + +@Preview +@Composable +private fun ThreadListHeaderConnectingPreview() { + ChatTheme { + ThreadListHeaderConnectingWithUser() + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt new file mode 100644 index 00000000000..0992eb5c278 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.threads + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewmodel.compose.viewModel +import io.getstream.chat.android.client.api.models.QueryThreadsRequest +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.ThreadListHeaderParams +import io.getstream.chat.android.compose.viewmodel.threads.ThreadListViewModel +import io.getstream.chat.android.compose.viewmodel.threads.ThreadsViewModelFactory +import io.getstream.chat.android.models.Thread + +/** + * Default root Threads screen component, that provides the necessary ViewModel. + * + * It can be used without most parameters for default behavior, that can be tweaked if necessary. + * + * @param viewModelFactory The factory used to build the [ThreadListViewModel]. + * @param title Header title. + * @param onHeaderAvatarClick Handle for when the user clicks on the header avatar. + * @param onThreadClick Handler for Thread item clicks. + */ +@Composable +public fun ThreadsScreen( + 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()) { + ChatTheme.componentFactory.ThreadListHeader( + params = ThreadListHeaderParams( + title = title, + currentUser = user, + connectionState = connectionState, + onAvatarClick = { onHeaderAvatarClick() }, + ), + ) + + ThreadList( + viewModel = listViewModel, + modifier = Modifier.fillMaxSize(), + onThreadClick = onThreadClick, + ) + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel.kt index 09516f7f006..7d2b513260b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/threads/ThreadListViewModel.kt @@ -17,6 +17,8 @@ package io.getstream.chat.android.compose.viewmodel.threads import androidx.lifecycle.ViewModel +import io.getstream.chat.android.models.ConnectionState +import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.common.feature.threads.ThreadListController import io.getstream.chat.android.ui.common.state.threads.ThreadListState import kotlinx.coroutines.flow.StateFlow @@ -34,6 +36,16 @@ public class ThreadListViewModel(private val controller: ThreadListController) : */ public val state: StateFlow = controller.state + /** + * The state of our network connection - if we're online, connecting or offline. + */ + public val connectionState: StateFlow = controller.connectionState + + /** + * The state of the currently logged in user. + */ + public val user: StateFlow = controller.user + /** * Loads the initial data when requested. * Overrides all previously retrieved data. diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 239a985cdab..2a3d8e98184 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -335,6 +335,7 @@ Already a member + Threads Reply to a message to start a thread " in " diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListHeaderTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListHeaderTest.kt new file mode 100644 index 00000000000..f613cbf4884 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadListHeaderTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.threads + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import io.getstream.chat.android.compose.ui.PaparazziComposeTest +import org.junit.Rule +import org.junit.Test + +internal class ThreadListHeaderTest : PaparazziComposeTest { + + @get:Rule + override val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_2) + + @Test + fun `connected, no user`() { + snapshotWithDarkMode { + ThreadListHeaderConnectedNoUser() + } + } + + @Test + fun `connected, with user`() { + snapshotWithDarkMode { + ThreadListHeaderConnectedWithUser() + } + } + + @Test + fun `connecting, no user`() { + snapshotWithDarkMode { + ThreadListHeaderConnectingNoUser() + } + } + + @Test + fun `connecting, with user`() { + snapshotWithDarkMode { + ThreadListHeaderConnectingWithUser() + } + } + + @Test + fun `offline, no user`() { + snapshotWithDarkMode { + ThreadListHeaderOfflineNoUser() + } + } + + @Test + fun `offline, with user`() { + snapshotWithDarkMode { + ThreadListHeaderOfflineWithUser() + } + } +} diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connected,_no_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_connected,_no_user.png new file mode 100644 index 0000000000000000000000000000000000000000..cec6075906ceed84e1371f8877c06d0200db1101 GIT binary patch literal 7231 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 literal 0 HcmV?d00001 diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_offline,_with_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListHeaderTest_offline,_with_user.png new file mode 100644 index 0000000000000000000000000000000000000000..b3729ca74afc9dbb9dd1fc094e66cb0c7f373c83 GIT binary patch literal 12472 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 AHvj+t literal 0 HcmV?d00001 diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.kt index be9fe0de96c..c352e4b17b0 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListController.kt @@ -22,6 +22,8 @@ import io.getstream.chat.android.client.api.state.QueryThreadsState import io.getstream.chat.android.client.api.state.queryThreadsAsState import io.getstream.chat.android.core.internal.InternalStreamChatApi import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider +import io.getstream.chat.android.models.ConnectionState +import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.common.state.threads.ThreadListState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,6 +65,16 @@ public class ThreadListController( public val state: StateFlow get() = _state + /** + * The state of our network connection - if we're online, connecting or offline. + */ + public val connectionState: StateFlow = chatClient.clientState.connectionState + + /** + * The state of the currently logged in user. + */ + public val user: StateFlow = chatClient.clientState.user + private val queryThreadsState = queryThreadsAsState() init { diff --git a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt index 658bc7b6cb5..f485d03eb4b 100644 --- a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt +++ b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/threads/ThreadListControllerTest.kt @@ -20,6 +20,8 @@ import app.cash.turbine.test import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.api.models.QueryThreadsRequest import io.getstream.chat.android.client.api.state.QueryThreadsState +import io.getstream.chat.android.client.setup.state.ClientState +import io.getstream.chat.android.models.ConnectionState import io.getstream.chat.android.models.QueryThreadsResult import io.getstream.chat.android.randomString import io.getstream.chat.android.randomThread @@ -205,7 +207,14 @@ internal class ThreadListControllerTest { private val queryThreadsStateFlow = MutableStateFlow(null) - private val mockChatClient: ChatClient = mock() + private val mockClientState: ClientState = mock { + on { connectionState } doReturn MutableStateFlow(ConnectionState.Connected) + on { user } doReturn MutableStateFlow(null) + } + + private val mockChatClient: ChatClient = mock { + on { clientState } doReturn mockClientState + } fun givenQueryThreadsState(state: QueryThreadsState) = apply { queryThreadsStateFlow.value = state From 65f716f5630db6e761bf24b47d7cefe8e75f7d1a Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:04:10 +0200 Subject: [PATCH 2/3] Extract common list header components --- .../ui/channels/header/ChannelListHeader.kt | 127 +------------ .../compose/ui/components/ListHeader.kt | 177 ++++++++++++++++++ .../compose/ui/theme/ChatComponentFactory.kt | 8 +- .../compose/ui/threads/ThreadListHeader.kt | 24 ++- 4 files changed, 206 insertions(+), 130 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.kt diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt index bb135704113..e93193f67bc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt @@ -16,25 +16,10 @@ package io.getstream.chat.android.compose.ui.channels.header -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape @@ -45,16 +30,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator -import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize +import io.getstream.chat.android.compose.ui.components.ListHeader import io.getstream.chat.android.compose.ui.components.button.StreamButton import io.getstream.chat.android.compose.ui.theme.ChannelListHeaderCenterContentParams import io.getstream.chat.android.compose.ui.theme.ChannelListHeaderLeadingContentParams import io.getstream.chat.android.compose.ui.theme.ChannelListHeaderTrailingContentParams import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.ui.theme.StreamTokens -import io.getstream.chat.android.compose.ui.theme.UserAvatarParams -import io.getstream.chat.android.compose.ui.util.clickable import io.getstream.chat.android.models.ConnectionState import io.getstream.chat.android.models.User import io.getstream.chat.android.previewdata.PreviewUserData @@ -121,107 +102,15 @@ public fun ChannelListHeader( } }, ) { - Surface( - modifier = modifier - .fillMaxWidth(), - shadowElevation = elevation, + ListHeader( + modifier = modifier, color = color, shape = shape, - ) { - Column { - Row( - Modifier - .fillMaxWidth() - .padding(StreamTokens.spacingSm), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), - ) { - leadingContent() - - centerContent() - - trailingContent() - } - HorizontalDivider( - thickness = 1.dp, - color = ChatTheme.colors.borderCoreDefault, - ) - } - } -} - -/** - * Represents the default leading content of a channel list header, which is the currently logged-in user avatar. - * - * We show the avatar if the user is available, otherwise we add a spacer to make sure the alignment is correct. - */ -@Composable -internal fun DefaultChannelListHeaderLeadingContent( - currentUser: User?, - onAvatarClick: (User?) -> Unit, -) { - if (currentUser != null) { - Box( - modifier = Modifier - .size(AvatarSize.ExtraLarge) - .clip(CircleShape) - .clickable { onAvatarClick(currentUser) }, - contentAlignment = Alignment.Center, - ) { - ChatTheme.componentFactory.UserAvatar( - params = UserAvatarParams( - modifier = Modifier - .size(AvatarSize.Large) - .testTag("Stream_UserAvatar"), - user = currentUser, - ), - ) - } - } else { - Spacer(modifier = Modifier.size(AvatarSize.ExtraLarge)) - } -} - -/** - * Represents the channel header's center slot. It either shows a [Text] if [connectionState] is - * [ConnectionState.CONNECTED], or a [NetworkLoadingIndicator] if there is no connections. - * - * @param connectionState The state of WebSocket connection. - * @param title The title to show. - */ -@Composable -internal fun RowScope.DefaultChannelListHeaderCenterContent( - connectionState: ConnectionState, - title: String, -) { - 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, - ) - } - } + elevation = elevation, + leadingContent = leadingContent, + centerContent = centerContent, + trailingContent = trailingContent, + ) } /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.kt new file mode 100644 index 00000000000..b8f3f91013e --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/ListHeader.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens +import io.getstream.chat.android.compose.ui.theme.UserAvatarParams +import io.getstream.chat.android.compose.ui.util.clickable +import io.getstream.chat.android.models.ConnectionState +import io.getstream.chat.android.models.User + +/** + * Common header layout. + * + * @param modifier Modifier for styling. + * @param color The background color of the header. + * @param shape The shape of the header. + * @param elevation The elevation of the header. + * @param leadingContent Composable for the leading slot (e.g. user avatar). + * @param centerContent Composable for the center slot (e.g. title or loading indicator). + * @param trailingContent Composable for the trailing slot (e.g. action button or spacer). + */ +@Composable +internal fun ListHeader( + modifier: Modifier = Modifier, + color: Color = ChatTheme.colors.backgroundCoreElevation1, + shape: Shape = RectangleShape, + elevation: Dp = 0.dp, + leadingContent: @Composable RowScope.() -> Unit, + centerContent: @Composable RowScope.() -> Unit, + trailingContent: @Composable RowScope.() -> Unit, +) { + Surface( + modifier = modifier + .fillMaxWidth(), + shadowElevation = elevation, + color = color, + shape = shape, + ) { + Column { + Row( + Modifier + .fillMaxWidth() + .padding(StreamTokens.spacingSm), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { + leadingContent() + + centerContent() + + trailingContent() + } + HorizontalDivider( + thickness = 1.dp, + color = ChatTheme.colors.borderCoreDefault, + ) + } + } +} + +/** + * Default leading content for a [ListHeader]. + * Shows the user avatar if available, otherwise a spacer to preserve alignment. + * + * @param currentUser The currently logged in user. + * @param onAvatarClick Action invoked when the avatar is clicked. + */ +@Composable +internal fun DefaultListHeaderLeadingContent( + currentUser: User?, + onAvatarClick: (User?) -> Unit, +) { + if (currentUser != null) { + Box( + modifier = Modifier + .size(AvatarSize.ExtraLarge) + .clip(CircleShape) + .clickable { onAvatarClick(currentUser) }, + contentAlignment = Alignment.Center, + ) { + ChatTheme.componentFactory.UserAvatar( + params = UserAvatarParams( + modifier = Modifier + .size(AvatarSize.Large) + .testTag("Stream_UserAvatar"), + user = currentUser, + ), + ) + } + } else { + Spacer(modifier = Modifier.size(AvatarSize.ExtraLarge)) + } +} + +/** + * Default center content for a [ListHeader]. + * Shows a title when connected, a loading indicator when connecting, or "Disconnected" when offline. + * + * @param connectionState The state of WebSocket connection. + * @param title The title to show. + */ +@Composable +internal fun RowScope.DefaultListHeaderCenterContent( + connectionState: ConnectionState, + title: String, +) { + 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, + ) + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt index aceaa979b95..b6294df7486 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt @@ -60,8 +60,6 @@ import io.getstream.chat.android.compose.state.messages.attachments.PollPickerMo import io.getstream.chat.android.compose.ui.attachments.content.UnsupportedAttachmentContent import io.getstream.chat.android.compose.ui.attachments.content.onFileAttachmentContentItemClick import io.getstream.chat.android.compose.ui.channel.info.ChannelInfoNavigationIcon -import io.getstream.chat.android.compose.ui.channels.header.DefaultChannelListHeaderCenterContent -import io.getstream.chat.android.compose.ui.channels.header.DefaultChannelListHeaderLeadingContent import io.getstream.chat.android.compose.ui.channels.header.DefaultChannelListHeaderTrailingContent import io.getstream.chat.android.compose.ui.channels.info.DefaultSelectedChannelMenuHeaderContent import io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenu @@ -80,6 +78,8 @@ import io.getstream.chat.android.compose.ui.channels.list.DefaultSearchResultIte import io.getstream.chat.android.compose.ui.channels.list.LocalSwipeRevealCoordinator import io.getstream.chat.android.compose.ui.channels.list.SearchResultItem import io.getstream.chat.android.compose.ui.channels.list.SwipeableChannelItem +import io.getstream.chat.android.compose.ui.components.DefaultListHeaderCenterContent +import io.getstream.chat.android.compose.ui.components.DefaultListHeaderLeadingContent import io.getstream.chat.android.compose.ui.components.DefaultSearchClearButton import io.getstream.chat.android.compose.ui.components.DefaultSearchLabel import io.getstream.chat.android.compose.ui.components.DefaultSearchLeadingIcon @@ -262,7 +262,7 @@ public interface ChatComponentFactory { */ @Composable public fun RowScope.ChannelListHeaderLeadingContent(params: ChannelListHeaderLeadingContentParams) { - DefaultChannelListHeaderLeadingContent( + DefaultListHeaderLeadingContent( currentUser = params.currentUser, onAvatarClick = params.onAvatarClick, ) @@ -278,7 +278,7 @@ public interface ChatComponentFactory { */ @Composable public fun RowScope.ChannelListHeaderCenterContent(params: ChannelListHeaderCenterContentParams) { - DefaultChannelListHeaderCenterContent( + DefaultListHeaderCenterContent( connectionState = params.connectionState, title = params.title, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt index ca100456b9f..ad20f422ae4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadListHeader.kt @@ -28,7 +28,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.ui.channels.header.ChannelListHeader +import io.getstream.chat.android.compose.ui.components.DefaultListHeaderCenterContent +import io.getstream.chat.android.compose.ui.components.DefaultListHeaderLeadingContent +import io.getstream.chat.android.compose.ui.components.ListHeader import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.models.ConnectionState @@ -37,7 +39,7 @@ import io.getstream.chat.android.previewdata.PreviewUserData /** * A header composable for the thread list screen. - * Internally reuses [ChannelListHeader] with no trailing action button. + * Uses the shared [ListHeader] layout with no trailing action button. * * @param modifier Modifier for styling. * @param title The title to display, when the network is available. @@ -59,15 +61,23 @@ public fun ThreadListHeader( elevation: Dp = 0.dp, onAvatarClick: (User?) -> Unit = {}, ) { - ChannelListHeader( + ListHeader( modifier = modifier, - title = title, - currentUser = currentUser, - connectionState = connectionState, color = color, shape = shape, elevation = elevation, - onAvatarClick = onAvatarClick, + leadingContent = { + DefaultListHeaderLeadingContent( + currentUser = currentUser, + onAvatarClick = onAvatarClick, + ) + }, + centerContent = { + DefaultListHeaderCenterContent( + connectionState = connectionState, + title = title, + ) + }, trailingContent = { Spacer(modifier = Modifier.size(AvatarSize.ExtraLarge)) }, From 94655557b6411760ac7e82fba1ff51177898c56c Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:51:44 +0200 Subject: [PATCH 3/3] Dump API --- .../api/stream-chat-android-compose.api | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index f65383190ef..3bd25852922 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -6211,9 +6211,9 @@ public final class io/getstream/chat/android/compose/ui/threads/ComposableSingle public final class io/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListHeaderKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListHeaderKt; public fun ()V - public final fun getLambda$-1674084940$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda$-186248340$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda$-2127007699$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-509567249$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-549714838$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$1494410151$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; } public final class io/getstream/chat/android/compose/ui/threads/ComposableSingletons$ThreadListKt {