Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ChatComponentFactory #5565

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@

### ⬆️ Improved
- Create snapshot tests for channels stateless components. [#5570](https://github.com/GetStream/stream-chat-android/pull/5570)
- Introduce `ChatComponentFactory` for easier channel components customization. Initially supporting channel stateless components. [#5565](https://github.com/GetStream/stream-chat-android/pull/5565)

### ✅ Added

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2014-2025 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.sample.ui.component

import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Build
import androidx.compose.material.icons.rounded.Face
import androidx.compose.material.icons.rounded.ThumbUp
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.getstream.chat.android.compose.state.channels.list.ItemState
import io.getstream.chat.android.compose.ui.theme.ChatComponentFactory
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.models.Channel
import io.getstream.chat.android.models.ConnectionState
import io.getstream.chat.android.models.User

class CustomChatComponentFactory : ChatComponentFactory(
channelListHeader = object : ChannelListHeader() {
@Composable
override fun RowScope.LeadingContent(
currentUser: User?,
onAvatarClick: (User?) -> Unit,
) {
Icon(
imageVector = Icons.Rounded.Face,
contentDescription = null,
)
}

@Composable
override fun RowScope.CenterContent(
connectionState: ConnectionState,
title: String,
) {
Text(
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f),
text = title,
textAlign = TextAlign.Center,
color = ChatTheme.colors.primaryAccent,
)
}

@Composable
override fun RowScope.TrailingContent(
onHeaderActionClick: () -> Unit,
) {
Icon(
imageVector = Icons.Rounded.ThumbUp,
contentDescription = null,
)
}
},
searchInput = object : SearchInput() {
@Composable
override fun RowScope.LeadingIcon() {
Icon(
imageVector = Icons.Rounded.Build,
contentDescription = null,
)
}

@Composable
override fun Label() {
Text(
text = "Search",
color = ChatTheme.colors.textHighEmphasis,
)
}
},
channelList = object : ChannelList() {
@Composable
override fun LazyItemScope.ChannelContent(
channelItem: ItemState.ChannelItemState,
currentUser: User?,
onChannelClick: (Channel) -> Unit,
onChannelLongClick: (Channel) -> Unit,
) {
Text(
text = channelItem.channel.name,
color = ChatTheme.colors.textHighEmphasis,
)
}

@Composable
override fun LoadingContent(modifier: Modifier) {
Text(
text = "Loading...",
color = ChatTheme.colors.textHighEmphasis,
)
}

@Composable
override fun BoxScope.HelperContent() {
Text(
text = "Helper content",
color = ChatTheme.colors.textLowEmphasis,
)
}
},
)
70 changes: 56 additions & 14 deletions stream-chat-android-compose/api/stream-chat-android-compose.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public sealed class ItemState {
*/
public data class SearchResultItemState(
val message: Message,
val channel: Channel?,
val channel: Channel? = null,
) : ItemState() {
override val key: String = message.id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,27 @@ public fun ChannelListHeader(
onAvatarClick: (User?) -> Unit = {},
onHeaderActionClick: () -> Unit = {},
leadingContent: @Composable RowScope.() -> Unit = {
DefaultChannelHeaderLeadingContent(
currentUser = currentUser,
onAvatarClick = onAvatarClick,
)
with(ChatTheme.componentFactory.channelListHeader) {
LeadingContent(
currentUser = currentUser,
onAvatarClick = onAvatarClick,
)
}
},
centerContent: @Composable RowScope.() -> Unit = {
DefaultChannelListHeaderCenterContent(
connectionState = connectionState,
title = title,
)
with(ChatTheme.componentFactory.channelListHeader) {
CenterContent(
connectionState = connectionState,
title = title,
)
}
},
trailingContent: @Composable RowScope.() -> Unit = {
DefaultChannelListHeaderTrailingContent(
onHeaderActionClick = onHeaderActionClick,
)
with(ChatTheme.componentFactory.channelListHeader) {
TrailingContent(
onHeaderActionClick = onHeaderActionClick,
)
}
},
) {
Surface(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,28 @@ public fun ChannelItem(
onChannelLongClick: (Channel) -> Unit,
modifier: Modifier = Modifier,
leadingContent: @Composable RowScope.(ItemState.ChannelItemState) -> Unit = {
DefaultChannelItemLeadingContent(
channelItem = it,
currentUser = currentUser,
)
with(ChatTheme.componentFactory.channelList) {
ChannelItemLeadingContent(
channelItem = channelItem,
currentUser = currentUser,
)
}
},
centerContent: @Composable RowScope.(ItemState.ChannelItemState) -> Unit = {
DefaultChannelItemCenterContent(
channelItemState = it,
currentUser = currentUser,
)
with(ChatTheme.componentFactory.channelList) {
ChannelItemCenterContent(
channelItem = channelItem,
currentUser = currentUser,
)
}
},
trailingContent: @Composable RowScope.(ItemState.ChannelItemState) -> Unit = {
DefaultChannelItemTrailingContent(
channel = it.channel,
currentUser = currentUser,
)
with(ChatTheme.componentFactory.channelList) {
ChannelItemTrailingContent(
channelItem = channelItem,
currentUser = currentUser,
)
}
},
) {
val channel = channelItem.channel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -95,34 +96,54 @@ public fun ChannelList(
onChannelClick: (Channel) -> Unit = {},
onChannelLongClick: (Channel) -> Unit = remember(viewModel) { { viewModel.selectChannel(it) } },
onSearchResultClick: (Message) -> Unit = {},
loadingContent: @Composable () -> Unit = { LoadingIndicator(modifier) },
emptyContent: @Composable () -> Unit = { DefaultChannelListEmptyContent(modifier) },
loadingContent: @Composable () -> Unit = {
ChatTheme.componentFactory.channelList.LoadingContent(modifier = modifier)
},
emptyContent: @Composable () -> Unit = {
ChatTheme.componentFactory.channelList.EmptyContent(modifier = modifier)
},
emptySearchContent: @Composable (String) -> Unit = { searchQuery ->
DefaultChannelSearchEmptyContent(
ChatTheme.componentFactory.channelList.EmptySearchContent(
searchQuery = searchQuery,
modifier = modifier,
)
},
helperContent: @Composable BoxScope.() -> Unit = {},
loadingMoreContent: @Composable () -> Unit = { DefaultChannelsLoadingMoreIndicator() },
channelContent: @Composable (ItemState.ChannelItemState) -> Unit = { itemState ->
helperContent: @Composable BoxScope.() -> Unit = {
with(ChatTheme.componentFactory.channelList) {
HelperContent()
}
},
loadingMoreContent: @Composable LazyItemScope.() -> Unit = {
with(ChatTheme.componentFactory.channelList) {
LoadingMoreContent()
}
},
channelContent: @Composable LazyItemScope.(ItemState.ChannelItemState) -> Unit = { itemState ->
val user by viewModel.user.collectAsState()
DefaultChannelItem(
channelItem = itemState,
currentUser = user,
onChannelClick = onChannelClick,
onChannelLongClick = onChannelLongClick,
)
with(ChatTheme.componentFactory.channelList) {
ChannelContent(
channelItem = itemState,
currentUser = user,
onChannelClick = onChannelClick,
onChannelLongClick = onChannelLongClick,
)
}
},
searchResultContent: @Composable (ItemState.SearchResultItemState) -> Unit = { itemState ->
searchResultContent: @Composable LazyItemScope.(ItemState.SearchResultItemState) -> Unit = { itemState ->
val user by viewModel.user.collectAsState()
DefaultSearchResultItem(
searchResultItemState = itemState,
currentUser = user,
onSearchResultClick = onSearchResultClick,
)
with(ChatTheme.componentFactory.channelList) {
SearchResultContent(
searchResultItem = itemState,
currentUser = user,
onSearchResultClick = onSearchResultClick,
)
}
},
divider: @Composable LazyItemScope.() -> Unit = {
with(ChatTheme.componentFactory.channelList) {
Divider()
}
},
divider: @Composable () -> Unit = { DefaultChannelItemDivider() },
) {
val user by viewModel.user.collectAsState()

Expand Down Expand Up @@ -193,32 +214,52 @@ public fun ChannelList(
onChannelClick: (Channel) -> Unit = {},
onChannelLongClick: (Channel) -> Unit = {},
onSearchResultClick: (Message) -> Unit = {},
loadingContent: @Composable () -> Unit = { DefaultChannelListLoadingIndicator(modifier) },
emptyContent: @Composable () -> Unit = { DefaultChannelListEmptyContent(modifier) },
loadingContent: @Composable () -> Unit = {
ChatTheme.componentFactory.channelList.LoadingContent(modifier = modifier)
},
emptyContent: @Composable () -> Unit = {
ChatTheme.componentFactory.channelList.EmptyContent(modifier = modifier)
},
emptySearchContent: @Composable (String) -> Unit = { searchQuery ->
DefaultChannelSearchEmptyContent(
ChatTheme.componentFactory.channelList.EmptySearchContent(
searchQuery = searchQuery,
modifier = modifier,
)
},
helperContent: @Composable BoxScope.() -> Unit = {},
loadingMoreContent: @Composable () -> Unit = { DefaultChannelsLoadingMoreIndicator() },
channelContent: @Composable (ItemState.ChannelItemState) -> Unit = { itemState ->
DefaultChannelItem(
channelItem = itemState,
currentUser = currentUser,
onChannelClick = onChannelClick,
onChannelLongClick = onChannelLongClick,
)
helperContent: @Composable BoxScope.() -> Unit = {
with(ChatTheme.componentFactory.channelList) {
HelperContent()
}
},
searchResultContent: @Composable (ItemState.SearchResultItemState) -> Unit = { itemState ->
DefaultSearchResultItem(
searchResultItemState = itemState,
currentUser = currentUser,
onSearchResultClick = onSearchResultClick,
)
loadingMoreContent: @Composable LazyItemScope.() -> Unit = {
with(ChatTheme.componentFactory.channelList) {
LoadingMoreContent()
}
},
channelContent: @Composable LazyItemScope.(ItemState.ChannelItemState) -> Unit = { channelItem ->
with(ChatTheme.componentFactory.channelList) {
ChannelContent(
channelItem = channelItem,
currentUser = currentUser,
onChannelClick = onChannelClick,
onChannelLongClick = onChannelLongClick,
)
}
},
searchResultContent: @Composable LazyItemScope.(ItemState.SearchResultItemState) -> Unit = { searchResultItem ->
with(ChatTheme.componentFactory.channelList) {
SearchResultContent(
searchResultItem = searchResultItem,
currentUser = currentUser,
onSearchResultClick = onSearchResultClick,
)
}
},
divider: @Composable LazyItemScope.() -> Unit = {
with(ChatTheme.componentFactory.channelList) {
Divider()
}
},
divider: @Composable () -> Unit = { DefaultChannelItemDivider() },
) {
val (isLoading, _, _, channels, searchQuery) = channelsState

Expand All @@ -242,6 +283,7 @@ public fun ChannelList(
divider = divider,
)
}

isLoading -> loadingContent()
searchQuery.query.isBlank() -> emptyContent()
else -> emptySearchContent(searchQuery.query)
Expand All @@ -256,10 +298,10 @@ public fun ChannelList(
* @param searchResultContent Composable that represents the search result item.
*/
@Composable
internal fun WrapperItemContent(
internal fun LazyItemScope.WrapperItemContent(
itemState: ItemState,
channelContent: @Composable (ItemState.ChannelItemState) -> Unit,
searchResultContent: @Composable (ItemState.SearchResultItemState) -> Unit,
channelContent: @Composable LazyItemScope.(ItemState.ChannelItemState) -> Unit,
searchResultContent: @Composable LazyItemScope.(ItemState.SearchResultItemState) -> Unit,
) {
when (itemState) {
is ItemState.ChannelItemState -> channelContent(itemState)
Expand Down
Loading
Loading