From 948b979e536163f9420f772f478656035819dd63 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Feb 2026 18:10:23 +0100 Subject: [PATCH 01/57] feat(compose): update ChannelsScreen and SearchInput to Figma tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChannelsScreen: search section padding top 8dp→16dp, horizontal 12dp→16dp (spacingMd), bottom 8dp (spacingXs) - SearchInput DefaultSearchLeadingIcon: 16x16dp size, leading 16dp padding, tint textLowEmphasis→textTertiary - SearchInput DefaultSearchLabel: typography body→bodyDefault, color textLowEmphasis→textTertiary - SearchInput: add 48dp minHeight, innerPadding start=spacingXs/end=spacingMd/v=spacingSm - InputField: border color borders→borderCoreDefault --- .../compose/ui/channels/ChannelsScreen.kt | 8 +++++++- .../compose/ui/components/SearchInput.kt | 20 ++++++++++++++----- .../ui/components/composer/InputField.kt | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt index f03c91128a6..bb2d8704c64 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt @@ -49,6 +49,7 @@ import io.getstream.chat.android.compose.state.channels.list.SearchQuery import io.getstream.chat.android.compose.ui.channels.list.ChannelList import io.getstream.chat.android.compose.ui.components.SimpleDialog import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.compose.viewmodel.channels.ChannelViewModelFactory import io.getstream.chat.android.models.Channel @@ -145,7 +146,12 @@ public fun ChannelsScreen( ChatTheme.componentFactory.ChannelListSearchInput( modifier = Modifier .testTag("Stream_SearchInput") - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding( + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingXs, + start = StreamTokens.spacingMd, + end = StreamTokens.spacingMd, + ) .fillMaxWidth(), query = searchQuery, onSearchStarted = {}, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt index 3eb7fece825..196bd4b2b06 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -43,6 +44,7 @@ import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.components.composer.InputField import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens /** * The search component that allows the user to fill in a search query and filter their items. @@ -89,6 +91,7 @@ public fun SearchInput( InputField( modifier = modifier + .defaultMinSize(minHeight = 48.dp) .onFocusEvent { newState -> val wasPreviouslyFocused = isFocused @@ -119,7 +122,12 @@ public fun SearchInput( } }, maxLines = 1, - innerPadding = PaddingValues(4.dp), + innerPadding = PaddingValues( + start = StreamTokens.spacingXs, // 8dp gap after icon + end = StreamTokens.spacingMd, // 16dp trailing padding + top = StreamTokens.spacingSm, // 12dp vertical + bottom = StreamTokens.spacingSm, // 12dp vertical + ), ) } @@ -129,10 +137,12 @@ public fun SearchInput( @Composable internal fun DefaultSearchLeadingIcon() { Icon( - modifier = Modifier.padding(horizontal = 6.dp), + modifier = Modifier + .padding(start = StreamTokens.spacingMd) // 16dp leading padding, matches Figma input content px + .size(16.dp), painter = painterResource(id = R.drawable.stream_compose_ic_search), contentDescription = null, - tint = ChatTheme.colors.textLowEmphasis, + tint = ChatTheme.colors.textTertiary, ) } @@ -143,8 +153,8 @@ internal fun DefaultSearchLeadingIcon() { internal fun DefaultSearchLabel() { Text( text = stringResource(id = R.string.stream_compose_search_input_hint), - style = ChatTheme.typography.body, - color = ChatTheme.colors.textLowEmphasis, + style = ChatTheme.typography.bodyDefault, + color = ChatTheme.colors.textTertiary, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/InputField.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/InputField.kt index 93e4f0f2667..9c17f8d4766 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/InputField.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/InputField.kt @@ -76,7 +76,7 @@ public fun InputField( modifier: Modifier = Modifier, enabled: Boolean = true, maxLines: Int = Int.MAX_VALUE, - border: BorderStroke = BorderStroke(1.dp, ChatTheme.colors.borders), + border: BorderStroke = BorderStroke(1.dp, ChatTheme.colors.borderCoreDefault), innerPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), keyboardOptions: KeyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), visualTransformation: VisualTransformation = DefaultInputFieldVisualTransformation( From 5f6cc82c96ea3471968c457c7e5968334a180083 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Feb 2026 18:11:18 +0100 Subject: [PATCH 02/57] feat(compose): update ChannelListHeader to Figma spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Container: padding 8dp→spacingSm (12dp), add spacedBy(spacingXs) gap between children - Add 1dp bottom border (borderCoreDefault) replacing drop shadow - Elevation default: headerElevation (4dp)→0.dp - Title typography: title3Bold→headingSmall, color: textHighEmphasis→textPrimary - Title padding: 16.dp→spacingMd (token-aligned) - Leading avatar: 40dp visual wrapped in 48dp hit target Box - Trailing button: accentPrimary bg, CircleShape, 0dp elevation, 48dp hit target --- .../ui/channels/header/ChannelListHeader.kt | 109 +++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) 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 b91814820e6..751a16cf121 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,6 +16,9 @@ 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 @@ -24,6 +27,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize 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 @@ -41,6 +46,7 @@ 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.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.ui.util.clickable import io.getstream.chat.android.models.ConnectionState import io.getstream.chat.android.models.User @@ -75,7 +81,7 @@ public fun ChannelListHeader( connectionState: ConnectionState, color: Color = ChatTheme.colors.barsBackground, shape: Shape = ChatTheme.shapes.header, - elevation: Dp = ChatTheme.dimens.headerElevation, + elevation: Dp = 0.dp, onAvatarClick: (User?) -> Unit = {}, onHeaderActionClick: () -> Unit = {}, leadingContent: @Composable RowScope.() -> Unit = { @@ -109,17 +115,25 @@ public fun ChannelListHeader( color = color, shape = shape, ) { - Row( - Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - leadingContent() + Column { + Row( + Modifier + .fillMaxWidth() + .padding(StreamTokens.spacingSm), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { + leadingContent() - centerContent() + centerContent() - trailingContent() + trailingContent() + } + // Bottom border (replaces drop shadow per Figma spec) + HorizontalDivider( + thickness = 1.dp, + color = ChatTheme.colors.borderCoreDefault, + ) } } } @@ -134,19 +148,27 @@ internal fun DefaultChannelHeaderLeadingContent( currentUser: User?, onAvatarClick: (User?) -> Unit, ) { - val size = Modifier.size(ChatTheme.dimens.channelAvatarSize) + val hitTargetSize = 48.dp + val avatarSize = ChatTheme.dimens.channelAvatarSize // 40dp if (currentUser != null) { - ChatTheme.componentFactory.UserAvatar( - modifier = size - .clickable { onAvatarClick(currentUser) } - .testTag("Stream_UserAvatar"), - user = currentUser, - showIndicator = false, - showBorder = false, - ) + Box( + modifier = Modifier + .size(hitTargetSize) + .clickable { onAvatarClick(currentUser) }, + contentAlignment = Alignment.Center, + ) { + ChatTheme.componentFactory.UserAvatar( + modifier = Modifier + .size(avatarSize) + .testTag("Stream_UserAvatar"), + user = currentUser, + showIndicator = false, + showBorder = false, + ) + } } else { - Spacer(modifier = size) + Spacer(modifier = Modifier.size(hitTargetSize)) } } @@ -168,11 +190,11 @@ internal fun RowScope.DefaultChannelListHeaderCenterContent( modifier = Modifier .weight(1f) .wrapContentWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = StreamTokens.spacingMd), text = title, - style = ChatTheme.typography.title3Bold, + style = ChatTheme.typography.headingSmall, maxLines = 1, - color = ChatTheme.colors.textHighEmphasis, + color = ChatTheme.colors.textPrimary, ) } @@ -182,11 +204,11 @@ internal fun RowScope.DefaultChannelListHeaderCenterContent( modifier = Modifier .weight(1f) .wrapContentWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = StreamTokens.spacingMd), text = stringResource(R.string.stream_compose_disconnected), - style = ChatTheme.typography.title3Bold, + style = ChatTheme.typography.headingSmall, maxLines = 1, - color = ChatTheme.colors.textHighEmphasis, + color = ChatTheme.colors.textPrimary, ) } } @@ -201,21 +223,28 @@ internal fun RowScope.DefaultChannelListHeaderCenterContent( internal fun DefaultChannelListHeaderTrailingContent( onHeaderActionClick: () -> Unit, ) { - Surface( - modifier = Modifier.size(40.dp), - onClick = onHeaderActionClick, - color = ChatTheme.colors.primaryAccent, - shape = ChatTheme.shapes.avatar, - shadowElevation = 4.dp, + Box( + modifier = Modifier + .size(48.dp) + .clickable(onClick = onHeaderActionClick), + contentAlignment = Alignment.Center, ) { - Icon( - modifier = Modifier - .wrapContentSize() - .testTag("Stream_CreateChannelIcon"), - painter = painterResource(id = R.drawable.stream_compose_ic_new_chat), - contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), - tint = Color.White, - ) + Surface( + modifier = Modifier.size(40.dp), + onClick = onHeaderActionClick, + color = ChatTheme.colors.accentPrimary, + shape = CircleShape, + shadowElevation = 0.dp, + ) { + Icon( + modifier = Modifier + .padding(horizontal = 10.dp, vertical = 10.dp) + .testTag("Stream_CreateChannelIcon"), + painter = painterResource(id = R.drawable.stream_compose_ic_new_chat), + contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), + tint = Color.White, + ) + } } } From 10525ffc14b6f6068a6e67ebb15fca0c7c4b0bff Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Feb 2026 18:12:43 +0100 Subject: [PATCH 03/57] feat(compose): restructure ChannelItem layout per Figma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Leading: avatar 40dp→channelListItemAvatarSize (48dp), all padding spacingMd (16dp) - Center content major restructure: timestamp+badge moved from trailing→title row, delivery status moved→message row - Channel name: bodyBold+fontSize16→bodyDefault, textHighEmphasis→textPrimary - Mute icon: moved inline into title row, textLowEmphasis→textTertiary - Message preview: body→captionDefault, textLowEmphasis→textSecondary - Typing indicator: gap 6dp→spacing2xs, body→captionDefault, textLowEmphasis→textSecondary - Trailing content: replaced with empty Spacer stub (spacingMd=16dp) for API compatibility --- .../compose/ui/channels/list/ChannelItem.kt | 195 +++++++++--------- 1 file changed, 103 insertions(+), 92 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index a02e639c10b..16e3e5eca6a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -25,9 +25,11 @@ import androidx.compose.foundation.layout.Arrangement 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.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -46,7 +48,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import io.getstream.chat.android.client.extensions.currentUserUnreadCount import io.getstream.chat.android.client.extensions.getCreatedAtOrNull import io.getstream.chat.android.client.extensions.internal.NEVER @@ -55,6 +56,7 @@ import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.TypingIndicator 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.util.getLastMessage import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.DraftMessage @@ -156,12 +158,12 @@ internal fun DefaultChannelItemLeadingContent( ChatTheme.componentFactory.ChannelAvatar( modifier = Modifier .padding( - start = ChatTheme.dimens.channelItemHorizontalPadding, - end = 4.dp, - top = ChatTheme.dimens.channelItemVerticalPadding, - bottom = ChatTheme.dimens.channelItemVerticalPadding, + start = StreamTokens.spacingMd, // 16dp (was channelItemHorizontalPadding = 8dp) + end = StreamTokens.spacingMd, // 16dp gap between avatar and content column + top = StreamTokens.spacingMd, // 16dp (was channelItemVerticalPadding = 12dp) + bottom = StreamTokens.spacingMd, // 16dp (was channelItemVerticalPadding = 12dp) ) - .size(ChatTheme.dimens.channelAvatarSize), + .size(ChatTheme.dimens.channelListItemAvatarSize), // 48dp (new dimen, NOT channelAvatarSize which = 40dp for header) channel = channelItem.channel, currentUser = currentUser, showIndicator = false, @@ -181,64 +183,114 @@ internal fun RowScope.DefaultChannelItemCenterContent( channelItemState: ItemState.ChannelItemState, currentUser: User?, ) { + val channel = channelItemState.channel + val lastMessage = channel.getLastMessage(currentUser) + val unreadCount = channel.currentUserUnreadCount(currentUserId = currentUser?.id) + val isLastMessageFromCurrentUser = lastMessage?.user?.id == currentUser?.id + Column( modifier = Modifier - .padding(start = 4.dp, end = 4.dp) .weight(1f) - .wrapContentHeight(), - verticalArrangement = Arrangement.Center, + .padding(vertical = StreamTokens.spacing3xs), // 2dp vertical padding + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp gap between title and message rows ) { - val channelName: (@Composable (modifier: Modifier) -> Unit) = @Composable { + // ── Title Row: [Name] [MuteIcon?] [Timestamp] [Badge?] ────────────── + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp default gap + ) { + // Channel name (flex-1) Text( - modifier = it.testTag("Stream_ChannelName"), - text = ChatTheme.channelNameFormatter.formatChannelName(channelItemState.channel, currentUser), - style = ChatTheme.typography.bodyBold, - fontSize = 16.sp, + modifier = Modifier + .testTag("Stream_ChannelName") + .weight(1f), + text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser), + style = ChatTheme.typography.bodyDefault, // 16sp Regular (was bodyBold + fontSize=16.sp override) maxLines = 1, overflow = TextOverflow.Ellipsis, - color = ChatTheme.colors.textHighEmphasis, + color = ChatTheme.colors.textPrimary, // was textHighEmphasis ) - } - - if (channelItemState.isMuted) { - Row(verticalAlignment = Alignment.CenterVertically) { - channelName(Modifier.weight(weight = 1f, fill = false)) + // Mute icon (optional, inline in title row after name) + if (channelItemState.isMuted) { Icon( modifier = Modifier .testTag("Stream_ChannelMutedIcon") - .padding(start = 8.dp) .size(16.dp), painter = painterResource(id = R.drawable.stream_compose_ic_muted), contentDescription = null, - tint = ChatTheme.colors.textLowEmphasis, + tint = ChatTheme.colors.textTertiary, // was textLowEmphasis ) } - } else { - channelName(Modifier) + + // Trailing: Timestamp + Badge (gap = spacing/xs = 8dp between them) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), // 8dp between timestamp and badge + ) { + // Timestamp + if (lastMessage != null) { + Timestamp( + date = lastMessage.getCreatedAtOrNull(), + textStyle = ChatTheme.typography.captionDefault.copy( + color = ChatTheme.colors.textTertiary, + ), + ) + } + + // Unread badge (optional) + if (unreadCount > 0) { + ChatTheme.componentFactory.ChannelItemUnreadCountIndicator( + unreadCount = unreadCount, + modifier = Modifier, + ) + } + } } - if (channelItemState.typingUsers.isNotEmpty()) { - UserTypingIndicator(channelItemState.typingUsers) - } else { - val lastMessageText = - channelItemState.draftMessage - ?.let { ChatTheme.messagePreviewFormatter.formatDraftMessagePreview(it) } - ?: channelItemState.channel.getLastMessage(currentUser)?.let { lastMessage -> - ChatTheme.messagePreviewFormatter.formatMessagePreview(lastMessage, currentUser) - } - ?: AnnotatedString("") - - if (lastMessageText.isNotEmpty()) { - Text( - modifier = Modifier.testTag("Stream_MessagePreview"), - text = lastMessageText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.body, - color = ChatTheme.colors.textLowEmphasis, - inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), - ) + // ── Message Row: [DeliveryStatus?] [MessagePreview or TypingIndicator] ── + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp gap + ) { + if (channelItemState.typingUsers.isNotEmpty()) { + // Typing indicator replaces message preview when active + UserTypingIndicator(channelItemState.typingUsers) + } else { + // Delivery status prefix: checkmark icon (only for own messages) + if (isLastMessageFromCurrentUser && lastMessage != null) { + ChatTheme.componentFactory.ChannelItemReadStatusIndicator( + channel = channel, + message = lastMessage, + currentUser = currentUser, + modifier = Modifier, + ) + } + + // Message preview text (draft overrides last message) + val lastMessageText = + channelItemState.draftMessage + ?.let { ChatTheme.messagePreviewFormatter.formatDraftMessagePreview(it) } + ?: lastMessage?.let { + ChatTheme.messagePreviewFormatter.formatMessagePreview(it, currentUser) + } + ?: AnnotatedString("") + + if (lastMessageText.isNotEmpty()) { + Text( + modifier = Modifier + .testTag("Stream_MessagePreview") + .weight(1f), + text = lastMessageText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ChatTheme.typography.captionDefault, + color = ChatTheme.colors.textSecondary, // was textLowEmphasis + inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), + ) + } } } } @@ -253,7 +305,7 @@ internal fun RowScope.DefaultChannelItemCenterContent( private fun UserTypingIndicator(users: List) { Row( modifier = Modifier, - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp (was 6dp) verticalAlignment = Alignment.CenterVertically, ) { TypingIndicator() @@ -267,8 +319,8 @@ private fun UserTypingIndicator(users: List) { ), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.body, - color = ChatTheme.colors.textLowEmphasis, + style = ChatTheme.typography.captionDefault, // was body + color = ChatTheme.colors.textSecondary, // was textLowEmphasis ) } } @@ -286,50 +338,9 @@ internal fun RowScope.DefaultChannelItemTrailingContent( channel: Channel, currentUser: User?, ) { - val lastMessage = channel.getLastMessage(currentUser) - - if (lastMessage != null) { - Column( - modifier = Modifier - .padding( - start = 4.dp, - end = ChatTheme.dimens.channelItemHorizontalPadding, - top = ChatTheme.dimens.channelItemVerticalPadding, - bottom = ChatTheme.dimens.channelItemVerticalPadding, - ) - .wrapContentHeight() - .align(Alignment.Bottom), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - val unreadCount = channel.currentUserUnreadCount(currentUserId = currentUser?.id) - - if (unreadCount > 0) { - ChatTheme.componentFactory.ChannelItemUnreadCountIndicator( - unreadCount = unreadCount, - modifier = Modifier, - ) - } - - val isLastMessageFromCurrentUser = lastMessage.user.id == currentUser?.id - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - if (isLastMessageFromCurrentUser) { - ChatTheme.componentFactory.ChannelItemReadStatusIndicator( - channel = channel, - message = lastMessage, - currentUser = currentUser, - modifier = Modifier, - ) - } - - Timestamp(date = lastMessage.getCreatedAtOrNull()) - } - } - } + // Timestamp, badge, and delivery status have moved to DefaultChannelItemCenterContent. + // This slot remains for API compatibility and provides the trailing end padding. + Spacer(modifier = Modifier.width(StreamTokens.spacingMd)) // 16dp end padding } @Preview(showBackground = true) From 2a32f9ffcab75c2f7becde56bf688d0658209493 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Feb 2026 18:13:33 +0100 Subject: [PATCH 04/57] feat(compose): update UnreadCountIndicator, Timestamp, and delivery status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UnreadCountIndicator: errorAccent (red)→accentPrimary (blue), add 2dp presenceBorder border, min 18dp→20dp, RoundedCornerShape(9)→pill (50), captionBold→numericExtraLarge, Color.White→badgeTextOnAccent - Timestamp: default style footnote→captionDefault (14sp), textLowEmphasis→textTertiary, add lineHeightNormal (20sp) - StreamDimens: add channelListItemAvatarSize=48.dp field (channelAvatarSize=40.dp retained for header) --- .../compose/ui/components/Timestamp.kt | 6 +++++- .../channels/UnreadCountIndicator.kt | 19 +++++++++++++------ .../android/compose/ui/theme/StreamDimens.kt | 4 ++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt index 5298fa9ec8e..6cb1c672ff6 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt @@ -27,6 +27,7 @@ import io.getstream.chat.android.compose.state.DateFormatType.DATE import io.getstream.chat.android.compose.state.DateFormatType.RELATIVE import io.getstream.chat.android.compose.state.DateFormatType.TIME import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.ui.common.helper.DateFormatter import java.util.Date @@ -45,7 +46,10 @@ public fun Timestamp( modifier: Modifier = Modifier, formatter: DateFormatter = ChatTheme.dateFormatter, formatType: DateFormatType = DATE, - textStyle: TextStyle = ChatTheme.typography.footnote.copy(ChatTheme.colors.textLowEmphasis), + textStyle: TextStyle = ChatTheme.typography.captionDefault.copy( // was footnote (12sp) + color = ChatTheme.colors.textTertiary, // was textLowEmphasis + lineHeight = StreamTokens.lineHeightNormal, // 20sp + ), ) { val timestamp = if (LocalInspectionMode.current) { "13:49" diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt index 6e580055374..bd1c65ae8bc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.ui.components.channels import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding @@ -31,6 +32,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens /** * Shows the unread count badge for each channel item, to showcase how many messages @@ -43,24 +45,29 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme public fun UnreadCountIndicator( unreadCount: Int, modifier: Modifier = Modifier, - color: Color = ChatTheme.colors.errorAccent, + color: Color = ChatTheme.colors.accentPrimary, // was errorAccent (red #FF3742) → accentPrimary (blue #005FFF) ) { val displayText = if (unreadCount > LimitTooManyUnreadCount) UnreadCountMany else unreadCount.toString() - val shape = RoundedCornerShape(9.dp) + val shape = RoundedCornerShape(50) // was RoundedCornerShape(9.dp) → pill/circle shape Box( modifier = modifier - .defaultMinSize(minWidth = 18.dp, minHeight = 18.dp) + .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) // was 18.dp → 20.dp + .border( + width = 2.dp, + color = ChatTheme.colors.presenceBorder, // white (#FFFFFF) -- reuses presence border token + shape = shape, + ) .background(shape = shape, color = color) - .padding(horizontal = 4.dp), + .padding(horizontal = StreamTokens.spacing2xs), // 4dp horizontal content padding contentAlignment = Alignment.Center, ) { Text( modifier = Modifier.testTag("Stream_UnreadCountIndicator"), text = displayText, - color = Color.White, + color = ChatTheme.colors.badgeTextOnAccent, // was Color.White (now uses semantic token) textAlign = TextAlign.Center, - style = ChatTheme.typography.captionBold, + style = ChatTheme.typography.numericExtraLarge, // was captionBold (10sp Bold) → numericExtraLarge (14sp Bold) ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt index cb4ae086962..494e954c869 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.unit.dp * @param channelItemVerticalPadding The vertical content padding inside channel list item. * @param channelItemHorizontalPadding The horizontal content padding inside channel list item. * @param channelAvatarSize The size of channel avatar. + * @param channelListItemAvatarSize The size of channel avatar in channel list items. Separate from [channelAvatarSize] + * which is used for the header avatar (40dp). Channel list items use 48dp per Figma spec. * @param selectedChannelMenuUserItemWidth The width of a member tile in the selected channel menu. * @param selectedChannelMenuUserItemHorizontalPadding The padding inside a member tile in the selected channel * menu. @@ -97,6 +99,7 @@ public data class StreamDimens( public val channelItemVerticalPadding: Dp, public val channelItemHorizontalPadding: Dp, public val channelAvatarSize: Dp, + public val channelListItemAvatarSize: Dp, // Size of channel avatar in list items (48dp). channelAvatarSize (40dp) is used for header avatar. public val selectedChannelMenuUserItemWidth: Dp, public val selectedChannelMenuUserItemHorizontalPadding: Dp, public val selectedChannelMenuUserItemAvatarSize: Dp, @@ -161,6 +164,7 @@ public data class StreamDimens( channelItemVerticalPadding = 12.dp, channelItemHorizontalPadding = 8.dp, channelAvatarSize = 40.dp, + channelListItemAvatarSize = 48.dp, selectedChannelMenuUserItemWidth = 80.dp, selectedChannelMenuUserItemHorizontalPadding = 8.dp, selectedChannelMenuUserItemAvatarSize = 64.dp, From 2fd84d00eb70556e1ad1edb716e319c039e73027 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Feb 2026 18:15:23 +0100 Subject: [PATCH 05/57] feat(compose): update SearchResultItem and SelectedChannelMenu tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchResultItem leading: avatar 40dp→48dp, padding channelItemHorizontalPadding→spacingMd (16dp) - SearchResultItem center: bodyBold+fontSize16→bodyDefault, textHighEmphasis→textPrimary, body→captionDefault, textLowEmphasis→textSecondary, add 4dp gap - SearchResultItem trailing: padding updated to spacingMd, timestamp footnote→captionDefault+textTertiary+20sp lineHeight, alignment Bottom→CenterVertically - SearchResultItem container Row: add Arrangement.spacedBy(spacingMd) for 16dp avatar-content gap - SelectedChannelMenu header: title3Bold→headingSmall, textHighEmphasis→textPrimary, footnoteBold→captionDefault, textLowEmphasis→textSecondary - ChannelOptions: all non-destructive options textHighEmphasis→textPrimary, textLowEmphasis→textSecondary (delete option unchanged) - ChatComponentFactory ChannelOptionsItem: bodyBold→bodyDefault - StreamHorizontalDivider: borders→borderCoreSurfaceSubtle --- .../ui/channels/info/SelectedChannelMenu.kt | 15 ++++-- .../ui/channels/list/SearchResultItem.kt | 50 +++++++++++-------- .../ui/components/StreamHorizontalDivider.kt | 2 +- .../ui/components/channels/ChannelOptions.kt | 24 ++++----- .../compose/ui/theme/ChatComponentFactory.kt | 2 +- 5 files changed, 54 insertions(+), 39 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt index e35743d7db0..6b69978fa93 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt @@ -39,6 +39,7 @@ import io.getstream.chat.android.compose.ui.components.SimpleMenu import io.getstream.chat.android.compose.ui.components.channels.ChannelMembers import io.getstream.chat.android.compose.ui.components.channels.buildDefaultChannelOptionsState 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.util.getMembersStatusText import io.getstream.chat.android.compose.ui.util.isOneToOne import io.getstream.chat.android.models.Channel @@ -127,11 +128,15 @@ internal fun DefaultSelectedChannelMenuHeaderContent( Text( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 16.dp), + .padding( + start = StreamTokens.spacingMd, // 16dp (was 16.dp, now token-aligned) + end = StreamTokens.spacingMd, // 16dp (was 16.dp) + top = StreamTokens.spacingMd, // 16dp (was 16.dp) + ), textAlign = TextAlign.Center, text = ChatTheme.channelNameFormatter.formatChannelName(selectedChannel, currentUser), - style = ChatTheme.typography.title3Bold, - color = ChatTheme.colors.textHighEmphasis, + style = ChatTheme.typography.headingSmall, // was title3Bold (18sp/W500) + color = ChatTheme.colors.textPrimary, // was textHighEmphasis maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -143,8 +148,8 @@ internal fun DefaultSelectedChannelMenuHeaderContent( currentUser = currentUser, userPresence = ChatTheme.userPresence, ), - style = ChatTheme.typography.footnoteBold, - color = ChatTheme.colors.textLowEmphasis, + style = ChatTheme.typography.captionDefault, // was footnoteBold + color = ChatTheme.colors.textSecondary, // was textLowEmphasis ) ChannelMembers( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt index fba5edae6e1..f100030e7f3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.unit.sp import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.ui.components.Timestamp 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.util.clickable import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User @@ -89,6 +90,7 @@ public fun SearchResultItem( Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingMd), // 16dp avatar-to-content gap ) { leadingContent(searchResultItemState) centerContent(searchResultItemState) @@ -121,12 +123,12 @@ internal fun DefaultSearchResultItemLeadingContent( user = user, modifier = Modifier .padding( - start = ChatTheme.dimens.channelItemHorizontalPadding, - end = 4.dp, - top = ChatTheme.dimens.channelItemVerticalPadding, - bottom = ChatTheme.dimens.channelItemVerticalPadding, + start = StreamTokens.spacingMd, // 16dp (was channelItemHorizontalPadding = 8dp) + end = 0.dp, // gap handled by Row's horizontalArrangement + top = StreamTokens.spacingMd, // 16dp (was channelItemVerticalPadding = 12dp) + bottom = StreamTokens.spacingMd, // 16dp ) - .size(ChatTheme.dimens.channelAvatarSize), + .size(48.dp), // 48dp (was channelAvatarSize = 40dp; Figma: 48dp) showIndicator = user.shouldShowOnlineIndicator( userPresence = ChatTheme.userPresence, currentUser = currentUser, @@ -150,18 +152,22 @@ internal fun RowScope.DefaultSearchResultItemCenterContent( ) { Column( modifier = Modifier - .padding(start = 4.dp, end = 4.dp) + .padding( + start = StreamTokens.spacing2xs, // 4dp (was 4.dp, same value but token-aligned) + end = StreamTokens.spacing2xs, // 4dp (was 4.dp) + top = StreamTokens.spacing3xs, // 2dp vertical padding + bottom = StreamTokens.spacing3xs, // 2dp + ) .weight(1f) .wrapContentHeight(), - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp gap ) { Text( text = ChatTheme.searchResultNameFormatter.formatMessageTitle(searchResultItemState, currentUser), - style = ChatTheme.typography.bodyBold, - fontSize = 16.sp, + style = ChatTheme.typography.bodyDefault, // was bodyBold + fontSize override maxLines = 1, overflow = TextOverflow.Ellipsis, - color = ChatTheme.colors.textHighEmphasis, + color = ChatTheme.colors.textPrimary, // was textHighEmphasis ) Text( @@ -171,8 +177,8 @@ internal fun RowScope.DefaultSearchResultItemCenterContent( ), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.body, - color = ChatTheme.colors.textLowEmphasis, + style = ChatTheme.typography.captionDefault, // was body + color = ChatTheme.colors.textSecondary, // was textLowEmphasis ) } } @@ -189,17 +195,21 @@ internal fun RowScope.DefaultSearchResultItemTrailingContent( Column( modifier = Modifier .padding( - start = 4.dp, - end = ChatTheme.dimens.channelItemHorizontalPadding, - top = ChatTheme.dimens.channelItemVerticalPadding, - bottom = ChatTheme.dimens.channelItemVerticalPadding, + start = StreamTokens.spacing2xs, // 4dp (was 4.dp) + end = StreamTokens.spacingMd, // 16dp (was channelItemHorizontalPadding = 8dp; Figma: spacing/md) + top = StreamTokens.spacingMd, // 16dp (was channelItemVerticalPadding = 12dp; Figma: spacing/md) + bottom = StreamTokens.spacingMd, // 16dp ) .wrapContentHeight() - .align(Alignment.Bottom), + .align(Alignment.CenterVertically), // was Alignment.Bottom; center-align with Figma layout horizontalAlignment = Alignment.End, ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Timestamp(date = searchResultItemState.message.createdAt) - } + Timestamp( + date = searchResultItemState.message.createdAt, + textStyle = ChatTheme.typography.captionDefault.copy( // was default footnote + color = ChatTheme.colors.textTertiary, // was textLowEmphasis + lineHeight = 20.sp, // Figma: line-height/normal = 20sp + ), + ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt index 9645d5adc14..f6e93deeb23 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt @@ -26,6 +26,6 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme internal fun StreamHorizontalDivider(thickness: Dp = .5.dp) { HorizontalDivider( thickness = thickness, - color = ChatTheme.colors.borders, + color = ChatTheme.colors.borderCoreSurfaceSubtle, // was borders (legacy token) ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt index ba2075639bb..ed166a63097 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt @@ -105,9 +105,9 @@ public fun buildDefaultChannelOptionsState( if (optionVisibility.isViewInfoVisible) { ChannelOptionState( title = stringResource(id = R.string.stream_compose_selected_channel_menu_view_info), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = ChatTheme.colors.textPrimary, // was textHighEmphasis iconPainter = painterResource(id = R.drawable.stream_compose_ic_person), - iconColor = ChatTheme.colors.textLowEmphasis, + iconColor = ChatTheme.colors.textSecondary, // was textLowEmphasis action = ViewInfo(selectedChannel), ) } else { @@ -116,9 +116,9 @@ public fun buildDefaultChannelOptionsState( if (optionVisibility.isLeaveChannelVisible && canLeaveChannel) { ChannelOptionState( title = stringResource(id = R.string.stream_compose_selected_channel_menu_leave_group), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = ChatTheme.colors.textPrimary, // was textHighEmphasis iconPainter = painterResource(id = R.drawable.stream_compose_ic_person_remove), - iconColor = ChatTheme.colors.textLowEmphasis, + iconColor = ChatTheme.colors.textSecondary, // was textLowEmphasis action = LeaveGroup(selectedChannel), ) } else { @@ -150,9 +150,9 @@ public fun buildDefaultChannelOptionsState( }, ChannelOptionState( title = stringResource(id = R.string.stream_compose_selected_channel_menu_dismiss), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = ChatTheme.colors.textPrimary, // was textHighEmphasis iconPainter = painterResource(id = R.drawable.stream_compose_ic_clear), - iconColor = ChatTheme.colors.textLowEmphasis, + iconColor = ChatTheme.colors.textSecondary, // was textLowEmphasis action = Cancel, ), ) @@ -185,9 +185,9 @@ private fun buildPinOption( }?.let { ChannelOptionState( title = stringResource(id = it.first), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = ChatTheme.colors.textPrimary, // was textHighEmphasis iconPainter = painterResource(id = it.second), - iconColor = ChatTheme.colors.textLowEmphasis, + iconColor = ChatTheme.colors.textSecondary, // was textLowEmphasis action = it.third, ) } @@ -219,9 +219,9 @@ private fun buildArchiveOption( }?.let { ChannelOptionState( title = stringResource(id = it.first), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = ChatTheme.colors.textPrimary, // was textHighEmphasis iconPainter = painterResource(id = it.second), - iconColor = ChatTheme.colors.textLowEmphasis, + iconColor = ChatTheme.colors.textSecondary, // was textLowEmphasis action = it.third, ) } @@ -248,9 +248,9 @@ private fun buildMuteOption( ChannelOptionState( title = stringResource(id = uiData.first), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = ChatTheme.colors.textPrimary, // was textHighEmphasis iconPainter = painterResource(id = uiData.second), - iconColor = ChatTheme.colors.textLowEmphasis, + iconColor = ChatTheme.colors.textSecondary, // was textLowEmphasis action = uiData.third, ) } else { 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 79f771d2f88..158f1619db3 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 @@ -2160,7 +2160,7 @@ public interface ChatComponentFactory { ChannelOptionsItemLeadingIcon(Modifier, option) }, onClick = onClick, - style = ChatTheme.typography.bodyBold, + style = ChatTheme.typography.bodyDefault, // was bodyBold (14sp/W500); Figma: body/default (16sp/Regular) itemHeight = 56.dp, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, From 6907fca10b3f62b3767da5e659593d40fa1f30bd Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 09:28:50 +0100 Subject: [PATCH 06/57] fix(compose): swap header icon from pencil to plus per Figma spec The Figma design uses a plus icon for the new channel button, not the pencil/edit icon. Switched from stream_compose_ic_new_chat to stream_compose_ic_add with explicit 24dp sizing. --- .../android/compose/ui/channels/header/ChannelListHeader.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 751a16cf121..f5f5d7b3868 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 @@ -238,9 +238,9 @@ internal fun DefaultChannelListHeaderTrailingContent( ) { Icon( modifier = Modifier - .padding(horizontal = 10.dp, vertical = 10.dp) + .size(24.dp) .testTag("Stream_CreateChannelIcon"), - painter = painterResource(id = R.drawable.stream_compose_ic_new_chat), + painter = painterResource(id = R.drawable.stream_compose_ic_add), contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), tint = Color.White, ) From 0fd252bc559f639dedb010d9a7f5265acd843509 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 14:36:35 +0100 Subject: [PATCH 07/57] refactor(compose): decouple SearchInput from InputField Move SearchInput to use BasicTextField directly, matching the pattern used by the new message composer. This removes the dependency on InputField and its composer-specific theme (inputBackground), making the search bar transparent so it inherits the parent background color. Works correctly in both light and dark mode. --- .../compose/ui/components/SearchInput.kt | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt index 196bd4b2b06..554754c6526 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt @@ -16,19 +16,24 @@ package io.getstream.chat.android.compose.ui.components +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,13 +41,17 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.ui.components.composer.InputField import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamTokens @@ -89,7 +98,21 @@ public fun SearchInput( null } - InputField( + var textState by remember { mutableStateOf(TextFieldValue(text = query)) } + if (textState.text != query) { + LaunchedEffect(query) { + if (textState.text != query) { + textState = textState.copy( + text = query, + selection = TextRange(query.length), + ) + } + } + } + + val shape = RoundedCornerShape(StreamTokens.radiusLg) + + BasicTextField( modifier = modifier .defaultMinSize(minHeight = 48.dp) .onFocusEvent { newState -> @@ -100,9 +123,32 @@ public fun SearchInput( } isFocused = newState.isFocused - }, - value = query, - onValueChange = onValueChange, + } + .border( + border = BorderStroke(1.dp, ChatTheme.colors.borderCoreDefault), + shape = shape, + ) + .clip(shape) + .padding( + start = StreamTokens.spacingXs, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingSm, + bottom = StreamTokens.spacingSm, + ), + value = textState, + onValueChange = { + textState = it + if (query != it.text) { + onValueChange(it.text) + } + }, + textStyle = ChatTheme.typography.bodyDefault.copy( + color = ChatTheme.colors.textPrimary, + ), + cursorBrush = SolidColor(ChatTheme.colors.accentPrimary), + singleLine = true, + maxLines = 1, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), decorationBox = { innerTextField -> Row( Modifier.fillMaxWidth(), @@ -121,13 +167,6 @@ public fun SearchInput( trailingContent?.invoke(this) } }, - maxLines = 1, - innerPadding = PaddingValues( - start = StreamTokens.spacingXs, // 8dp gap after icon - end = StreamTokens.spacingMd, // 16dp trailing padding - top = StreamTokens.spacingSm, // 12dp vertical - bottom = StreamTokens.spacingSm, // 12dp vertical - ), ) } From 74788eac9f602aee5bb1a70840d19c36f681c2cc Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 14:45:23 +0100 Subject: [PATCH 08/57] fix(compose): update SearchInput radius and fix icon clipping Use radius3xl (24dp) matching the composer's MessageInputShape instead of radiusLg (12dp). Move padding into decorationBox so the search icon isn't clipped by the border/clip modifiers. --- .../compose/ui/components/SearchInput.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt index 554754c6526..06241213b41 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt @@ -110,7 +110,7 @@ public fun SearchInput( } } - val shape = RoundedCornerShape(StreamTokens.radiusLg) + val shape = RoundedCornerShape(StreamTokens.radius3xl) BasicTextField( modifier = modifier @@ -128,13 +128,7 @@ public fun SearchInput( border = BorderStroke(1.dp, ChatTheme.colors.borderCoreDefault), shape = shape, ) - .clip(shape) - .padding( - start = StreamTokens.spacingXs, - end = StreamTokens.spacingMd, - top = StreamTokens.spacingSm, - bottom = StreamTokens.spacingSm, - ), + .clip(shape), value = textState, onValueChange = { textState = it @@ -151,7 +145,14 @@ public fun SearchInput( keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), decorationBox = { innerTextField -> Row( - Modifier.fillMaxWidth(), + Modifier + .fillMaxWidth() + .padding( + start = StreamTokens.spacingXs, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingSm, + bottom = StreamTokens.spacingSm, + ), verticalAlignment = Alignment.CenterVertically, ) { leadingIcon() From a171ec0ef4cc752f33933a8ead1c2d453b0cb493 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 14:47:44 +0100 Subject: [PATCH 09/57] fix(compose): reduce header plus icon from 24dp to 20dp Match the composer's icon size (icon/size/md = 20dp) for consistent icon sizing across the SDK. --- .../android/compose/ui/channels/header/ChannelListHeader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f5f5d7b3868..d674c5b3c5a 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 @@ -238,7 +238,7 @@ internal fun DefaultChannelListHeaderTrailingContent( ) { Icon( modifier = Modifier - .size(24.dp) + .size(20.dp) .testTag("Stream_CreateChannelIcon"), painter = painterResource(id = R.drawable.stream_compose_ic_add), contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), From 3cfd855c06bc4131a24a951c7dedab95b5eed1a0 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 14:55:19 +0100 Subject: [PATCH 10/57] fix: update search leading icon default paddings --- .../chat/android/compose/ui/components/SearchInput.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt index 06241213b41..3eaab18a232 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -54,6 +55,7 @@ import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R 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.util.padding /** * The search component that allows the user to fill in a search query and filter their items. @@ -178,7 +180,10 @@ public fun SearchInput( internal fun DefaultSearchLeadingIcon() { Icon( modifier = Modifier - .padding(start = StreamTokens.spacingMd) // 16dp leading padding, matches Figma input content px + .padding( + start = StreamTokens.spacingSm, + end = StreamTokens.spacingXs + ) .size(16.dp), painter = painterResource(id = R.drawable.stream_compose_ic_search), contentDescription = null, From ba9152fee9ef0a41aebce5fd32a0719f83751eb8 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 15:00:20 +0100 Subject: [PATCH 11/57] fix(compose): replace search icon with Figma stroked variant Replace the filled 24dp search icon with the stroked 20dp variant from Figma (IconMagnifyingGlassSearch), matching the new design system's stroked icon style. --- .../res/drawable/stream_compose_ic_search.xml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml index 95e6ea8098f..a265d01b997 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml @@ -15,14 +15,17 @@ limitations under the License. --> From a0a2f7dc6c3e607343ce29fad3830b65a14fb1d0 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 15:03:05 +0100 Subject: [PATCH 12/57] fix(compose): center plus icon in header button with 10dp padding Add Box with 10dp padding and center alignment inside the Surface, matching Figma button/padding-x/icon-only/md and button/padding-y/md tokens. The icon was not centered and appeared oversized. --- .../ui/channels/header/ChannelListHeader.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 d674c5b3c5a..8d5ca61da6c 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 @@ -236,14 +236,19 @@ internal fun DefaultChannelListHeaderTrailingContent( shape = CircleShape, shadowElevation = 0.dp, ) { - Icon( - modifier = Modifier - .size(20.dp) - .testTag("Stream_CreateChannelIcon"), - painter = painterResource(id = R.drawable.stream_compose_ic_add), - contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), - tint = Color.White, - ) + Box( + modifier = Modifier.padding(10.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .size(20.dp) + .testTag("Stream_CreateChannelIcon"), + painter = painterResource(id = R.drawable.stream_compose_ic_add), + contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), + tint = Color.White, + ) + } } } } From 68275bd290cfca14611b860eb4f76d41861e76aa Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Feb 2026 11:42:34 +0100 Subject: [PATCH 13/57] feat(compose): add attachment type icons and message states to channel list preview Add inline preview icons for photo, video, file, link, and location attachment types in the channel list message preview. Handle deleted messages, error state indicator, and empty chat placeholder per Figma spec. Voice messages and location now include sender name prefix for group chats. --- .../compose/ui/channels/list/ChannelItem.kt | 28 ++- .../channels/MessageReadStatusIcon.kt | 12 +- .../ui/util/MessagePreviewFormatter.kt | 167 +++++++++++++----- .../ui/util/MessagePreviewIconFactory.kt | 64 +++++-- .../src/main/res/values/strings.xml | 22 ++- 5 files changed, 222 insertions(+), 71 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 16e3e5eca6a..a16c44569eb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -276,21 +276,19 @@ internal fun RowScope.DefaultChannelItemCenterContent( ?: lastMessage?.let { ChatTheme.messagePreviewFormatter.formatMessagePreview(it, currentUser) } - ?: AnnotatedString("") - - if (lastMessageText.isNotEmpty()) { - Text( - modifier = Modifier - .testTag("Stream_MessagePreview") - .weight(1f), - text = lastMessageText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.captionDefault, - color = ChatTheme.colors.textSecondary, // was textLowEmphasis - inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), - ) - } + + Text( + modifier = Modifier + .testTag("Stream_MessagePreview") + .weight(1f), + text = lastMessageText + ?: AnnotatedString(stringResource(R.string.stream_compose_no_messages_yet)), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ChatTheme.typography.captionDefault, + color = ChatTheme.colors.textSecondary, + inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), + ) } } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt index 8c82f66b5c3..2c39b072a1b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt @@ -105,7 +105,7 @@ public fun MessageReadStatusIcon( else -> isSentIcon() } - SyncStatus.FAILED_PERMANENTLY -> Unit + SyncStatus.FAILED_PERMANENTLY -> IsErrorIcon(modifier = modifier) } } @@ -164,6 +164,16 @@ private fun IsSentIcon(modifier: Modifier) { ) } +@Composable +private fun IsErrorIcon(modifier: Modifier) { + Icon( + modifier = modifier.testTag("Stream_MessageReadStatus_isError"), + painter = painterResource(id = R.drawable.stream_compose_ic_error), + contentDescription = null, + tint = ChatTheme.colors.accentError, + ) +} + @Composable private fun IsDeliveredIcon(modifier: Modifier) { Icon( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt index c20e019c6e0..7d279b3e275 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import io.getstream.chat.android.client.utils.message.hasAudioRecording import io.getstream.chat.android.client.utils.message.hasSharedLocation +import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isPoll import io.getstream.chat.android.client.utils.message.isPollClosed import io.getstream.chat.android.client.utils.message.isSystem @@ -34,6 +35,7 @@ import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.theme.StreamColors import io.getstream.chat.android.compose.ui.theme.StreamTypography import io.getstream.chat.android.models.Attachment +import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User @@ -157,18 +159,23 @@ private class DefaultMessagePreviewFormatter( currentUser: User?, ): AnnotatedString { return buildAnnotatedString { - message.let { message -> - val displayedText = when (autoTranslationEnabled) { - true -> currentUser?.language?.let { userLanguage -> - message.getTranslation(userLanguage).ifEmpty { message.text } - } ?: message.text + val displayedText = when (autoTranslationEnabled) { + true -> currentUser?.language?.let { userLanguage -> + message.getTranslation(userLanguage).ifEmpty { message.text } + } ?: message.text - else -> message.text - }.trim() + else -> message.text + }.trim() - if (message.isSystem()) { - append(displayedText) - } else if (message.isPoll()) { + when { + message.isSystem() -> append(displayedText) + + message.isDeleted() -> { + appendSenderName(message, currentUser, senderNameTextStyle) + append(context.getString(R.string.stream_compose_message_deleted_preview)) + } + + message.isPoll() -> { if (message.isPollClosed()) { append( context.getString( @@ -184,37 +191,121 @@ private class DefaultMessagePreviewFormatter( ), ) } - } else if (message.hasAudioRecording()) { + } + + message.hasAudioRecording() -> { + appendSenderName(message, currentUser, senderNameTextStyle) appendInlineContent(DefaultMessagePreviewIconFactory.VOICE_MESSAGE) append(SPACE) append(context.getString(R.string.stream_compose_audio_recording_preview)) - } else { - appendSenderName( - message = message, - currentUser = currentUser, - senderNameTextStyle = senderNameTextStyle, - ) - if (message.hasSharedLocation()) { - appendSharedLocationMessageText( - message = message, - textStyle = messageTextStyle, - ) - } else { - appendMessageText( - messageText = displayedText, - messageTextStyle = messageTextStyle, - ) + } + + message.hasSharedLocation() -> { + appendSenderName(message, currentUser, senderNameTextStyle) + appendInlineContent(DefaultMessagePreviewIconFactory.LOCATION) + append(SPACE) + message.sharedLocation?.let { location -> + append(context.getString(location.getMessageTextResId())) } - appendAttachmentText( - attachments = message.attachments, - attachmentFactories = attachmentFactories, - attachmentTextStyle = attachmentTextFontStyle, - ) } + + else -> { + appendSenderName(message, currentUser, senderNameTextStyle) + appendTypedAttachmentPreview(message.attachments, displayedText) + } + } + } + } + + /** + * Appends a typed attachment preview (icon + label/caption) if the message contains + * a recognizable attachment type. Falls back to the default text + attachment formatting. + */ + private fun AnnotatedString.Builder.appendTypedAttachmentPreview( + attachments: List, + displayedText: String, + ) { + // Classify attachments — links first (images with ogUrl are links, not photos) + val links = attachments.filter { it.titleLink != null || it.ogUrl != null } + val images = attachments.filter { + it.type == AttachmentType.IMAGE && it.ogUrl == null && it.titleLink == null + } + val videos = attachments.filter { it.type == AttachmentType.VIDEO } + val files = attachments.filter { it.type == AttachmentType.FILE } + + when { + images.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.PHOTO) + append(SPACE) + appendAttachmentLabel( + caption = displayedText, + count = images.size, + singleLabelResId = R.string.stream_compose_photo_preview, + pluralLabelResId = R.plurals.stream_compose_photos_preview, + ) + } + + videos.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.VIDEO) + append(SPACE) + appendAttachmentLabel( + caption = displayedText, + count = videos.size, + singleLabelResId = R.string.stream_compose_video_preview, + pluralLabelResId = R.plurals.stream_compose_videos_preview, + ) + } + + files.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.FILE) + append(SPACE) + appendAttachmentLabel( + caption = displayedText, + count = files.size, + singleLabelResId = R.string.stream_compose_file_preview, + pluralLabelResId = R.plurals.stream_compose_files_preview, + ) + } + + links.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.LINK) + append(SPACE) + if (displayedText.isNotEmpty()) { + append(displayedText) + } else { + append(context.getString(R.string.stream_compose_link_preview)) + } + } + + else -> { + // No recognizable typed attachment — use default text + attachment formatting + appendMessageText(displayedText, messageTextStyle) + appendAttachmentText(attachments, attachmentFactories, attachmentTextFontStyle) } } } + /** + * Appends the appropriate label for an attachment preview: + * - If caption (message text) exists: show the caption + * - If no caption and single: show type label ("Photo") + * - If no caption and multiple: show count + plural ("2 Photos") + */ + private fun AnnotatedString.Builder.appendAttachmentLabel( + caption: String, + count: Int, + singleLabelResId: Int, + pluralLabelResId: Int, + ) { + when { + caption.isNotEmpty() -> append(caption) + count > 1 -> append( + context.resources.getQuantityString(pluralLabelResId, count, count), + ) + else -> append(context.getString(singleLabelResId)) + } + } + /** * Generates a preview text for the given draft message. * This is used to show a preview of the draft message in the the channel list. @@ -326,16 +417,4 @@ private class DefaultMessagePreviewFormatter( } } - private fun AnnotatedString.Builder.appendSharedLocationMessageText( - message: Message, - textStyle: TextStyle, - ) { - message.sharedLocation?.let { sharedLocation -> - val text = context.getString(sharedLocation.getMessageTextResId()) - appendMessageText( - messageText = text, - messageTextStyle = textStyle, - ) - } - } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt index 196e672b4b5..60d4a90d1df 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt @@ -52,27 +52,71 @@ public interface MessagePreviewIconFactory { internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { companion object { - /** - * The key for the voice message preview icon. - */ internal const val VOICE_MESSAGE = "voice_message" + internal const val PHOTO = "photo" + internal const val VIDEO = "video" + internal const val FILE = "file" + internal const val LINK = "link" + internal const val LOCATION = "location" + internal const val POLL = "poll" } override fun createPreviewIcons(): Map { + val placeholder = Placeholder( + width = 16.sp, + height = 16.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ) return mapOf( - VOICE_MESSAGE to InlineTextContent( - placeholder = Placeholder( - width = 16.sp, - height = 16.sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { + VOICE_MESSAGE to InlineTextContent(placeholder) { Icon( painter = painterResource(id = R.drawable.stream_compose_ic_mic), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, ) }, + PHOTO to InlineTextContent(placeholder) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_camera), + contentDescription = null, + tint = ChatTheme.colors.textLowEmphasis, + ) + }, + VIDEO to InlineTextContent(placeholder) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_video), + contentDescription = null, + tint = ChatTheme.colors.textLowEmphasis, + ) + }, + FILE to InlineTextContent(placeholder) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_file), + contentDescription = null, + tint = ChatTheme.colors.textLowEmphasis, + ) + }, + LINK to InlineTextContent(placeholder) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_link), + contentDescription = null, + tint = ChatTheme.colors.textLowEmphasis, + ) + }, + LOCATION to InlineTextContent(placeholder) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_map_pin), + contentDescription = null, + tint = ChatTheme.colors.textLowEmphasis, + ) + }, + POLL to InlineTextContent(placeholder) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_poll), + contentDescription = null, + tint = ChatTheme.colors.textLowEmphasis, + ) + }, ) } } 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 5e5d9c1a220..ce8f6fd6487 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -153,8 +153,28 @@ Also send as direct message File type is not supported - + Voice message + Photo + Video + File + Link + Message deleted + No messages yet + + %d Photo + %d Photos + + + %d Video + %d Videos + + + %d File + %d Files + + + Record audio message Hold to record. Release to send Slide to cancel From 1fc8abbfce8949f7dfbde2de7324fe97184845b2 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Feb 2026 11:55:47 +0100 Subject: [PATCH 14/57] fix(compose): show deleted message preview in channel list The getPreviewMessage function filters out deleted messages, so the formatter's isDeleted check was never reached. Add getLastMessageIncludingDeleted to pass deleted messages to the formatter. Suppress delivery status icon for deleted messages. --- .../compose/ui/channels/list/ChannelItem.kt | 11 ++++++++--- .../chat/android/compose/ui/util/ChannelUtils.kt | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index a16c44569eb..56ad1b9720a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -57,7 +57,9 @@ import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.TypingIndicator import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamTokens +import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.compose.ui.util.getLastMessage +import io.getstream.chat.android.compose.ui.util.getLastMessageIncludingDeleted import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.SyncStatus @@ -184,7 +186,10 @@ internal fun RowScope.DefaultChannelItemCenterContent( currentUser: User?, ) { val channel = channelItemState.channel - val lastMessage = channel.getLastMessage(currentUser) + // Use raw last message (including deleted) for preview; fall back to filtered for non-deleted + val rawLastMessage = channel.getLastMessageIncludingDeleted(currentUser) + val isLastMessageDeleted = rawLastMessage?.isDeleted() == true + val lastMessage = if (isLastMessageDeleted) rawLastMessage else channel.getLastMessage(currentUser) val unreadCount = channel.currentUserUnreadCount(currentUserId = currentUser?.id) val isLastMessageFromCurrentUser = lastMessage?.user?.id == currentUser?.id @@ -259,8 +264,8 @@ internal fun RowScope.DefaultChannelItemCenterContent( // Typing indicator replaces message preview when active UserTypingIndicator(channelItemState.typingUsers) } else { - // Delivery status prefix: checkmark icon (only for own messages) - if (isLastMessageFromCurrentUser && lastMessage != null) { + // Delivery status prefix: checkmark icon (only for own non-deleted messages) + if (isLastMessageFromCurrentUser && lastMessage != null && !isLastMessageDeleted) { ChatTheme.componentFactory.ChannelItemReadStatusIndicator( channel = channel, message = lastMessage, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt index ebc8b909ac3..bf261a360ae 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt @@ -17,6 +17,10 @@ package io.getstream.chat.android.compose.ui.util import android.content.Context +import io.getstream.chat.android.client.extensions.getCreatedAtOrDefault +import io.getstream.chat.android.client.extensions.internal.NEVER +import io.getstream.chat.android.client.utils.message.isRegular +import io.getstream.chat.android.client.utils.message.isSystem import io.getstream.chat.android.compose.R import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.Message @@ -34,6 +38,17 @@ import java.util.Date */ public fun Channel.getLastMessage(currentUser: User?): Message? = getPreviewMessage(currentUser) +/** + * Returns channel's last regular or system message, **including deleted messages**. + * Used by the channel list to show "Message deleted" when the last message was deleted. + */ +internal fun Channel.getLastMessageIncludingDeleted(currentUser: User?): Message? = + messages.asSequence() + .filter { it.createdAt != null || it.createdLocallyAt != null } + .filter { it.user.id == currentUser?.id || !it.shadowed } + .filter { it.isRegular() || it.isSystem() } + .maxByOrNull { it.getCreatedAtOrDefault(NEVER) } + /** * Filters the read status of each person other than the target user. * From 26dc02484adb56832ca59ff1aa83eee4e7cd82ba Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Feb 2026 12:00:24 +0100 Subject: [PATCH 15/57] fix(compose): ensure preview icons fill placeholder bounds Add Modifier.fillMaxSize() to all inline preview icons so they properly scale to the 16sp placeholder regardless of their intrinsic drawable size (some are 12dp, mic is 20dp, poll is 24dp). --- .../compose/ui/util/MessagePreviewIconFactory.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt index 60d4a90d1df..cd4d1febe30 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt @@ -16,8 +16,10 @@ package io.getstream.chat.android.compose.ui.util +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.Icon +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -67,9 +69,11 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { height = 16.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.Center, ) + val iconModifier = Modifier.fillMaxSize() return mapOf( VOICE_MESSAGE to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_mic), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -77,6 +81,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, PHOTO to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_camera), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -84,6 +89,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, VIDEO to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_video), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -91,6 +97,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, FILE to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_file), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -98,6 +105,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, LINK to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_link), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -105,6 +113,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, LOCATION to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_map_pin), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -112,6 +121,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, POLL to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_poll), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, From 8e643703958180d1bdb11706e2697153fd371061 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Feb 2026 12:42:08 +0100 Subject: [PATCH 16/57] fix(compose): use TextCenter alignment for preview icons instead of fillMaxSize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert fillMaxSize() on icons — the real issue is PlaceholderVerticalAlign.Center centers in the line box (16sp) rather than the text glyphs (14sp). TextCenter fixes the vertical alignment against the caption text. --- .../compose/ui/util/MessagePreviewIconFactory.kt | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt index cd4d1febe30..133de5c17d4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt @@ -16,10 +16,8 @@ package io.getstream.chat.android.compose.ui.util -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.Icon -import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -67,13 +65,11 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { val placeholder = Placeholder( width = 16.sp, height = 16.sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, ) - val iconModifier = Modifier.fillMaxSize() return mapOf( VOICE_MESSAGE to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_mic), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -81,7 +77,6 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, PHOTO to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_camera), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -89,7 +84,6 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, VIDEO to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_video), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -97,7 +91,6 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, FILE to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_file), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -105,7 +98,6 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, LINK to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_link), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -113,7 +105,6 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, LOCATION to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_map_pin), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -121,7 +112,6 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, POLL to InlineTextContent(placeholder) { Icon( - modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_poll), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, From 77da7103d5400a5545c4f72fbce4aa445e5607c7 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Feb 2026 12:47:12 +0100 Subject: [PATCH 17/57] feat(compose): show sender name in group chat previews and fix icon alignment - getSenderDisplayName now returns the sender's name for group chats (was only returning "You" for current user, null for everyone else) - Add isDirectMessaging parameter to formatMessagePreview so the formatter knows when to suppress the sender name (DMs only) - ChannelItem passes isOneToOne to the formatter - Reduce icon placeholder from 16sp to 14sp (matching font size) with Modifier.size(14.dp) to prevent text baseline displacement --- .../compose/ui/channels/list/ChannelItem.kt | 6 +++++- .../compose/ui/util/MessagePreviewFormatter.kt | 18 ++++++++++++------ .../ui/util/MessagePreviewIconFactory.kt | 17 ++++++++++++++--- .../android/compose/ui/util/MessageUtils.kt | 8 +++++--- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 56ad1b9720a..6e7451ee354 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -60,6 +60,7 @@ import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.compose.ui.util.getLastMessage import io.getstream.chat.android.compose.ui.util.getLastMessageIncludingDeleted +import io.getstream.chat.android.compose.ui.util.isOneToOne import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.SyncStatus @@ -186,6 +187,7 @@ internal fun RowScope.DefaultChannelItemCenterContent( currentUser: User?, ) { val channel = channelItemState.channel + val isDirectMessaging = channel.isOneToOne(currentUser) // Use raw last message (including deleted) for preview; fall back to filtered for non-deleted val rawLastMessage = channel.getLastMessageIncludingDeleted(currentUser) val isLastMessageDeleted = rawLastMessage?.isDeleted() == true @@ -279,7 +281,9 @@ internal fun RowScope.DefaultChannelItemCenterContent( channelItemState.draftMessage ?.let { ChatTheme.messagePreviewFormatter.formatDraftMessagePreview(it) } ?: lastMessage?.let { - ChatTheme.messagePreviewFormatter.formatMessagePreview(it, currentUser) + ChatTheme.messagePreviewFormatter.formatMessagePreview( + it, currentUser, isDirectMessaging, + ) } Text( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt index 7d279b3e275..2db524b8088 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt @@ -60,7 +60,11 @@ public interface MessagePreviewFormatter { * @param currentUser The currently logged in user. * @return The formatted text representation for the given message. */ - public fun formatMessagePreview(message: Message, currentUser: User?): AnnotatedString + public fun formatMessagePreview( + message: Message, + currentUser: User?, + isDirectMessaging: Boolean = false, + ): AnnotatedString /** * Generates a preview text for the given draft message. @@ -157,6 +161,7 @@ private class DefaultMessagePreviewFormatter( override fun formatMessagePreview( message: Message, currentUser: User?, + isDirectMessaging: Boolean, ): AnnotatedString { return buildAnnotatedString { val displayedText = when (autoTranslationEnabled) { @@ -171,7 +176,7 @@ private class DefaultMessagePreviewFormatter( message.isSystem() -> append(displayedText) message.isDeleted() -> { - appendSenderName(message, currentUser, senderNameTextStyle) + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) append(context.getString(R.string.stream_compose_message_deleted_preview)) } @@ -194,14 +199,14 @@ private class DefaultMessagePreviewFormatter( } message.hasAudioRecording() -> { - appendSenderName(message, currentUser, senderNameTextStyle) + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) appendInlineContent(DefaultMessagePreviewIconFactory.VOICE_MESSAGE) append(SPACE) append(context.getString(R.string.stream_compose_audio_recording_preview)) } message.hasSharedLocation() -> { - appendSenderName(message, currentUser, senderNameTextStyle) + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) appendInlineContent(DefaultMessagePreviewIconFactory.LOCATION) append(SPACE) message.sharedLocation?.let { location -> @@ -210,7 +215,7 @@ private class DefaultMessagePreviewFormatter( } else -> { - appendSenderName(message, currentUser, senderNameTextStyle) + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) appendTypedAttachmentPreview(message.attachments, displayedText) } } @@ -342,8 +347,9 @@ private class DefaultMessagePreviewFormatter( message: Message, currentUser: User?, senderNameTextStyle: TextStyle, + isDirectMessaging: Boolean = false, ) { - val sender = message.getSenderDisplayName(context, currentUser) + val sender = message.getSenderDisplayName(context, currentUser, isDirectMessaging) if (sender != null) { append("$sender: ") diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt index 133de5c17d4..f26b6987a99 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt @@ -16,11 +16,14 @@ package io.getstream.chat.android.compose.ui.util +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.Icon +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -63,13 +66,15 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { override fun createPreviewIcons(): Map { val placeholder = Placeholder( - width = 16.sp, - height = 16.sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + width = 14.sp, + height = 14.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, ) + val iconModifier = Modifier.size(14.dp) return mapOf( VOICE_MESSAGE to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_mic), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -77,6 +82,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, PHOTO to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_camera), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -84,6 +90,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, VIDEO to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_video), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -91,6 +98,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, FILE to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_file), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -98,6 +106,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, LINK to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_link), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -105,6 +114,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, LOCATION to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_map_pin), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, @@ -112,6 +122,7 @@ internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { }, POLL to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_poll), contentDescription = null, tint = ChatTheme.colors.textLowEmphasis, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt index bd4ab0c8557..50eb4cc571a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt @@ -54,10 +54,12 @@ public fun showOriginalTextAsState(cid: String, messageId: String): State context.getString(R.string.stream_compose_channel_list_you) - else -> null + when { + user.id == currentUser?.id -> context.getString(R.string.stream_compose_channel_list_you) + isDirectMessaging -> null + else -> user.name } private val fullSizeAttachmentTypes = setOf( From a20a5e0e08ee00cab652e1900d479e38005f7b96 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Feb 2026 16:39:01 +0100 Subject: [PATCH 18/57] fix(compose): align channel list item with Figma spec - Configurable mute icon position (inline title / trailing bottom) - DM typing shows "Typing" without name; group shows proper name formats - Remove "You:" prefix from DM outgoing non-deleted messages - Replace hardcoded mute icon size with design token --- .../api/stream-chat-android-compose.api | 57 ++++++--- .../compose/ui/channels/list/ChannelItem.kt | 115 +++++++++++++----- .../android/compose/ui/theme/ChatConfig.kt | 21 ++++ .../android/compose/ui/theme/StreamTokens.kt | 1 + .../ui/util/MessagePreviewFormatter.kt | 17 ++- .../android/compose/ui/util/MessageUtils.kt | 1 + .../src/main/res/values/strings.xml | 7 ++ .../compose/ui/channels/ChannelItemTest.kt | 20 +++ ...ia_gallery_options_menu_for_other_user.png | Bin 16995 -> 16996 bytes ...edia_gallery_options_menu_for_own_user.png | Bin 19536 -> 19536 bytes ...enTest_media_gallery_header_connecting.png | Bin 14795 -> 14909 bytes ...edia_gallery_header_message_without_id.png | Bin 10852 -> 11121 bytes ...creenTest_media_gallery_header_offline.png | Bin 12758 -> 13004 bytes ...ScreenTest_media_gallery_header_online.png | Bin 10871 -> 11148 bytes ...eenTest_media_gallery_screen_connected.png | Bin 30691 -> 30870 bytes ...creenTest_media_gallery_screen_offline.png | Bin 31539 -> 31701 bytes ...llery_screen_with_gallery_bottom_sheet.png | Bin 29606 -> 29757 bytes ...media_gallery_screen_with_options_menu.png | Bin 37127 -> 37254 bytes ...ry_screen_with_share_large_file_prompt.png | Bin 38076 -> 38155 bytes ...nelFilesAttachmentsContentTest_content.png | Bin 13385 -> 13388 bytes ...hmentsContentTest_content_in_dark_mode.png | Bin 13103 -> 13103 bytes ...lesAttachmentsContentTest_loading_more.png | Bin 13419 -> 13423 bytes ...sContentTest_loading_more_in_dark_mode.png | Bin 13143 -> 13142 bytes ...o_DirectChannelInfoContentTest_content.png | Bin 29852 -> 29851 bytes ...elInfoContentTest_content_in_dark_mode.png | Bin 31546 -> 31545 bytes ...annelInfoContentTest_collapsed_members.png | Bin 36465 -> 36465 bytes ...entTest_collapsed_members_in_dark_mode.png | Bin 37208 -> 37209 bytes ...hannelInfoContentTest_expanded_members.png | Bin 42901 -> 42900 bytes ...tentTest_expanded_members_in_dark_mode.png | Bin 45209 -> 45212 bytes ...channels_ChannelItemTest_draft_message.png | Bin 18123 -> 19080 bytes ...ItemTest_last_message_delivered_status.png | Bin 23664 -> 23996 bytes ...elItemTest_last_message_pending_status.png | Bin 24125 -> 24371 bytes ...annelItemTest_last_message_seen_status.png | Bin 23713 -> 24082 bytes ...annelItemTest_last_message_sent_status.png | Bin 23451 -> 23800 bytes ...channels_ChannelItemTest_muted_channel.png | Bin 24576 -> 24653 bytes ...ItemTest_muted_channel_trailing_bottom.png | Bin 0 -> 13496 bytes ...i.channels_ChannelItemTest_no_messages.png | Bin 12382 -> 18388 bytes ...annels_ChannelItemTest_unread_messages.png | Bin 24629 -> 25318 bytes ...annelListHeaderTest_connected,_no_user.png | Bin 14096 -> 11218 bytes ...nelListHeaderTest_connected,_with_user.png | Bin 17097 -> 14384 bytes ...nnelListHeaderTest_connecting,_no_user.png | Bin 16662 -> 13822 bytes ...elListHeaderTest_connecting,_with_user.png | Bin 19586 -> 17008 bytes ...ChannelListHeaderTest_offline,_no_user.png | Bin 14771 -> 11793 bytes ...annelListHeaderTest_offline,_with_user.png | Bin 17779 -> 14962 bytes ...annels_ChannelListTest_loaded_channels.png | Bin 74365 -> 71373 bytes ..._ChannelListTest_loading_more_channels.png | Bin 62398 -> 69739 bytes ...hannels_ChannelListTest_search_results.png | Bin 35068 -> 34217 bytes ....channels_ChannelsTest_loaded_channels.png | Bin 13677 -> 22313 bytes ...lectedChannelMenuTest_selected_channel.png | Bin 37181 -> 38798 bytes ...MenuTest_selected_channel_in_dark_mode.png | Bin 40031 -> 41117 bytes ...s.poll_PollOptionInputTest_empty_input.png | Bin 9242 -> 9241 bytes ...ts.poll_PollOptionInputTest_with_input.png | Bin 9613 -> 9605 bytes ...ts_SearchInputTest_focused_empty_query.png | Bin 10121 -> 10906 bytes ...s_SearchInputTest_focused_filled_query.png | Bin 10796 -> 11636 bytes ..._SearchInputTest_unfocused_empty_query.png | Bin 10113 -> 10880 bytes ...ns_MentionListTest_loaded_mention_list.png | Bin 61767 -> 59426 bytes ...tionListTest_loading_more_mention_list.png | Bin 61876 -> 59539 bytes ...ts.poll_CreatePollScreenTest_dark_mode.png | Bin 34983 -> 34980 bytes ...s.poll_CreatePollScreenTest_light_mode.png | Bin 35792 -> 35788 bytes ...l_AudioRecordingButtonTest_button_hold.png | Bin 18099 -> 14855 bytes ...AudioRecordingButtonTest_button_locked.png | Bin 28403 -> 25711 bytes ...essageListTest_scroll_to_bottom_button.png | Bin 93702 -> 93555 bytes ...t_scroll_to_bottom_button_in_dark_mode.png | Bin 86586 -> 86397 bytes ...innedMessageItemTest_pinnedMessageItem.png | Bin 23079 -> 23685 bytes ...MessageListTest_loaded_pinned_messages.png | Bin 39881 -> 42910 bytes ...eListTest_loading_more_pinned_messages.png | Bin 39986 -> 43014 bytes ...e.ui.threads_ThreadItemTest_threadItem.png | Bin 42157 -> 45003 bytes ....threads_ThreadListTest_loaded_threads.png | Bin 66425 -> 73367 bytes ...Test_loaded_threads_with_unread_banner.png | Bin 73433 -> 79886 bytes ...ds_ThreadListTest_loading_more_threads.png | Bin 66511 -> 73453 bytes 70 files changed, 190 insertions(+), 49 deletions(-) create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel_trailing_bottom.png 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 88349d60d38..8e68ca5e670 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1282,6 +1282,7 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public static field lambda-6 Lkotlin/jvm/functions/Function2; public static field lambda-7 Lkotlin/jvm/functions/Function2; public static field lambda-8 Lkotlin/jvm/functions/Function2; + public static field lambda-9 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; @@ -1291,6 +1292,7 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public final fun getLambda-6$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-7$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-8$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-9$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/channels/list/ComposableSingletons$ChannelListKt { @@ -2813,6 +2815,20 @@ public final class io/getstream/chat/android/compose/ui/theme/AttachmentPickerTh public final fun defaultTheme (Lio/getstream/chat/android/compose/ui/theme/StreamColors;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerTheme; } +public final class io/getstream/chat/android/compose/ui/theme/ChannelListConfig { + public static final field $stable I + public fun ()V + public fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;)V + public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public final fun copy (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getMuteIndicatorPosition ()Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/compose/ui/theme/ChannelMediaAttachmentsPreviewBottomBarParams { public static final field $stable I public fun (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V @@ -3247,12 +3263,14 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatComponentFacto public final class io/getstream/chat/android/compose/ui/theme/ChatConfig { public static final field $stable I public fun ()V - public fun (Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;)V - public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;)V + public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/compose/ui/theme/ComposerConfig; - public final fun copy (Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChatConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; + public final fun component2 ()Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public final fun copy (Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChatConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; public fun equals (Ljava/lang/Object;)Z + public final fun getChannelList ()Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; public final fun getComposer ()Lio/getstream/chat/android/compose/ui/theme/ComposerConfig; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -3624,6 +3642,14 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageUnreadSepar public final fun defaultTheme (Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Landroidx/compose/runtime/Composer;II)Lio/getstream/chat/android/compose/ui/theme/MessageUnreadSeparatorTheme; } +public final class io/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition : java/lang/Enum { + public static final field INLINE_TITLE Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public static final field TRAILING_BOTTOM Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public static fun values ()[Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; +} + public final class io/getstream/chat/android/compose/ui/theme/ReactionOptionsTheme { public static final field $stable I public static final field Companion Lio/getstream/chat/android/compose/ui/theme/ReactionOptionsTheme$Companion; @@ -3891,8 +3917,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors$Compa public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public static final field $stable I public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamDimens$Companion; - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F public final fun component10-D9Ej5fM ()F public final fun component11-D9Ej5fM ()F @@ -3944,12 +3970,13 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun component53-D9Ej5fM ()F public final fun component54-D9Ej5fM ()F public final fun component55-D9Ej5fM ()F + public final fun component56-D9Ej5fM ()F public final fun component6-D9Ej5fM ()F public final fun component7-D9Ej5fM ()F public final fun component8-D9Ej5fM ()F public final fun component9-D9Ej5fM ()F - public final fun copy-B0eyH1I (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; - public static synthetic fun copy-B0eyH1I$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public final fun copy-hAWACDU (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public static synthetic fun copy-hAWACDU$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; public fun equals (Ljava/lang/Object;)Z public final fun getAttachmentsContentFileUploadWidth-D9Ej5fM ()F public final fun getAttachmentsContentFileWidth-D9Ej5fM ()F @@ -3971,6 +3998,7 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun getChannelAvatarSize-D9Ej5fM ()F public final fun getChannelItemHorizontalPadding-D9Ej5fM ()F public final fun getChannelItemVerticalPadding-D9Ej5fM ()F + public final fun getChannelListItemAvatarSize-D9Ej5fM ()F public final fun getCommandSuggestionItemHorizontalPadding-D9Ej5fM ()F public final fun getCommandSuggestionItemIconSize-D9Ej5fM ()F public final fun getCommandSuggestionItemVerticalPadding-D9Ej5fM ()F @@ -4334,13 +4362,6 @@ public final class io/getstream/chat/android/compose/ui/util/ChannelUtilsKt { public static final fun isOneToOne (Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;)Z } -public final class io/getstream/chat/android/compose/ui/util/ComposableSingletons$MessagePreviewIconFactoryKt { - public static final field INSTANCE Lio/getstream/chat/android/compose/ui/util/ComposableSingletons$MessagePreviewIconFactoryKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; -} - public final class io/getstream/chat/android/compose/ui/util/ComposableSingletons$SnackbarPopupKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/util/ComposableSingletons$SnackbarPopupKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; @@ -4395,7 +4416,7 @@ public final class io/getstream/chat/android/compose/ui/util/MessageListUtilsKt public abstract interface class io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter { public static final field Companion Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter$Companion; public abstract fun formatDraftMessagePreview (Lio/getstream/chat/android/models/DraftMessage;)Landroidx/compose/ui/text/AnnotatedString; - public abstract fun formatMessagePreview (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;)Landroidx/compose/ui/text/AnnotatedString; + public abstract fun formatMessagePreview (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Z)Landroidx/compose/ui/text/AnnotatedString; public abstract fun formatMessageTitle (Lio/getstream/chat/android/models/Message;)Landroidx/compose/ui/text/AnnotatedString; } @@ -4403,6 +4424,10 @@ public final class io/getstream/chat/android/compose/ui/util/MessagePreviewForma public final fun defaultFormatter (Landroid/content/Context;ZLio/getstream/chat/android/compose/ui/theme/StreamTypography;Ljava/util/List;Lio/getstream/chat/android/compose/ui/theme/StreamColors;)Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter; } +public final class io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter$DefaultImpls { + public static synthetic fun formatMessagePreview$default (Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;ZILjava/lang/Object;)Landroidx/compose/ui/text/AnnotatedString; +} + public abstract interface class io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory { public static final field Companion Lio/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory$Companion; public abstract fun createPreviewIcons ()Ljava/util/Map; diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 6e7451ee354..41a2f3243bb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.extensions.currentUserUnreadCount import io.getstream.chat.android.client.extensions.getCreatedAtOrNull import io.getstream.chat.android.client.extensions.internal.NEVER @@ -55,7 +54,10 @@ import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.TypingIndicator +import io.getstream.chat.android.compose.ui.theme.ChatConfig import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.ChannelListConfig +import io.getstream.chat.android.compose.ui.theme.MuteIndicatorPosition import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.compose.ui.util.getLastMessage @@ -195,6 +197,8 @@ internal fun RowScope.DefaultChannelItemCenterContent( val unreadCount = channel.currentUserUnreadCount(currentUserId = currentUser?.id) val isLastMessageFromCurrentUser = lastMessage?.user?.id == currentUser?.id + val mutePosition = ChatTheme.config.channelList.muteIndicatorPosition + Column( modifier = Modifier .weight(1f) @@ -207,28 +211,34 @@ internal fun RowScope.DefaultChannelItemCenterContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp default gap ) { - // Channel name (flex-1) - Text( - modifier = Modifier - .testTag("Stream_ChannelName") - .weight(1f), - text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser), - style = ChatTheme.typography.bodyDefault, // 16sp Regular (was bodyBold + fontSize=16.sp override) - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = ChatTheme.colors.textPrimary, // was textHighEmphasis - ) - - // Mute icon (optional, inline in title row after name) - if (channelItemState.isMuted) { - Icon( + // Channel name + optional inline mute icon (flex-1) + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), + ) { + Text( modifier = Modifier - .testTag("Stream_ChannelMutedIcon") - .size(16.dp), - painter = painterResource(id = R.drawable.stream_compose_ic_muted), - contentDescription = null, - tint = ChatTheme.colors.textTertiary, // was textLowEmphasis + .testTag("Stream_ChannelName") + .weight(1f, fill = false), + text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser), + style = ChatTheme.typography.bodyDefault, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ChatTheme.colors.textPrimary, ) + + // Mute icon — inline in title row (INLINE_TITLE variant) + if (channelItemState.isMuted && mutePosition == MuteIndicatorPosition.INLINE_TITLE) { + Icon( + modifier = Modifier + .testTag("Stream_ChannelMutedIcon") + .size(StreamTokens.size16), + painter = painterResource(id = R.drawable.stream_compose_ic_muted), + contentDescription = null, + tint = ChatTheme.colors.textTertiary, + ) + } } // Trailing: Timestamp + Badge (gap = spacing/xs = 8dp between them) @@ -256,7 +266,7 @@ internal fun RowScope.DefaultChannelItemCenterContent( } } - // ── Message Row: [DeliveryStatus?] [MessagePreview or TypingIndicator] ── + // ── Message Row: [DeliveryStatus?] [MessagePreview or TypingIndicator] [MuteIcon?] ── Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -264,7 +274,7 @@ internal fun RowScope.DefaultChannelItemCenterContent( ) { if (channelItemState.typingUsers.isNotEmpty()) { // Typing indicator replaces message preview when active - UserTypingIndicator(channelItemState.typingUsers) + UserTypingIndicator(channelItemState.typingUsers, isDirectMessaging) } else { // Delivery status prefix: checkmark icon (only for own non-deleted messages) if (isLastMessageFromCurrentUser && lastMessage != null && !isLastMessageDeleted) { @@ -299,6 +309,18 @@ internal fun RowScope.DefaultChannelItemCenterContent( inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), ) } + + // Mute icon — at end of message row (TRAILING_BOTTOM variant) + if (channelItemState.isMuted && mutePosition == MuteIndicatorPosition.TRAILING_BOTTOM) { + Icon( + modifier = Modifier + .testTag("Stream_ChannelMutedIcon") + .size(StreamTokens.size16), + painter = painterResource(id = R.drawable.stream_compose_ic_muted), + contentDescription = null, + tint = ChatTheme.colors.textTertiary, + ) + } } } } @@ -309,7 +331,22 @@ internal fun RowScope.DefaultChannelItemCenterContent( * @param users The list of users currently typing. */ @Composable -private fun UserTypingIndicator(users: List) { +private fun UserTypingIndicator(users: List, isDirectMessaging: Boolean) { + val context = LocalContext.current + val typingText = when { + isDirectMessaging -> stringResource(R.string.stream_compose_channel_list_typing) + users.size == 1 -> stringResource(R.string.stream_compose_channel_list_typing_one, users.first().name) + users.size == 2 -> stringResource( + R.string.stream_compose_channel_list_typing_two, + users[0].name, + users[1].name, + ) + else -> context.resources.getQuantityString( + R.plurals.stream_compose_channel_list_typing_many, + users.size, + users.size, + ) + } Row( modifier = Modifier, horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp (was 6dp) @@ -318,16 +355,11 @@ private fun UserTypingIndicator(users: List) { TypingIndicator() Text( modifier = Modifier.testTag("Stream_ChannelListTypingIndicator"), - text = LocalContext.current.resources.getQuantityString( - R.plurals.stream_compose_message_list_header_typing_users, - users.size, - users.first().name, - users.size - 1, - ), + text = typingText, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.captionDefault, // was body - color = ChatTheme.colors.textSecondary, // was textLowEmphasis + style = ChatTheme.typography.captionDefault, + color = ChatTheme.colors.textSecondary, ) } } @@ -383,6 +415,25 @@ internal fun ChannelItemMuted() { ) } +@Preview(showBackground = true) +@Composable +private fun ChannelItemMutedTrailingBottomPreview() { + ChatTheme( + config = ChatConfig(channelList = ChannelListConfig(muteIndicatorPosition = MuteIndicatorPosition.TRAILING_BOTTOM)), + ) { + ChannelItemMutedTrailingBottom() + } +} + +@Composable +internal fun ChannelItemMutedTrailingBottom() { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isMuted = true, + ) +} + @Preview(showBackground = true) @Composable private fun ChannelItemUnreadMessagesPreview() { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt index ac0ff1af821..6f6cca6d329 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt @@ -16,13 +16,34 @@ package io.getstream.chat.android.compose.ui.theme +/** + * Defines where the mute indicator icon is placed in the channel list item. + */ +public enum class MuteIndicatorPosition { + /** Icon appears inline after the channel name in the title row. */ + INLINE_TITLE, + /** Icon appears at the trailing end of the message/preview row. */ + TRAILING_BOTTOM, +} + +/** + * Behavioral configuration for the channel list. + * + * @param muteIndicatorPosition Where the mute icon is placed in the channel list item. + */ +public data class ChannelListConfig( + val muteIndicatorPosition: MuteIndicatorPosition = MuteIndicatorPosition.INLINE_TITLE, +) + /** * Central behavioral configuration for the Chat SDK, accessible through `ChatTheme.config`. * * @param composer Configuration for the message composer behavior. + * @param channelList Configuration for the channel list behavior. */ public data class ChatConfig( val composer: ComposerConfig = ComposerConfig(), + val channelList: ChannelListConfig = ChannelListConfig(), ) /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt index 20055524ccf..dd08eba54d2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt @@ -37,6 +37,7 @@ internal object StreamTokens { val radius4xl = CornerSize(32.dp) val size12 = 12.dp + val size16 = 16.dp val spacing3xs = 2.dp val spacing2xs = 4.dp diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt index 2db524b8088..786b078b31a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt @@ -176,7 +176,22 @@ private class DefaultMessagePreviewFormatter( message.isSystem() -> append(displayedText) message.isDeleted() -> { - appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) + if (isDirectMessaging && message.user.id == currentUser?.id) { + // DM outgoing deleted: explicit "You:" since delivery icon is absent + val youLabel = context.getString(R.string.stream_compose_channel_list_you) + append("$youLabel: ") + addStyle( + SpanStyle( + fontStyle = senderNameTextStyle.fontStyle, + fontWeight = senderNameTextStyle.fontWeight, + fontFamily = senderNameTextStyle.fontFamily, + ), + start = 0, + end = youLabel.length, + ) + } else { + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) + } append(context.getString(R.string.stream_compose_message_deleted_preview)) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt index 50eb4cc571a..66682ab2beb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt @@ -57,6 +57,7 @@ internal fun Message.getSenderDisplayName( isDirectMessaging: Boolean = false, ): String? = when { + user.id == currentUser?.id && isDirectMessaging -> null user.id == currentUser?.id -> context.getString(R.string.stream_compose_channel_list_you) isDirectMessaging -> null else -> user.name 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 ce8f6fd6487..36d5a10610e 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -25,6 +25,13 @@ Draft: You + Typing + %s is typing + %1$s and %2$s are typing + + %d person is typing + %d people are typing + No channels available No results for \"%1$s\" diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt index 51bee81da52..169593ed731 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt @@ -19,12 +19,17 @@ package io.getstream.chat.android.compose.ui.channels import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import io.getstream.chat.android.compose.ui.PaparazziComposeTest +import io.getstream.chat.android.compose.ui.theme.ChatConfig +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.ChannelListConfig +import io.getstream.chat.android.compose.ui.theme.MuteIndicatorPosition import io.getstream.chat.android.compose.ui.channels.list.ChannelItemDraftMessage import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessageDeliveredStatus import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessagePendingStatus import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessageSeenStatus import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessageSentStatus import io.getstream.chat.android.compose.ui.channels.list.ChannelItemMuted +import io.getstream.chat.android.compose.ui.channels.list.ChannelItemMutedTrailingBottom import io.getstream.chat.android.compose.ui.channels.list.ChannelItemNoMessages import io.getstream.chat.android.compose.ui.channels.list.ChannelItemUnreadMessages import org.junit.Rule @@ -49,6 +54,21 @@ internal class ChannelItemTest : PaparazziComposeTest { } } + @Test + fun `muted channel trailing bottom`() { + snapshot { + ChatTheme( + config = ChatConfig( + channelList = ChannelListConfig( + muteIndicatorPosition = MuteIndicatorPosition.TRAILING_BOTTOM, + ), + ), + ) { + ChannelItemMutedTrailingBottom() + } + } + } + @Test fun `unread messages`() { snapshotWithDarkMode { diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png index 7d6edaa7d60f0b040905d89ce6c2ad04da840024..8166b9d66d94c71ba374fa8e5f9f4529585fa475 100644 GIT binary patch literal 16996 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 literal 16995 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 BlWzb3 diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png index d6e564fdbc820e225ace62945251f4580eaba44b..51daf557a744e3fd121885780f99584946412a49 100644 GIT binary patch delta 12896 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 G-}ygJQj13b delta 12967 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 literal 14795 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 diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png index 9c6923dac4c2033c9524ab9e4b0dbe44bcac8fba..bdfa37636f10957cf3d2a755e9fe4e9f931caaa0 100644 GIT binary patch delta 7615 zcma)>X;hO}^zN~=(pH9AL{J1uQ3RP}hCrZ}3I!Qd6l4~eWHurs1d_KF3o-@?gUApq zlZX%*gM@%qAj~K;LWm{|LPA2Ak`R*I*Z;cre!VwqWv%le?>T3mv-h)~-k7pb^dhA3p5Ojih~c&hc|x$m6{XkrR=Dyev`>>s}y4^W9VtJV(1W%at7lwvEm z2XAf6(^*0y`@@f2-%I?ZE-7)dUqV932pnQ#WR-z{*LsAHnj1u*#3)+UT5I>q9Pw2^ ziDZOt7~RFpXT_EvV;mb zk8OO(Da*icaccna}eJ*1*)syu0`eRN-0w5}si;x59wX+eps*Q?Se-sB!SekW=G|D;f+ z__}96vn!sP8PHHV?HxvG*?$sQgA>w<;*n+TEC{0>=iwEd9fI!fnF-v8pK+_JZ}cd$ zO)c>B(yJ$pB99y#L!<*LE*B0K|jlcA2&X`GLleptYqvxv> zJR`2cHks6wHzoVFA1-fWX3e-^P@+20?e5A~ZfGDfMp*5qo85_W0gCZ`n zv;H9NBVuA?g2bib{A?F-Y=`r~f1R-irX96~F`c5J`?EnIgwYd{!UR8cwdbnDQA6+zR?P8@-u6XU}UL6hq_cJs$F#a_e27= zqSzMBvZi9dB)AmO-=hZ@vPtG*wZ#uXeU-8f@L|z2eIfaEthjkl^k^*8$D8(<3R4^` zMhO%M{ypE{{;)*|AF3y~ly%&5c_-%8(Bf@`I}^;N%WKo+si%x|Oubrpk8K3Z=E>@j z`*Y-z1e1|c%Vs6BG6-Co|1r8WbgbO3lQbVAj)XK(uB@%s0eEfu_hGmZ*OI8|m$Y1M z73a8>VUFGGLHlsGo3*LIT8K)5hB zPvlgWWD?-(;WY-kLBTXuQ>T(a6<(fR)qaUt9F}p#2)Xlxwl(U9(Ic9!e+hydHxSHM zU_4G4xj_NpJ@42QnP$A_!gs5JqtYjC{vcDVq`=qHDlwmL>Kjr+@*dHmnO)@axe)k5 zVpwnE&(nTDO3O&we8{x+3Cl~7{iUX#n6$0P22vX{p7x1y5JX`PjR}i%@OhMspL;pK zPswd&`yfJxd9=b(IUC+nT5k9hGM1Y1^$L&xcn631ep)c@5Kf%zC}YsQ<-{?A#Iv4| z7HtF@0SVlJI(&{(kuTEgW31S$Rn>ynlIkq|sQ8T3`R?6mpS6AE80QVldTw^!dTdzh z3%vW{WUadn+NC;q%aOC?WTz>cum|nm`bE@9u>ACNa<6}H?XMKsTCRowjI}Wpa4vPI zwkZQkn5M?k_rug9{@P4aLqFCR3@y)TpbHDq0@#ZYVjo7Ok*&K32^5W`jr- znW~m2|CfF?1amXvko(8yS1N#Uq;gctEV#|4`XNSHvJ>|tx#3#(rRS)$iU~c!Vj(oR zbLY_|*qg2Cc{C??9Z_CAex>axo}6t?3GRQj;2knGHDvG1s?;jw#W3U8o>_JfX;OJvs1{tt|JB= z&)fZN&L0o1DT6z@S#!9w_;Q|r+B`&Gg7fiL;-h6_LUqNW!S>N+vewgt0E$ZY9TxeXb$y!^L+x{$UuXk0Kcx z)wnzze!%;o7ap?Dd$>N2$V^Ua!xjJ9v_| zM~S`hv=!EYR4I(!r=S+QOC%;ld}u_qO?Z|zpDSpU zQ@vZufuX@l$MA~klFIlQ>Z|FLluPV1j2dJG7ZsEIF%FL!x4#Y~&>QNMG+>cVi@W0_*xOrq{XbRShd~wf(x??Jw)?8S{0<}TIMf=z#?7N9%@64&+6jsG^JJV z`e!#&3d?_DWfD{DkDtX`@!kXP23_aHZ>7{igDQ5^wk_DsSp^5EsE)+urkMd9XioJW z@o)1#u{I*aGbyTT8ANZx%M|iLjcgpfze9t-JiSW|&ZSA+`2BCoZLy6S{F9h+&c)qrK zg!B`hgJ_h>IJ~O5AJ?+IWyEJs(^CFJt(@b|6+j^0Oby zIso9Zt6@IXX~TvpRt3dO<4%O`W9}=|k{$0+d%l8thXj0Ud+1liu9TuggFW$-R z3AcCRgpx6+6}lm?n1|y><>6@HWKD*s!O6)tc^0y zjq*;^C}ZxA*SK~6;}3j!A2=6JA6TNjc2Cy$<~L#QwhoFjJQKjfQ8!qQ8j4}VQIjn! zH;EHr8m70N2KE-;rF%7OuLi$}5z3f5Max>eS~uvVPk>=oQ<<;mzltrRfXOSs+VcuX zrDk0r&e38KZhi4LvTNBmjI4qK?Hb%WI=l5i^yX(CoN%va2{c-x0DGK{vY{Ua+O=r=Sjh-@bCsl{%~~9g`W;>8 zysJKOYqfB0HvMctoyhE##I7ylC2a;K-v9eBxj3x6m@;^Ge=TfM$w;`rI~2243p2ka zsAyvJT}Ve$p9YehvxHy%4l9V$od&8?NjjcTJQ4Jbj> zbLK1e%#$<1-2`AhEEb@mKz#%D`VVQ&){5S%qkdPp?rtOY&NZWDk=EwGrCkST?@yMTN^BE>vNR;1*2xvW zCj@lgoA#27=o}j>d7cj9xc$$h2@9WA>19KkzzTGg=UcdV>9Mpjy&BMQ4B}jm$ zcS6~SxY6a1z<1S4Xy8C34@*BksbTlR-W7`^;ho@m*PG&o{F**A&nzYhpkFsEvI_<2 zV@S3=yGCO+q9nSx=E&^Lit1WQ(b*R|q{!Pf$Mq((DMX+qUU4h`n^u`(M2~y39>EDz zM#C}$O%Zc%eY)PKeoXu=$ZAKR_SI{BCC8mEHjY){WHOirluj1Y*lt=*T(QtoEpp))~K-+`x8;RO$bXfmo}b zloftUkttRaeIfrK@zl`!*^6%_-v7z}wfXJO{~T%_l56``>ev~_yh{5g*ppgDKyd$` z`|Ef74slHWbYZg zg9)6qz>I8qI)-n<4}mn8Z0wYpRe1&)Oj{;U5_}S7+yD{8iuua!q~+byBqW7D6xP#)*#-+1JL;ONo~~TTPf29obR4d1Z+eG4CQx?28j1(m`FHy z_TOM8yk_-|%NPd7baMKdxL(4b`kjQJO4Of`k_L(Je-gjHuqaP z#?*$a;OX1gUUeoWaWE6Mo1%8`6pLBCOYqevmw!P z2T%+j>Y#oRn3CSV+i9x4&JU<@DKQHnboRj1>+oI3y`})+x~FMOc6#H_@3m4@Hqxr`P zS9>^G>Z&fSEomUI9c*1lIb1+;O?KJBn7W=*>ODl_>si1uU3T$43x5|-uePo{i3E|9 z^V-QM_%!M}yA`sn2nLg2fdz2GZsF>q?(BBcU-yAAK;-Uw9NuaYyJ#(=Y^Dp^Lgm)Q zZyLW+A*Nkf3-D?V9l1JI4sFKNj8nW}I@i$BF4Bcg0a%b0@k)?_RA=iJKX26xTRRX& zZ*~ax<|l+)YCr}89Fy$W{$#KRP)H7s!YrFs8x~hUfXlK>YX<73 zRcUAzQC%YYC!5;?tjbLq8hQh~Vc6ytYq{dv^{vx`{-Oz-=w2f=Y+hauS3)sKv%~}D&ya8=lkE}Qk-Iu%pJ=N&O5wm$EV{cZPGnYllWcZ@_%L;gT zIj>R7*>nOCyp1s071t>p6%!;(Y=q;)xOyxRyc_-V7lLchR(fn4AdIn1!8WioM&7JUTQxv1Pi^Wxay-Fb3naVVfO3HUINitR z1x}!z;S=fP0KW6`{bY4{NSK@6(783(6lA|8>2(S4L*^8R;ZSBMb`n*`|2QF`=Uof^ z2KJySe%kA-6~}jt?7k0qJ)U-Z#>4llEpFuR1Day@bJ!85w6%H0{z4k&rydJli{Fuy&JvoqM4p!bzEw!FoCQBpHwo2 z(b_wX5gwjO-h8Vt_Eec5z0owJzXsa5hWPJ$D+;%K%I|XQ+!lbUV{*#)$+a3>d3Nde z1$kU-@*+$g8J?OI+;8$H?7zBkLwQ?E8%~crxOwW3K5>}+@3l)X9jAlhbmDD#OwB(E z80U9= z_W>;}eLMw4??I$GOCB@7Pv#g*#iGBa4wRjK+EM0m^y(wdCgbpW+k8x&VX%Fc(q}8T zyh`xJXX#J)LmBR;J%Mlg{#G41?U=Xjx$y3aP#gF$rem$z9)@lULG~})c_%Y>b3o1S zd@^s-B)C79cB2^Kb?UU1W8QJR1;W<$FOiy<=a7wcJ!!P7>zd{dxkdAe*rx3)gITm~ z`&IwDB+oc{%lc@hIqx2g13TqZyX%_c_O0Q38!Y~`qfv076YS@g$vr)dhA_{o(Y#dkz7xRymgEM)o`aduQjHJTsxI@WI)=gF#CBc9E$=bFP3D4@9LB{f+(=3Nuh z_ytaKd)cdM+m(+p4MW%Cyf+(z$of0^8+q$959H2PNs0HyOu|MNy!~wqDchgq!+Fc* zH99fvIzmC!%y?KPZ*}uY343H1DD^@oEF!1|qW5fr7OJr3sL(XzfKG`o9tWG9< zQgbhIKU^I{N~=0y95%&3+3a%x1vY~|u+3I2_cOhfD&%}*ZuF5SfYnmlsvx4=i~ZfX zwF*b9Fw{}zhBef&O5Hf{zi;jAuhMTHNXa)F9)0&Y-#D#`?h37;zDF)ia zgXNjUVSD=pC-)i9lwr|8-zO%@L(K?-vb@7eT2ku`!6ifVRD5<}yU$OWP2qS~$NyDa z%}lQmYO5G>7#)C3S~1wCYR#X4Z?7pe8#h_gG~HXJ4;7w1xXSF zQB`oMzR50gxy4?)HuM?0v;ZIa?v~Gka+D~f+wrQTBG3@@-lTff$Rzmm6xf)i85QJE z!eo!%?}=RN9qweS;OWku6}!mKJoU14sl{LBqW@k-_m_IcCsuXeuA?rW4(E^LK5FT= zTX_K}vt5gPQ9dW{y;ZoL5^{5Ys1-hm$amijAj=^Y5BU4c=vAr1o@z!{@b}+MgbVdw za%>#Bq54FYpPV(z+z~Xz;UEc$ZdWYS!=X*@Q|?fewp|k_iUDOnmP?g7*7nKT3uU&p zex^@?D>KnQf&UHz-T1%HEdrC_6BCjckufoHtwG>0i6pHVhgM^eGA@#-{vHX5RqCL> zZk|xnCm9m0b4clu09~wx+^J=Dk9D6PjLka@W+hFoxN|}vR#!GQj!Z05j!F6@>EWIV z*QQVv$z1y#V=(3uCxBwA9qg^$eQjkSS(5*-+hhOe;C==w;Q+NH*FSlAm-b5_Se;)_ zvy(qfoTJzjjM9=;EP+A)u)(wLk)2VConsT}(9~=BIENM9#OR1?6X7WhQEG7+u`=w1 z#-2W5l(%jkY;SF8L`)9;hnV2Hk#xb#o_57?w|3m`(p08Y%F61iM8pGn(AS?H5A1h- zaZ(xf28gy+dHZ~ZT$zrouH>jC=Qj{{Ma+GSwI~Wk0fehlCQJX;DenId(apuJREl#{ zODLp#+i~qh|L8ICX^?6vzmqN2%DJAN=1^pFOS?Scm%ic7Jo#`xn9y%-s2t6zx_Huv zKfrdiK;79B-8yYrEg%0iRK0f~&Dkpn;m$|_h3j60I$9CBbrz>TEjaHBZV~%PLlwKA zB@5KPO&!iUVLZOGHen3@?IS61qk5Tm#%%bqL}7TN*DYdC zs#o9vj}s2!+8PB;bzygI%Sya4(lfghRWy5ECmDo;@c$nCFlQhkaWZ9AzcJ9`u?6hb zkD%c{D)C!IF1tvf+POFn1TATa_s@GA!P(X)Yrmr8xg?&c#BV{5pa;n5mtOw^mw}_D zmR|=(!4iM8ke!ltuP+P|;n-$o3mDZf;BbHe1puNVKgD5v}{yLa3R z{@Ge~HP`& z`#?@E^-g%Jov;3ZMKstS^@oM`!_mR1f&~P|>jfk~Y(-cS3aH7l))K+0l)<%wc((OB=j0y1nw@VW|3E{E zeNiMSHO{ZGD^Re2-uV=wXq6B{3Ph@oCzT{+z!_~(!gm5bMPw2n_T%+U{M2fJbjc*~ z4$|93&350JfCuKPU>it$L3<>AidMXat$PeI)9oE0xb0lbEXw9KBd1qrIe@K2y|s2# z`7~qBx_K-820rrs;ti$xB<^}KpsSA-+~ah<)lD59YI;*@)jruvxYV#9xjzUkd}NLO zrHUYfp4MU0V-qoiC}G4|JzhEW-hQaLsp>TEE}-@yM{*?r7sG(Y?0nC2zTCpX$U|zM z$!gb`ct!X?d5279OyLl~yRO^p{!*=mZKZR#QD~vh+Fx_pK}EbxygokC;teULIS?G+ z3+%Fu;?`zzv;Ed)^vB@qs3Ok}UoHlRz65N{t6s zB^3gdLd!=QQWNtzW++NYplwYzsV%s^xr?NS77Ug}76yF=N(<07$sxd*NbgS(eY_`v( z5Cqh>-$5lS8Hc$F`#<)oJa-H2OYnqO#E$z_^nQ<0H=$?JBmDe83|(3d4fgFEkF1`! zQJrXN?9&vF6g}L($=#h_Dr2n*mdelZPQc>^o=yZcMYIO{MD)>?LUAlYa8x$8D&`&~ z%~orNi~)P+;5oE@5;7ylkQJ?qjG^Qq?jM1sz zGvY?RQ-s#+of;U``{QLgs|&@MUTyd|$JgDeELb!SFsFCK2|2K*G+Yhx zMpC7*dVmV|=;18BI5!+QnaG_$EvM3a6tJ^JJDfVqK~I0GI;S-7ly>g|abmSw_MSbI z;l|`Y6(5ODIX5HN#tuMMc!?`p3W=op`%#r|cq1kgUV$_YXBjG5ZmTz}H`KqX3vWi9 zx7yu#A`j8!dDxU19J4qY|D|X7)fE$RAN}YO^<>hO10W2BST~l#TXZ9D8{*u36GbP~ z4F1g=wzmy381UaJ=#QT|u2zPih30>`n&Q~RHu0bvN+LQq<^XL+InvRk&QYg#rkytK z($y&0>YC!%RZ}$7R=BSME^Jkm{!(+1)J~wzeHBVd1WaiWWr6 ze+dAhZ~YUEHdDxO7bRI6UK&tNxb0W84vpc`A+Cr%@I-O-IZ(-96XH4Ln5@lHawj?I zAywkr6KHKU4vN4I`HiS54~1^#-m5qNPY;x zLbDK5wBY>1O7wm~d^g(81ABSC~(q0Uvm_f7+^pqt`D~|9Z?WfP41{+NKgJ_$tm_ZA^M<%>jT& zg*{(c`1Zpex!e`^*_h~-h+E1$OL@ay9!;Dr)YN`uQ3ua%jVg?O8(lurvca{f)bfGt z8>5q0&YNAl!idW)^1<{9QBO87yzW&>EZZfZp`FcokUh9e#>gn2wkE87AYUtD@OYAgaNac;ly^@l2@JZhRwjj z`>*uwqR{bH7Ty)jT_~uw9k)}CD)n?fRK6`w%GYthS1#u7ZcqXNVy^69#l008t=Jzv zu|g{i=BQuY?CINDFB<=p9Pwl+LsfLAz6=PH6NQiz`c7wb#T2BZNz1UH{RtcQVRFG|5O0W=PaC#De zp*Q|c4@Dg^X@uphn=MP9>=kq~7uuvGP9@o<*9T^`aE7a=WlH`=+3WXhUnfyk=Rc;_ z&q66RgHbP;oMT9}g3N3xmHnJrSy8G+%tnxGDid436Vp?BT6#Fh+|)8@MWx2aAcprn z;BD0M$6f`nL%7Bfo5$5xx#B34b^MHX5Io3gW1EC_x`p-aly)EmrB@a`CXe398_b+XBMi+4{ zn#bx^uh|pRS#8Zysgpw`i7fD6d|tvFZwtGrV2I z{@rRK9Oru1k?YTr0=2OQ%B@9~29NhuxPyaatiPWDewKVcj&PT@S9dYLy#sL7J)kAj zyrs`@Gb#Ul64E7$x=v#^EN@e*YfJUuOwFRgj1m~<5ye%)!!o*a-INtqUiTqB;E0Rm zll#;ViX+}_RT2EhuB?9mSrvLoyFA9uMC`|OY};`Mfda@3So@11$S*Si+6P8Bj{FX@~zqj2k}jW?smdx`aD0!k5N zB)z)s?ZvW)whyefi?FW;Jo*;KZhx7mUl^BG1a3V%d8njRCB}moBY0N7TyJdiHf%s8 z0oIj^A2?{NMp@pX&y{V9Ft%D*Jtb!YOiuSWN!N7m|FN7ReG4bya34n}Gjn48ob)hj z^+Gg0yk{q7PlcKyWRp<8SK&r$>xkc3;fE;41B2yUD$B!oKi}i*_0_Zfx?3Rqp=2yWm zS|jV>%_g3pNcP8AM=+=JqAFNSeYGv7wA50O&O0}6SHw=~D4ss;*?yvTc-+|rHEccT z?i;Tu@{q9++_kmK)i~C$ROejf3l6Xr30R9eOa*YRwBn^$^MHQACf*3np>M4;A?o%@ z3&EPN3nxD+H4Juws^D`pY{$O7^BEX`7+tlA1tI7$)cTBGopV2@ICK_DEoq?Tk!R{$U z-@WN%KEq+~GM~d}!wYxU0mwH=EdDmGJ#y9@pkyZlTQ%*oQ1UBv;YKQtOohE%;aYY? zkUIn3yLRFAdh@-l`|U9P^&1p}kmHKr+?5cVSTt%SiTD%(@S~r%R7SFrW9rtFMfIDZ zQ|s)r)p)Tj$dyI`Gz!I@O8jZK*x*=1`=}j{FJvQtKv2)X#~Vz_lP)ak7_;;3+rk(C zH}KKo$+_R0KkG@?Z>wZmtaS^_-MynPCRglNgB=y^9X#UJhyCgS#4@AM>nmdCI2@SB zM#LkCRod1%#m0&?Y^ZoCC8O3{2`uk@_gr#`I4X$1bxI=Za~n5l9Zqo&tIpv$HwB4k zeJ8fR9o4Tu&j%TB`!6|9-~9l8E*h)K6LWccIqWAn>nWImPMW`$n`h`Nfw{p}n)2C5 zZ(+Yl#I&ml7})-(E|7G6d_vei5dUVaXSs4~<0Gj4W?PVWX9^kOV=>4SRtdgq9|qAG zLz!Z6EaU&{<%3#J}x@RPn;9E37yo zRTcUgR&wR+tPvbo7|c8+Qz8;_(L#yFa~Z{^mjb1^kGA36eN`HvxIYht;091S7;MMD zL)2umc*}Ww_4x14=?Ut|+kVsc@TVOj0dWW%DlEjjl24|bRO7Z26#+Pp*IhZ)b0;wpQ`Ed=tfPWc-oNdp1YW>qbN63`@Tk@E)L&hG0ew&cr(d(Q z{k>muveVS#oryClzkkS9Yi*xzzM&-cVK0Uqox6cj zj_NZ|hF}Oo2co~+lP;yg?}qW&Pgj+|VXraKL4a8IiW#Lrl9=uLtuEUD8MvmPVONoH}gGbipd7LhE1uE%M zs-udoSNGhx{i?1Di?=i6^yIM{6RwGBfRI%+Zq29XS#==qev7mmQO8K)pODSIJ&*#9 zY_p%Th7|TzL)q1?N^A`n7@*WVseljVG<^9nTEhYYF=rR|t0%r4#1DSRcoNkftxCB& zk(25uw@|Z@Ki@s_cN+QGwcn_UPLt`6gASsX#OQ|}u+?%Jb{80JC_oC*5(@nHbnG$o5ND9aUu51zzXV#`X{4+gm=Av+~tB+9j?*plo`@$I=N}^;X za7W0u_YZl+&){BaaQeGe`~b=rbF6NgR(Rc372Gv*∓_0y^D5_Z{lS8mf)y2<~PD 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 U-{eV@z>m{ux4#-rUHi}f0VOm&fdBvi delta 9202 zcmZX4cT|&E*Dnq;GJb&37irRDP!JG82Z6viD5x|$N{a%5NHZcekjEKkKstyDp#&8K z5do!!l0=2jAr1(kM-w0dAu$0`NW0I>d%v~rS~veZS!bQH_da{?vwy#HMs7|8=j%nA zm!CO#;!@)5#)4e&rEOgQjaa|vVbACn?^F+*c$+n?jLb1ndGqk6ON?KR8ohdWG(IQ; z=c?q#$UM@w_zTxmA}oDz-lqeo#3EoFn zx^iqsM{I;;X$oo<-zN@QRJ!GP#aJZ@H`*uCbx>E?_z99-C9^+Kuv|2;G+3yy2vpB) z$#KWsp-)_w|M>4Hbqjb3`_#^cX6)r|=UgOTv`NYuW;9bI(Y7vq`MX?p&-Uf-L2_rh zu)xUlyWoi22N~-j2a}HKX;UAjKjSV)@3;6hE{1cD)1$fXzcB2L>&v){353s;^s$+^ z5(kOC(+btyv^<%G51YNwuN|J-u@as*$H)n^k#-IPxmwAL$MKX(Z{KGNzr{JjQRSi= zrF=W%hVHYojbBM^AJPaB+^=QW6ye@1pehiT;62Csq^F@VD(;{Ln=ag(X>qH-jYs2< zVg1Lfxv+=GevWSQb>ZHbxwVBUgt%h`4UOuUoT?=S3ac81>pwBb+AMf#PV2anBBhXT zN!KC&qoVJ2eg-_*8@7E9(ZPQifRct)q&~%qIKx}B5shSj-XnMD=pr02MGbn1zg4zK zc56(EOH4cYkG8@mN>3&qzx(~m)}TSf=M;?L#p-HB+ga=ihaPa}1lPRB zs4mVOirc!&B`vS_;WXrr@i)w!-H)|;Aj%W@Q8ncWwSqEs80;