Redesign channel list, swipe actions and actions bottom sheet#6181
Redesign channel list, swipe actions and actions bottom sheet#6181aleksandar-apostolov wants to merge 58 commits intov7from
Conversation
- 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
- 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
- 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
…tatus - 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)
- 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
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.
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.
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.
Match the composer's icon size (icon/size/md = 20dp) for consistent icon sizing across the SDK.
Replace the filled 24dp search icon with the stroked 20dp variant from Figma (IconMagnifyingGlassSearch), matching the new design system's stroked icon style.
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.
…l 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.
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.
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).
…illMaxSize 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.
…lignment - 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
- 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
# Conflicts: # stream-chat-android-compose/api/stream-chat-android-compose.api
…review Replace hardcoded 📊 emoji with ic_poll drawable via appendInlineContent, matching the pattern used by all other attachment type previews.
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
Fix detekt MaxLineLength/LongMethod/MagicNumber violations, spotless formatting, and docs compile error from formatMessagePreview signature.
…d poll sender prefix and chatTextRead token Replace filled delivery status icons (clock, single check, double check) with stroked outlines per Figma spec. Add sender name prefix for poll messages in group chats. Add chatTextRead semantic color token for read status icon tint.
Badge number felt oversized inside the 20dp pill with 2dp border. Switched from numericExtraLarge (14sp) to numericLarge (12sp) for better visual proportion.
Add visual feedback for focus (blue rounded border) and selected (tinted background) states on channel list items, matching the Figma design system interaction states.
Fix spotless violation in UnreadCountIndicator and re-record ChannelItemTest + ChannelListTest Paparazzi snapshots.
Channel preview now shows sender name for non-DM participant messages. Update assertMessageInChannelPreview to expect "ParticipantName: text" when fromCurrentUser is false.
Integrate AND-1089 design system migration (StreamDesign tokens). Conflict resolution: - StreamColors/StreamDimens deleted → inlined custom tokens (48dp avatar, 16dp icon) - borderCorePrimary → borderUtilitySelected - borderCoreSurfaceSubtle → borderCoreSubtle - barsBackground → backgroundElevationElevation1 - ChatTheme.shapes.header → RectangleShape - textLowEmphasis → textSecondary (MessagePreviewIconFactory) - Kept channel-list redesign layout, typography, and spacing - Regenerated .api file via apiDump - Accepted v7 snapshots (to be regenerated via Paparazzi)
Resolve conflicts from channel-list merge (which included v7/AND-1089 design system migration). Update legacy token references: - footnoteBold → captionEmphasis (SwipeActionItem) - errorAccent → accentError (ChatComponentFactory) - overlay → backgroundCoreScrim (SelectedChannelMenu) - highlight → backgroundCoreHighlight (CustomChatComponentFactory) - appBackground → backgroundCoreApp (CustomChatComponentFactory)
…com/GetStream/stream-chat-android into redesign/channel-list-swipe-actions
Replace legacy Material-style icons with filled variants from Stream Foundations. All icons now use 16x16 viewport with filled paths for consistent rendering. Mute/unmute switched from speaker to bell icons.
Mute/unmute: use IconMute (speaker) and IconVolumeFull instead of bell icons. Add: use IconPlusLarge (plus) instead of IconEditBigSolid (pen).
Reorder channel actions: ViewInfo, Mute, Pin, Archive, Leave, Delete. Destructive actions (Leave, Delete) are now grouped last. Mark LeaveGroup as isDestructive = true.
Replace horizontal member list with compact header: channel avatar (left) + channel name + member count/status (right). Matches Figma Channel Detail Sheet design.
…ions Channel list title: bodyDefault → headingSmall (semi-bold) to match Figma. Swipe actions: remove text label, show icon only in a centered Box.
Light gray background with dark icon instead of dark slate500 with white icon. Matches Figma swipe action design.
Item height 56dp → 48dp, icon container 56dp → 48dp with 12dp padding (24dp icon). More compact to match Figma design.
Icon: spacingXl (24dp) with spacingSm (12dp) horizontal padding. Replaces hardcoded 48dp/12dp values with design system tokens.
No dividers between channel options per Figma design.
Row height 48dp → 44dp, horizontal padding spacing2xs (4dp), icon 24dp with 8dp end gap. Matches Mobile / List Item Figma component.
…ngMd spacing2xs (4dp) → spacingMd (16dp) to match visible Figma padding.
spacingXs (8dp) bottom content padding for breathing room below last action item.
# Conflicts: # stream-chat-android-compose/api/stream-chat-android-compose.api # stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt
|
- Replace spinner with shimmer skeleton items (8 placeholder rows matching channel item layout: avatar + title/timestamp + message) - Redesign empty state: 32dp icon, "No conversations yet" copy, optional "Start a chat" CTA wired to header action - Update ChatComponentFactory.ChannelListEmptyContent with onStartChatClick callback - ChannelsScreen passes onHeaderActionClick as empty state CTA
WalkthroughThis pull request introduces a comprehensive redesign of the Stream Chat Android channel list UI, refactors the channel action system from a sealed class to an interface-based model with richer metadata, implements swipe-to-reveal actions for channel items, and updates message preview formatting. It includes planning documentation, configuration updates, and supporting visual assets. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as Channel UI
participant Coordinator as SwipeRevealCoordinator
participant ViewModel as ChannelListViewModel
participant Action as ChannelAction
User->>UI: Swipes channel item right
UI->>Coordinator: onItemOpened(channelCid)
Coordinator->>Coordinator: Close all other open items
UI->>UI: Show swipe actions (Mute/Pin/Archive/More)
User->>UI: Taps swipe action
UI->>ViewModel: executeOrConfirm(action)
alt Action requires confirmation
ViewModel->>UI: Show ConfirmationPopup
User->>UI: Confirms action
UI->>ViewModel: confirmPendingAction()
else No confirmation needed
ViewModel->>ViewModel: Execute immediately
end
ViewModel->>Action: action.onAction()
Action->>Action: Perform channel operation
ViewModel->>UI: Update channel state
UI->>Coordinator: closeAll()
UI->>UI: Hide swipe actions, refresh list
sequenceDiagram
participant Compose as Compose Layout
participant ChannelItem as ChannelItem
participant Config as ChannelListConfig
participant Theme as ChatTheme
Compose->>Config: Query muteIndicatorPosition
Config-->>Compose: Returns INLINE_TITLE or TRAILING_BOTTOM
alt INLINE_TITLE
Compose->>ChannelItem: Render mute icon in title row
ChannelItem->>ChannelItem: Center content layout
else TRAILING_BOTTOM
Compose->>ChannelItem: Render mute icon in message preview row
ChannelItem->>ChannelItem: Trailing bottom layout
end
Compose->>Config: Query swipeActionsEnabled
Config-->>Compose: Returns true/false
alt swipeActionsEnabled true
Compose->>ChannelItem: Wrap with SwipeableChannelItem
ChannelItem->>Theme: Get DefaultChannelSwipeActions
else swipeActionsEnabled false
Compose->>ChannelItem: Render standard item
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt (1)
74-92:⚠️ Potential issue | 🟡 MinorKDoc on the public overload does not document the newly handled
FAILED_PERMANENTLYstate.The
@paramblock and description make no mention of when/how the error state is rendered. Consider adding a note so consumers understand the full state machine.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt` around lines 74 - 92, Update the KDoc for the public MessageReadStatusIcon composable to document the newly-handled FAILED_PERMANENTLY state: mention that the composable now renders an error/failed state when message.syncStatus == SyncStatus.FAILED_PERMANENTLY (or equivalent), describe which icon/composable (e.g., isPendingIcon/isSentIcon/isDeliveredIcon or a new failure representation) will be shown in that case, and add an `@param` entry or sentence explaining when and why the failed state is displayed so callers understand the full state machine.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt (1)
97-101:⚠️ Potential issue | 🟡 MinorUse local text state for clear/placeholder visibility.
The clear button and placeholder are keyed off
query, but typing first updatestextState. This can cause transient flicker/desync until parent state catches up.💡 Proposed fix
- val trailingContent: (`@Composable` RowScope.() -> Unit)? = if (isFocused && query.isNotEmpty()) { + val hasText = textState.text.isNotEmpty() + val trailingContent: (`@Composable` RowScope.() -> Unit)? = if (isFocused && hasText) { clearButton } else { null } @@ - if (query.isEmpty()) { + if (textState.text.isEmpty()) { label() }Also applies to: 163-164
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt` around lines 97 - 101, The clear button and placeholder visibility are currently driven by the external `query` prop which lags the local input buffer, causing flicker; change the visibility checks to use the local compose state (`textState`) instead—e.g., in the `trailingContent` assignment (where `isFocused && query.isNotEmpty()` is used) and the placeholder visibility logic (lines referenced around 163-164) use `isFocused && textState.text.isNotEmpty()` or `textState.value.text.isNotEmpty()` depending on your TextFieldValue type so the UI reacts immediately to local edits before the parent `query` updates.
♻️ Duplicate comments (1)
.planning/ROADMAP.md (1)
1-130: No additional issues in this file beyond the scope concern already noted.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/ROADMAP.md around lines 1 - 130, Remove the stray "[duplicate_comment]" marker from the review comment content associated with "Roadmap: Release Process Migration" so the comment reads only the intended sentence ("No additional issues in this file beyond the scope concern already noted.")—locate the review text containing the duplicate token and delete it, leaving the comment clean and unambiguous.
🧹 Nitpick comments (18)
stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml (1)
24-29:strokeColorshould use a placeholder color, not a semantic gray.The project convention for
stream_compose_ic_*.xmldrawables is to use a hardcoded placeholder color (e.g.#FFFFFF) because tint is applied programmatically by the Compose UI layer at runtime. Using#687385(a specific semantic gray) embeds a design-specific value in the asset instead of a neutral placeholder. While Android'ssrc_intint mode will still override it at render time, it violates the established pattern and can cause the icon to render with the wrong color if the composable consumer forgets to apply a tint.🎨 Proposed fix
- android:strokeColor="#687385" + android:strokeColor="#FFFFFF"Based on learnings: "For drawable XMLs named
stream_compose_ic_*.xmlin thestream-chat-android-composemodule, use hardcoded placeholder colors because tint is applied programmatically when used in composables to ensure theme-aware coloring at runtime."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml` around lines 24 - 29, Replace the hardcoded semantic gray in the search drawable with the project's placeholder color: open stream_compose_ic_search.xml and change the android:strokeColor attribute (and any other explicit semantic colors in this drawable) from "#687385" to the neutral placeholder (e.g. "#FFFFFF") so the Compose layer can apply tint programmatically; ensure android:fillColor remains transparent and strokeWidth/strokeLineCap/strokeLineJoin are unchanged.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt (1)
67-132: Avoid undocumented suppression by extracting a small icon builder helper.Line [67] introduces
@Suppress("LongMethod")without rationale. Prefer removing the suppression and extracting duplication into a helper so the method stays within limits naturally.♻️ Proposed refactor
- `@Suppress`("LongMethod") override fun createPreviewIcons(): Map<String, InlineTextContent> { val placeholder = Placeholder( 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.textSecondary, - ) - }, - PHOTO to InlineTextContent(placeholder) { - Icon( - modifier = iconModifier, - painter = painterResource(id = R.drawable.stream_compose_ic_camera), - contentDescription = null, - tint = ChatTheme.colors.textSecondary, - ) - }, + previewIcon(VOICE_MESSAGE, R.drawable.stream_compose_ic_mic, placeholder, iconModifier), + previewIcon(PHOTO, R.drawable.stream_compose_ic_camera, placeholder, iconModifier), // ...same for the rest ) } + + private fun previewIcon( + key: String, + drawableRes: Int, + placeholder: Placeholder, + iconModifier: Modifier, + ): Pair<String, InlineTextContent> = + key to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = drawableRes), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }As per coding guidelines: "
**/*.kt: Use@OptInannotations explicitly; avoid suppressions unless documented".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt` around lines 67 - 132, Remove the undocumented `@Suppress`("LongMethod") on MessagePreviewIconFactory.createPreviewIcons and refactor duplicated InlineTextContent + Icon creation into a small private helper (e.g., buildPreviewIcon(drawableRes: Int): InlineTextContent) that captures the Placeholder and Icon modifier; then replace each anonymous InlineTextContent block (for VOICE_MESSAGE, PHOTO, VIDEO, FILE, LINK, LOCATION, POLL) with calls to that helper, keeping Placeholder and iconModifier shared and eliminating the long-method suppression..planning/STATE.md (1)
5-9: Consider adding an owner/update cadence for status freshness.Lines 5–9 are time-sensitive and can become stale quickly; adding an “owner” and expected refresh cadence would reduce drift.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/STATE.md around lines 5 - 9, Update the time-sensitive block by adding explicit ownership and a refresh cadence so status remains fresh: add fields such as "Owner: <team/person>" and "Refresh cadence: <frequency, e.g. weekly/monthly/after-change>" alongside the existing "Phase", "Plan", "Status", and "Last activity" entries, and include a short sentence describing who is responsible for updates and when to update (e.g., "Owner will review and refresh this status every X days or after significant changes")..planning/PROJECT.md (1)
44-49: Add explicit target branch and coordination note in constraints.Please include that changes should target
developand note release-owner coordination when touching changelog/release-process artifacts.Based on learnings: Target
developbranch for PRs; sync with release owners before touching versioning, publishing, or changelog scripts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/PROJECT.md around lines 44 - 49, Update the Constraints section to explicitly state that PRs must target the develop branch and add a coordination note that any changes to versioning, publishing, changelog or release-process artifacts (e.g., CHANGELOG.md, release scripts) require prior sync with the release owner; reference the "Constraints" heading and the file name CHANGELOG.md so reviewers can find and enforce the rule.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt (1)
83-110:isErrorIconslot is absent from the public overload, breaking customisation parity.All four other delivery states (
isReadIcon,isPendingIcon,isSentIcon,isDeliveredIcon) are exposed as slot parameters so integrators can override them. The newly-handledFAILED_PERMANENTLYbranch (line 108) hard-wiresIsErrorIconwith no override path. This is inconsistent with the rest of the API surface and prevents theming/branding of the error state.♻️ Proposed refactor
`@Composable` public fun MessageReadStatusIcon( message: Message, isMessageRead: Boolean, modifier: Modifier = Modifier, isMessageDelivered: Boolean = false, readCount: Int = 0, isReadIcon: `@Composable` () -> Unit = { IsReadCount(modifier = modifier, readCount = readCount) }, isPendingIcon: `@Composable` () -> Unit = { IsPendingIcon(modifier = modifier) }, isSentIcon: `@Composable` () -> Unit = { IsSentIcon(modifier = modifier) }, isDeliveredIcon: `@Composable` () -> Unit = { IsDeliveredIcon(modifier = modifier) }, + isErrorIcon: `@Composable` () -> Unit = { IsErrorIcon(modifier = modifier) }, ) { val syncStatus = message.syncStatus when (syncStatus) { SyncStatus.IN_PROGRESS, SyncStatus.SYNC_NEEDED, SyncStatus.AWAITING_ATTACHMENTS, -> isPendingIcon() SyncStatus.COMPLETED -> when { isMessageRead -> isReadIcon() isMessageDelivered -> isDeliveredIcon() else -> isSentIcon() } - SyncStatus.FAILED_PERMANENTLY -> IsErrorIcon(modifier = modifier) + SyncStatus.FAILED_PERMANENTLY -> isErrorIcon() } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt` around lines 83 - 110, The MessageReadStatusIcon API hard-codes IsErrorIcon for SyncStatus.FAILED_PERMANENTLY and should expose an error slot for parity; add a new parameter isErrorIcon: `@Composable` () -> Unit to MessageReadStatusIcon (defaulting to { IsErrorIcon(modifier = modifier) }) and call that isErrorIcon() in the FAILED_PERMANENTLY branch instead of directly invoking IsErrorIcon, so integrators can override the error state rendering.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt (1)
36-47: KDoc is missing thecolorparameter.The public function signature exposes
coloras an overridable parameter whose default has just changed (fromaccentErrortoaccentPrimary). Documenting its intent and the new default helps API consumers understand the change.📝 Proposed addition
* `@param` unreadCount The number of messages the user didn't read. * `@param` modifier Modifier for styling. + * `@param` color Background color of the badge. Defaults to [ChatTheme.colors.accentPrimary]. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt` around lines 36 - 47, Update the KDoc for the composable function UnreadCountIndicator to include the missing `@param` entry for color, describing that it controls the badge tint and noting the default value (ChatTheme.colors.accentPrimary) and its intent (to match the app's accent for unread badges); keep other parameter docs intact and ensure the KDoc reflects the changed default from the previous accentError to accentPrimary so callers understand the behavioral change.stream-chat-android-compose/src/main/res/values/strings.xml (1)
31-34: Prefer positional numeric placeholders in new plurals.Using
%1$dinstead of%dmakes translator-side reordering safer and keeps formatting style more localization-friendly.♻️ Proposed i18n formatting tweak
<plurals name="stream_compose_channel_list_typing_many"> - <item quantity="one">%d person is typing</item> - <item quantity="other">%d people are typing</item> + <item quantity="one">%1$d person is typing</item> + <item quantity="other">%1$d people are typing</item> </plurals> @@ <plurals name="stream_compose_photos_preview"> - <item quantity="one">%d Photo</item> - <item quantity="other">%d Photos</item> + <item quantity="one">%1$d Photo</item> + <item quantity="other">%1$d Photos</item> </plurals> <plurals name="stream_compose_videos_preview"> - <item quantity="one">%d Video</item> - <item quantity="other">%d Videos</item> + <item quantity="one">%1$d Video</item> + <item quantity="other">%1$d Videos</item> </plurals> <plurals name="stream_compose_files_preview"> - <item quantity="one">%d File</item> - <item quantity="other">%d Files</item> + <item quantity="one">%1$d File</item> + <item quantity="other">%1$d Files</item> </plurals>Also applies to: 182-193
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/res/values/strings.xml` around lines 31 - 34, The plural resource stream_compose_channel_list_typing_many uses unpositioned numeric placeholders ("%d"); update both <item> entries to use positional placeholders ("%1$d") so translators can reorder safely and maintain consistent formatting; search for other plural resources (e.g., the similar block referenced at lines 182-193) and apply the same replacement to their numeric placeholders as well to ensure all plural strings use "%1$d".stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt (2)
93-93: Verify thatspacedByplus inner padding matches the intended Figma gap.
Arrangement.spacedBy(spacingMd)(16dp) applies between all three children. Combined with the center content's ownstart = spacing2xs(4dp), the effective visual gap between the avatar and text is 20dp, not 16dp. If the design intent is exactly 16dp edge-to-edge, consider removing the center content's horizontal start padding, or removing the arrangement spacing and relying solely on per-child padding.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt` at line 93, The horizontalArrangement uses Arrangement.spacedBy(StreamTokens.spacingMd) which adds 16dp between all children while the center content also has start = StreamTokens.spacing2xs (4dp), producing a 20dp gap; to match the Figma 16dp edge-to-edge spacing remove the center content's start padding (the padding on the composable that sets start = StreamTokens.spacing2xs) or alternatively remove the Arrangement.spacedBy and apply per-child paddings so only one 16dp gap exists between the avatar and text; update the code references: horizontalArrangement/Arrangement.spacedBy(StreamTokens.spacingMd) and the center content padding (start = StreamTokens.spacing2xs) accordingly.
207-213: Remove redundant textStyle override.The
Timestampcomposable's defaulttextStyleisChatTheme.typography.captionDefault.copy(color = ChatTheme.colors.textTertiary, lineHeight = StreamTokens.lineHeightNormal). SinceStreamTokens.lineHeightNormal = 20.sp, this explicit override passes identical values and can be removed:- Timestamp( - date = searchResultItemState.message.createdAt, - textStyle = ChatTheme.typography.captionDefault.copy( - color = ChatTheme.colors.textTertiary, - lineHeight = 20.sp, - ), - ) + Timestamp(date = searchResultItemState.message.createdAt)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt` around lines 207 - 213, The explicit textStyle override on the Timestamp composable is redundant because Timestamp already defaults to ChatTheme.typography.captionDefault.copy(color = ChatTheme.colors.textTertiary, lineHeight = StreamTokens.lineHeightNormal); remove the textStyle parameter from the Timestamp call (the one using searchResultItemState.message.createdAt) so it relies on the default style provided by Timestamp/ChatTheme.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt (1)
214-214: Prefer decomposition over an undocumentedLongMethodsuppression.
DefaultChannelItemCenterContentis now doing multiple responsibilities (title row, preview row, status/mute variants). Splitting into focused helpers would keep it maintainable without suppression.As per coding guidelines:
**/*.kt: Use@OptInannotations explicitly; avoid suppressions unless documented.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt` at line 214, The suppression of LongMethod should be removed and DefaultChannelItemCenterContent refactored into smaller helper composables: extract the title row rendering into a function like renderChannelTitleRow, the preview/last-message row into renderChannelPreviewRow, and status/mute and unread badge logic into renderChannelStatusOrMute; update DefaultChannelItemCenterContent to compose these helpers, keep parameter lists minimal and pass only necessary state (channel, lastMessagePreview, isMuted, isOnline, unreadCount), and add explicit `@OptIn` annotations if any experimental APIs are used instead of suppressing LongMethod.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt (1)
63-67: Update public API docs forisDirectMessagingand avoid undocumented suppression.The new public parameter is missing KDoc, and the new
LongMethodsuppression should be avoided or explicitly justified.📝 Proposed KDoc update
/** * Generates a preview text for the given message. * * `@param` message The message whose data is used to generate the preview text. * `@param` currentUser The currently logged in user. + * `@param` isDirectMessaging Whether preview formatting should use direct-message rules. * `@return` The formatted text representation for the given message. */As per coding guidelines:
**/*.kt: Document public APIs with KDoc, including thread expectations and state notes; and avoid suppressions unless documented.Also applies to: 163-163
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt` around lines 63 - 67, Add KDoc for the public function formatMessagePreview and its new parameter isDirectMessaging: document what the function returns, expected thread/state assumptions, and explain the boolean’s effect (e.g., how message preview differs in direct messaging vs channels). Remove the undocumented `@Suppress`("LongMethod") (or if you must keep it, add a KDoc justification comment explaining why the method legitimately exceeds length and why splitting isn't appropriate). Ensure the KDoc appears above the formatMessagePreview declaration and explicitly describes isDirectMessaging behavior and any thread/state constraints.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt (1)
74-74: Avoid introducing an undocumentedLongMethodsuppression.Consider extracting parts of
SearchInput(state sync / decoration) into helpers instead of suppressing the rule directly.As per coding guidelines:
**/*.kt: Use@OptInannotations explicitly; avoid suppressions unless documented.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt` at line 74, Remove the undocumented `@Suppress`("LongMethod") on SearchInput: instead split the large function by extracting the state synchronization logic and the TextField decoration into small helpers (e.g., create functions named syncSearchState(...) and decorateSearchTextField(...) called from SearchInput) so the body is shorter and the lint rule is satisfied; if any experimental APIs are required, replace suppressions with explicit `@OptIn`(...) annotations on SearchInput or the new helper functions and document the reason in a brief KDoc.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt (1)
21-36: Add thread/state notes to enum KDoc.Please include a brief statement about state/thread expectations for this public enum (for example, stateless style marker safe for use from any thread).
As per coding guidelines
**/*.kt: Document public APIs with KDoc, including thread expectations and state notes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt` around lines 21 - 36, The public enum SwipeActionStyle lacks KDoc about thread/state expectations; update the KDoc for SwipeActionStyle to add a short thread/state note stating that this enum is a stateless, immutable style marker and safe to reference from any thread (no synchronization required), and include that it carries no mutable state and is safe for concurrent use in UI code such as composables and background threads.stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt (1)
19-30: Expand public KDoc with thread/state expectations.This public API is documented, but it should also state thread expectations and state behavior (for example: immutable value object, safe to pass across threads, fields are resolved UI text).
As per coding guidelines
**/*.kt: Document public APIs with KDoc, including thread expectations and state notes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt` around lines 19 - 30, Update the KDoc for the public data class ConfirmationPopup to include thread and state expectations: state that this is an immutable value object (data class) safe to pass across threads, its properties (title, message, confirmButtonText) are resolved UI text and should not hold mutable or lifecycle-bound references, and note any expectations about resolution timing (e.g., already localized/resolved before construction) so callers know it's safe for background/worker threads and UI usage; reference the ConfirmationPopup type and its properties when adding these notes.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt (1)
164-170:firstOrNull { it != null }is equivalent tofirstNotNullOfOrNull { it }— but more importantly, for DMs the archive action is always returned.Since
archiveActionandpinActionare never null, the first non-null in the DM list[archiveAction, muteAction, pinAction]is alwaysarchiveAction, and in the group list[muteAction, archiveAction, pinAction], it'smuteActionif available, otherwisearchiveAction. The fallback priority works, but the function can never returnnulloverall (pin is always non-null). Consider simplifying the return type to non-nullable if that's the intent, or usefilterNotNull().first().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt` around lines 164 - 170, The current selection logic in DefaultChannelSwipeActions builds candidates = listOf(...) containing archiveAction, muteAction, pinAction and then returns candidates.firstOrNull { it != null }, but since pinAction (and archiveAction/pinAction) are non-null the function can never return null; update the implementation to reflect that non-null guarantee: either change the function return type to non-nullable and return candidates.filterNotNull().first() (or .first { true } after filtering) or, if null is genuinely possible, adjust the candidates construction so some entries can be null and use filterNotNull().firstOrNull(); reference the candidates variable and the archiveAction/muteAction/pinAction symbols when making the change.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt (1)
95-95: Document or remove the blanket suppression onbuildDefaultChannelActions.Line 95 adds
@Suppress("LongMethod", "LongParameterList")without rationale. Prefer further decomposition (or a scoped justification comment) instead of an undocumented suppression.As per coding guidelines:
**/*.kt: "Use@OptInannotations explicitly; avoid suppressions unless documented".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt` at line 95, The suppression on buildDefaultChannelActions is too broad and undocumented; remove the blanket `@Suppress`("LongMethod", "LongParameterList") and either refactor buildDefaultChannelActions into smaller helper functions (e.g., extract action creation into createChannelActionList, buildActionItem, configureActionHandlers) to reduce method length and parameter count, or if suppression is unavoidable, replace it with a narrow, documented suppression scoped to the specific offending expression(s) and add a one-line rationale comment above it; also ensure any experimental APIs used are annotated with `@OptIn` rather than suppressed.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt (1)
26-30: Add thread-confinement notes to public KDoc.Public methods mutate shared coordinator state, but KDoc doesn’t state thread expectations (e.g., main-thread use) or state guarantees.
As per coding guidelines:
**/*.kt: "Document public APIs with KDoc, including thread expectations and state notes".Also applies to: 34-66
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt` around lines 26 - 30, Update the KDoc for the public class SwipeRevealCoordinator to include thread-confinement and state guarantees: explicitly state that all public methods that mutate shared coordinator state must be called from the main/UI thread (or specify the required dispatcher), describe whether calls are reentrant or synchronized, and note any atomicity/visibility guarantees clients can rely on; also add the same thread/state note to each public method in the class so callers see the requirement at the call site.stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt (1)
63-73: Consider safe defaults for destructive actions.
LeaveGroupandDeleteConversationare destructive, butconfirmationPopupdefaults tonull(Line 67, Line 108). That makes accidental immediate execution easy when a caller forgets to pass confirmation metadata.Also applies to: 104-114
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt` around lines 63 - 73, LeaveGroup and DeleteConversation currently have confirmationPopup defaulting to null making destructive actions prone to accidental immediate execution; update their constructors (classes LeaveGroup and DeleteConversation) so confirmationPopup defaults to a safe non-null ConfirmationPopup (e.g., a standard destructive confirmation with title, message and confirm/cancel labels) instead of null, and ensure callers can still override it by providing a custom ConfirmationPopup via the confirmationPopup parameter declared on the ChannelAction implementations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.planning/PROJECT.md:
- Line 5: The planning note "Migrating stream-chat-android's release process to
match stream-video-android and stream-core-android" (the line referencing "No
code changes") is out of scope for this channel-list redesign PR; either remove
or relocate that planning track to its own PR and update .planning/PROJECT.md so
its content matches the channel-list redesign intent, specifically edit or
remove the sentence at the paragraph containing "Migrating stream-chat-android's
release process..." (and the duplicated lines mentioning "No code changes"
around the second occurrence) so the document no longer references the
release-process migration in this PR.
In
`@stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt`:
- Around line 34-38: The test helper in UserRobotChannelListAsserts uses a
nullable Boolean parameter (fromCurrentUser: Boolean?) to encode three modes and
lacks KDoc; create a new enum (e.g., PreviewAuthorPrefix with values
CURRENT_USER, OTHER_USER, NONE), replace the fromCurrentUser: Boolean? parameter
with prefix: PreviewAuthorPrefix in the relevant function(s) that build
expectedPreview, update the logic that computes expectedPreview (the when block
that currently checks true/false/null) to switch on PreviewAuthorPrefix, add
KDoc to the public helper explaining the enum and each mode, and update all call
sites as suggested (true → CURRENT_USER, false → OTHER_USER, omit → NONE).
In
`@stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt`:
- Around line 50-73: The animateItem() modifier is applied to ChannelItem (so
animation is ignored) but must be applied to the direct lazy child; move
animateItem() into the SwipeableChannelItem modifier parameter and keep only the
pinned background on ChannelItem. Specifically, pass Modifier.animateItem() (and
any other base modifiers such as backgroundCoreApp) into SwipeableChannelItem’s
modifier argument (SwipeableChannelItem(... modifier = Modifier.animateItem()
...)), and update ChannelItem to receive itemModifier that only sets the pinned
background when channelItem.channel.isPinned() without calling animateItem();
apply the same change in ChannelListItemContent to ensure animations work when
swipeEnabled is true.
In `@stream-chat-android-compose/api/stream-chat-android-compose.api`:
- Around line 4049-4059: The change to
MessagePreviewFormatter.formatMessagePreview added a new parameter
isDirectMessaging, which breaks external implementations; restore backward
compatibility by adding an overload with the previous signature
(formatMessagePreview(Message, User): AnnotatedString) that delegates to the new
signature (formatMessagePreview(Message, User, Boolean)) using the same default
boolean you intend (match the default used in formatMessagePreview$default), so
existing custom formatters continue to work; add this overload either as a
concrete default implementation in MessagePreviewFormatter (or in
MessagePreviewFormatter.DefaultImpls) and ensure Companion.defaultFormatter and
callers still resolve to the new three-arg method.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt`:
- Around line 178-191: The swipeActionHandler currently hard-codes calls to
viewModel methods for specific ChannelAction subclasses (MuteChannel,
UnmuteChannel, PinChannel, UnpinChannel, ArchiveChannel, UnarchiveChannel),
which bypasses any custom behavior implemented in ChannelAction.onAction; change
the handler to invoke action.onAction(context) (or the action's onAction API)
for all actions, delegating to the action itself, and only fall back to
viewModel.executeOrConfirm(action) if the action does not implement/onAction
returns a sentinel; keep the existing scope.launch { swipeCoordinator.closeAll()
} and ensure the handler still closes the swipe before delegating so custom
onAction logic runs while maintaining current cleanup.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelListLoadingItem.kt`:
- Around line 58-66: The code uses Modifier.weight(...) inside
ChannelListLoadingItem (and the Row/ShimmerProgressIndicator blocks) but is
missing the androidx.compose.foundation.layout.weight import; add an import for
androidx.compose.foundation.layout.weight to the top of the file so the
Modifier.weight calls compile, then rebuild to verify compilation.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt`:
- Around line 74-76: The function SwipeableChannelItem uses the experimental
AnchoredDraggableState (constructed with initialValue = SwipeRevealValue.Closed)
without opt-in; add the explicit opt-in annotation
`@OptIn`(ExperimentalFoundationApi::class) to the SwipeableChannelItem declaration
so the use of AnchoredDraggableState is properly opted-in (follow the same
pattern used in FileAttachmentContent.kt/GiphyAttachmentContent.kt).
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionItem.kt`:
- Line 38: Update the KDoc for the label parameter on both SwipeActionItem
overloads: replace the inaccurate "The label text below the icon" description
with a statement that it is an accessibility label used as the Icon's
contentDescription (i.e., an accessibility/content description string), since
the label is not rendered as visible Text but passed to the Icon's
contentDescription. Reference the label parameter in the SwipeActionItem
functions and mention its use with the Icon composable (contentDescription) so
reviewers can verify the change.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt`:
- Around line 54-59: The code iterates the mutable registry while calling the
suspending state.animateTo(...) in onItemOpened (and the similar onItemClosed
block), which can be mutated by unregister() during suspension and cause
ConcurrentModificationException; fix this by iterating over a snapshot of the
registry (e.g., registry.toList() or registry.values.toList()) and then calling
animateTo on those snapshot entries so mutations to registry during suspension
don't affect the iterator; update the iteration in onItemOpened and the
corresponding onItemClosed (and any other places that call state.animateTo while
iterating registry) to use the snapshot before invoking animateTo.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt`:
- Around line 168-174: IsErrorIcon currently passes null for contentDescription
which breaks accessibility; update the IsErrorIcon composable to use a non-null
localized string (e.g.
R.string.stream_ui_message_list_semantics_message_status_error) as the
contentDescription and add the matching string entry to strings.xml to mirror
the pattern used by IsPendingIcon/IsSentIcon/IsDeliveredIcon so TalkBack/Switch
Access announce the error state.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt`:
- Around line 51-52: The magic numeric literal 50 used to create the pill shape
should be replaced with a named constant to remove the undocumented `@Suppress`
and improve readability: introduce a private constant (e.g., PILL_CORNER_PERCENT
= 50) near the top of UnreadCountIndicator.kt and use
RoundedCornerShape(PILL_CORNER_PERCENT) in place of the literal so the
`@Suppress`("MagicNumber") can be removed and the shape variable remains
RoundedCornerShape(…) for the pill styling.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt`:
- Around line 419-431: The animated placement modifier animateItem() is applied
only to the inner ChannelItem, causing the swipe background/actions in
SwipeableChannelItem to remain unsynchronized; move the Modifier.animateItem()
to the outer composable by applying it to SwipeableChannelItem (in the branch
where swipeEnabled is true) instead of inside ChannelItem so the whole swipe
container (including ChannelSwipeActions/background) participates in placement
animations while keeping ChannelItem’s existing modifiers and parameters
unchanged.
In `@stream-chat-android-compose/src/main/res/values/strings.xml`:
- Around line 323-324: The two string resources
stream_compose_poll_created_preview and stream_compose_poll_closed_preview are
marked translatable="false" but are user-facing; remove the translatable="false"
attribute (or set it to true) so these resources become localizable and ensure
corresponding translations are added to the appropriate
values-<locale>/strings.xml files; update any documentation/comments referencing
these resource names if necessary to indicate they require translation.
In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt`:
- Around line 57-70: The test muting snapshot uses snapshot { } instead of the
dark-mode variant; update the test function muting snapshot by replacing
snapshot { } with snapshotWithDarkMode { } (the test function `fun \`muted
channel trailing bottom\`()` that renders `ChannelItemMutedTrailingBottom()`
inside `ChatTheme` with `ChatConfig(channelList =
ChannelListConfig(muteIndicatorPosition =
MuteIndicatorPosition.TRAILING_BOTTOM))`) so the component is validated in both
light and dark themes.
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt`:
- Around line 24-35: Update the KDoc for the ChannelAction
sealed/interface/class and its public properties (e.g., ChannelAction, onAction,
confirmationPopup, requiredCapability, isDestructive, icon, label) to include
explicit thread and state expectations: state whether instances are immutable
after creation, which thread or dispatcher onAction will be invoked on (UI/main
thread, IO, or caller thread), whether callers must marshal to a specific thread
to call onAction, and any lifecycle/ownership notes for confirmationPopup and
channel references; add similar KDoc sentences to each public concrete action
class/block so every public API in this file documents threading and state
guarantees clearly.
In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelActionsDialogFragment.kt`:
- Around line 165-168: channelOptionTextView.setStartDrawable currently
force-unwraps requireContext().getDrawableCompat(action.icon)!! which can NPE
for custom ChannelAction icons; change this to handle null safely by obtaining
the drawable from requireContext().getDrawableCompat(action.icon) and if null
use a safe fallback drawable or call requireNotNull(...) with a descriptive
message referencing the ChannelAction icon (e.g., include action.id or
action.label) before passing to setStartDrawable; update
ChannelActionsDialogFragment where channelOptionTextView and setStartDrawable
are used to avoid the !! and ensure a non-null Drawable is always provided.
---
Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt`:
- Around line 74-92: Update the KDoc for the public MessageReadStatusIcon
composable to document the newly-handled FAILED_PERMANENTLY state: mention that
the composable now renders an error/failed state when message.syncStatus ==
SyncStatus.FAILED_PERMANENTLY (or equivalent), describe which icon/composable
(e.g., isPendingIcon/isSentIcon/isDeliveredIcon or a new failure representation)
will be shown in that case, and add an `@param` entry or sentence explaining when
and why the failed state is displayed so callers understand the full state
machine.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt`:
- Around line 97-101: The clear button and placeholder visibility are currently
driven by the external `query` prop which lags the local input buffer, causing
flicker; change the visibility checks to use the local compose state
(`textState`) instead—e.g., in the `trailingContent` assignment (where
`isFocused && query.isNotEmpty()` is used) and the placeholder visibility logic
(lines referenced around 163-164) use `isFocused && textState.text.isNotEmpty()`
or `textState.value.text.isNotEmpty()` depending on your TextFieldValue type so
the UI reacts immediately to local edits before the parent `query` updates.
---
Duplicate comments:
In @.planning/ROADMAP.md:
- Around line 1-130: Remove the stray "[duplicate_comment]" marker from the
review comment content associated with "Roadmap: Release Process Migration" so
the comment reads only the intended sentence ("No additional issues in this file
beyond the scope concern already noted.")—locate the review text containing the
duplicate token and delete it, leaving the comment clean and unambiguous.
---
Nitpick comments:
In @.planning/PROJECT.md:
- Around line 44-49: Update the Constraints section to explicitly state that PRs
must target the develop branch and add a coordination note that any changes to
versioning, publishing, changelog or release-process artifacts (e.g.,
CHANGELOG.md, release scripts) require prior sync with the release owner;
reference the "Constraints" heading and the file name CHANGELOG.md so reviewers
can find and enforce the rule.
In @.planning/STATE.md:
- Around line 5-9: Update the time-sensitive block by adding explicit ownership
and a refresh cadence so status remains fresh: add fields such as "Owner:
<team/person>" and "Refresh cadence: <frequency, e.g.
weekly/monthly/after-change>" alongside the existing "Phase", "Plan", "Status",
and "Last activity" entries, and include a short sentence describing who is
responsible for updates and when to update (e.g., "Owner will review and refresh
this status every X days or after significant changes").
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt`:
- Line 214: The suppression of LongMethod should be removed and
DefaultChannelItemCenterContent refactored into smaller helper composables:
extract the title row rendering into a function like renderChannelTitleRow, the
preview/last-message row into renderChannelPreviewRow, and status/mute and
unread badge logic into renderChannelStatusOrMute; update
DefaultChannelItemCenterContent to compose these helpers, keep parameter lists
minimal and pass only necessary state (channel, lastMessagePreview, isMuted,
isOnline, unreadCount), and add explicit `@OptIn` annotations if any experimental
APIs are used instead of suppressing LongMethod.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt`:
- Around line 164-170: The current selection logic in DefaultChannelSwipeActions
builds candidates = listOf(...) containing archiveAction, muteAction, pinAction
and then returns candidates.firstOrNull { it != null }, but since pinAction (and
archiveAction/pinAction) are non-null the function can never return null; update
the implementation to reflect that non-null guarantee: either change the
function return type to non-nullable and return
candidates.filterNotNull().first() (or .first { true } after filtering) or, if
null is genuinely possible, adjust the candidates construction so some entries
can be null and use filterNotNull().firstOrNull(); reference the candidates
variable and the archiveAction/muteAction/pinAction symbols when making the
change.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt`:
- Line 93: The horizontalArrangement uses
Arrangement.spacedBy(StreamTokens.spacingMd) which adds 16dp between all
children while the center content also has start = StreamTokens.spacing2xs
(4dp), producing a 20dp gap; to match the Figma 16dp edge-to-edge spacing remove
the center content's start padding (the padding on the composable that sets
start = StreamTokens.spacing2xs) or alternatively remove the
Arrangement.spacedBy and apply per-child paddings so only one 16dp gap exists
between the avatar and text; update the code references:
horizontalArrangement/Arrangement.spacedBy(StreamTokens.spacingMd) and the
center content padding (start = StreamTokens.spacing2xs) accordingly.
- Around line 207-213: The explicit textStyle override on the Timestamp
composable is redundant because Timestamp already defaults to
ChatTheme.typography.captionDefault.copy(color = ChatTheme.colors.textTertiary,
lineHeight = StreamTokens.lineHeightNormal); remove the textStyle parameter from
the Timestamp call (the one using searchResultItemState.message.createdAt) so it
relies on the default style provided by Timestamp/ChatTheme.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt`:
- Around line 21-36: The public enum SwipeActionStyle lacks KDoc about
thread/state expectations; update the KDoc for SwipeActionStyle to add a short
thread/state note stating that this enum is a stateless, immutable style marker
and safe to reference from any thread (no synchronization required), and include
that it carries no mutable state and is safe for concurrent use in UI code such
as composables and background threads.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt`:
- Around line 26-30: Update the KDoc for the public class SwipeRevealCoordinator
to include thread-confinement and state guarantees: explicitly state that all
public methods that mutate shared coordinator state must be called from the
main/UI thread (or specify the required dispatcher), describe whether calls are
reentrant or synchronized, and note any atomicity/visibility guarantees clients
can rely on; also add the same thread/state note to each public method in the
class so callers see the requirement at the call site.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt`:
- Line 95: The suppression on buildDefaultChannelActions is too broad and
undocumented; remove the blanket `@Suppress`("LongMethod", "LongParameterList")
and either refactor buildDefaultChannelActions into smaller helper functions
(e.g., extract action creation into createChannelActionList, buildActionItem,
configureActionHandlers) to reduce method length and parameter count, or if
suppression is unavoidable, replace it with a narrow, documented suppression
scoped to the specific offending expression(s) and add a one-line rationale
comment above it; also ensure any experimental APIs used are annotated with
`@OptIn` rather than suppressed.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt`:
- Around line 83-110: The MessageReadStatusIcon API hard-codes IsErrorIcon for
SyncStatus.FAILED_PERMANENTLY and should expose an error slot for parity; add a
new parameter isErrorIcon: `@Composable` () -> Unit to MessageReadStatusIcon
(defaulting to { IsErrorIcon(modifier = modifier) }) and call that isErrorIcon()
in the FAILED_PERMANENTLY branch instead of directly invoking IsErrorIcon, so
integrators can override the error state rendering.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt`:
- Around line 36-47: Update the KDoc for the composable function
UnreadCountIndicator to include the missing `@param` entry for color, describing
that it controls the badge tint and noting the default value
(ChatTheme.colors.accentPrimary) and its intent (to match the app's accent for
unread badges); keep other parameter docs intact and ensure the KDoc reflects
the changed default from the previous accentError to accentPrimary so callers
understand the behavioral change.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt`:
- Line 74: Remove the undocumented `@Suppress`("LongMethod") on SearchInput:
instead split the large function by extracting the state synchronization logic
and the TextField decoration into small helpers (e.g., create functions named
syncSearchState(...) and decorateSearchTextField(...) called from SearchInput)
so the body is shorter and the lint rule is satisfied; if any experimental APIs
are required, replace suppressions with explicit `@OptIn`(...) annotations on
SearchInput or the new helper functions and document the reason in a brief KDoc.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt`:
- Around line 63-67: Add KDoc for the public function formatMessagePreview and
its new parameter isDirectMessaging: document what the function returns,
expected thread/state assumptions, and explain the boolean’s effect (e.g., how
message preview differs in direct messaging vs channels). Remove the
undocumented `@Suppress`("LongMethod") (or if you must keep it, add a KDoc
justification comment explaining why the method legitimately exceeds length and
why splitting isn't appropriate). Ensure the KDoc appears above the
formatMessagePreview declaration and explicitly describes isDirectMessaging
behavior and any thread/state constraints.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt`:
- Around line 67-132: Remove the undocumented `@Suppress`("LongMethod") on
MessagePreviewIconFactory.createPreviewIcons and refactor duplicated
InlineTextContent + Icon creation into a small private helper (e.g.,
buildPreviewIcon(drawableRes: Int): InlineTextContent) that captures the
Placeholder and Icon modifier; then replace each anonymous InlineTextContent
block (for VOICE_MESSAGE, PHOTO, VIDEO, FILE, LINK, LOCATION, POLL) with calls
to that helper, keeping Placeholder and iconModifier shared and eliminating the
long-method suppression.
In
`@stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml`:
- Around line 24-29: Replace the hardcoded semantic gray in the search drawable
with the project's placeholder color: open stream_compose_ic_search.xml and
change the android:strokeColor attribute (and any other explicit semantic colors
in this drawable) from "#687385" to the neutral placeholder (e.g. "#FFFFFF") so
the Compose layer can apply tint programmatically; ensure android:fillColor
remains transparent and strokeWidth/strokeLineCap/strokeLineJoin are unchanged.
In `@stream-chat-android-compose/src/main/res/values/strings.xml`:
- Around line 31-34: The plural resource stream_compose_channel_list_typing_many
uses unpositioned numeric placeholders ("%d"); update both <item> entries to use
positional placeholders ("%1$d") so translators can reorder safely and maintain
consistent formatting; search for other plural resources (e.g., the similar
block referenced at lines 182-193) and apply the same replacement to their
numeric placeholders as well to ensure all plural strings use "%1$d".
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt`:
- Around line 63-73: LeaveGroup and DeleteConversation currently have
confirmationPopup defaulting to null making destructive actions prone to
accidental immediate execution; update their constructors (classes LeaveGroup
and DeleteConversation) so confirmationPopup defaults to a safe non-null
ConfirmationPopup (e.g., a standard destructive confirmation with title, message
and confirm/cancel labels) instead of null, and ensure callers can still
override it by providing a custom ConfirmationPopup via the confirmationPopup
parameter declared on the ChannelAction implementations.
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt`:
- Around line 19-30: Update the KDoc for the public data class ConfirmationPopup
to include thread and state expectations: state that this is an immutable value
object (data class) safe to pass across threads, its properties (title, message,
confirmButtonText) are resolved UI text and should not hold mutable or
lifecycle-bound references, and note any expectations about resolution timing
(e.g., already localized/resolved before construction) so callers know it's safe
for background/worker threads and UI usage; reference the ConfirmationPopup type
and its properties when adding these notes.
|
|
||
| ## What This Is | ||
|
|
||
| Migrating stream-chat-android's release process to match stream-video-android and stream-core-android. Currently chat uses manual CHANGELOG.md updates; target is label-based automatic release notes via GitHub. |
There was a problem hiding this comment.
Scope mismatch with this PR’s stated objective.
Line 5 and Line 47 describe a release-process migration with “No code changes,” which conflicts with this PR’s channel-list redesign scope. Please move this planning track to a dedicated PR (or update this document to match the actual PR intent) to avoid audit/review confusion.
Also applies to: 47-48
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.planning/PROJECT.md at line 5, The planning note "Migrating
stream-chat-android's release process to match stream-video-android and
stream-core-android" (the line referencing "No code changes") is out of scope
for this channel-list redesign PR; either remove or relocate that planning track
to its own PR and update .planning/PROJECT.md so its content matches the
channel-list redesign intent, specifically edit or remove the sentence at the
paragraph containing "Migrating stream-chat-android's release process..." (and
the duplicated lines mentioning "No code changes" around the second occurrence)
so the document no longer references the release-process migration in this PR.
| val expectedPreview = when (fromCurrentUser) { | ||
| true -> "You: $text" | ||
| false -> "${ParticipantRobot.name}: $text" | ||
| null -> text | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the actual function definition to see current state and any existing KDoc
fd -t f "UserRobotChannelListAsserts.kt" --exec cat -n {}Repository: GetStream/stream-chat-android
Length of output: 2320
🏁 Script executed:
# Find all usages of assertMessageInChannelPreview
rg -n --type=kt 'assertMessageInChannelPreview' -B2 -A2Repository: GetStream/stream-chat-android
Length of output: 99
🏁 Script executed:
# Find all usages of assertMessageInChannelPreview without the problematic --type flag
rg -n 'assertMessageInChannelPreview' -B1 -A1Repository: GetStream/stream-chat-android
Length of output: 10193
Add KDoc documentation and replace nullable-Boolean state encoding with explicit enum.
This is a public test helper (imported and used across multiple test files) that currently lacks required KDoc. The fromCurrentUser: Boolean? parameter encodes three distinct assertion modes (CURRENT_USER, OTHER_USER, NONE), which are all actively used but unclear without documentation. Replace with an explicit enum to clarify state intent and add KDoc per coding guidelines.
Suggested refactor
+enum class PreviewAuthorPrefix { CURRENT_USER, OTHER_USER, NONE }
+
+/**
+ * Asserts the channel preview text with optional author prefix mode.
+ * `@param` text Expected message text
+ * `@param` prefix Author prefix mode: CURRENT_USER ("You: "), OTHER_USER ("name: "), or NONE (no prefix)
+ */
-fun UserRobot.assertMessageInChannelPreview(text: String, fromCurrentUser: Boolean? = null): UserRobot {
- val expectedPreview = when (fromCurrentUser) {
- true -> "You: $text"
- false -> "${ParticipantRobot.name}: $text"
- null -> text
+fun UserRobot.assertMessageInChannelPreview(
+ text: String,
+ prefix: PreviewAuthorPrefix = PreviewAuthorPrefix.NONE,
+): UserRobot {
+ val expectedPreview = when (prefix) {
+ PreviewAuthorPrefix.CURRENT_USER -> "You: $text"
+ PreviewAuthorPrefix.OTHER_USER -> "${ParticipantRobot.name}: $text"
+ PreviewAuthorPrefix.NONE -> text
}
assertEquals(expectedPreview, Channel.messagePreview.waitToAppear().text.trimEnd())
return this
}Update call sites: fromCurrentUser = true → prefix = PreviewAuthorPrefix.CURRENT_USER, fromCurrentUser = false → prefix = PreviewAuthorPrefix.OTHER_USER, no parameter → prefix = PreviewAuthorPrefix.NONE.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt`
around lines 34 - 38, The test helper in UserRobotChannelListAsserts uses a
nullable Boolean parameter (fromCurrentUser: Boolean?) to encode three modes and
lacks KDoc; create a new enum (e.g., PreviewAuthorPrefix with values
CURRENT_USER, OTHER_USER, NONE), replace the fromCurrentUser: Boolean? parameter
with prefix: PreviewAuthorPrefix in the relevant function(s) that build
expectedPreview, update the logic that computes expectedPreview (the when block
that currently checks true/false/null) to switch on PreviewAuthorPrefix, add
KDoc to the public helper explaining the enum and each mode, and update all call
sites as suggested (true → CURRENT_USER, false → OTHER_USER, omit → NONE).
| val itemModifier = Modifier | ||
| .animateItem() | ||
| .run { | ||
| if (channelItem.channel.isPinned()) { | ||
| background(color = ChatTheme.colors.backgroundCoreHighlight) | ||
| } else { | ||
| this | ||
| } | ||
| } | ||
|
|
||
| if (swipeEnabled) { | ||
| SwipeableChannelItem( | ||
| channelCid = channelItem.channel.cid, | ||
| backgroundColor = ChatTheme.colors.backgroundCoreApp, | ||
| swipeActions = { Row { ChannelSwipeActions(channelItem) } }, | ||
| ) { | ||
| ChannelItem( | ||
| modifier = itemModifier, | ||
| channelItem = channelItem, | ||
| currentUser = currentUser, | ||
| onChannelClick = onChannelClick, | ||
| onChannelLongClick = onChannelLongClick, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the SwipeableChannelItem definition to understand its signature
fd -t f "SwipeableChannelItem" --type ktRepository: GetStream/stream-chat-android
Length of output: 242
🏁 Script executed:
# Let's also check the actual file mentioned in the review
fd -t f "CustomChatComponentFactory.kt"Repository: GetStream/stream-chat-android
Length of output: 346
🏁 Script executed:
# Search for SwipeableChannelItem usage and definition
rg "SwipeableChannelItem" --type kt -A 3Repository: GetStream/stream-chat-android
Length of output: 99
🏁 Script executed:
# Look for animateItem() usage patterns in the codebase
rg "animateItem()" --type kt -B 2 -A 2Repository: GetStream/stream-chat-android
Length of output: 99
🏁 Script executed:
# Check the ChatComponentFactory for the same swipe pattern
fd -t f "ChatComponentFactory.kt" | head -20Repository: GetStream/stream-chat-android
Length of output: 455
🏁 Script executed:
# Search for SwipeableChannelItem definition and usage
rg "fun SwipeableChannelItem" -A 10Repository: GetStream/stream-chat-android
Length of output: 3327
🏁 Script executed:
# Search for SwipeableChannelItem composable signature
rg "SwipeableChannelItem\(" -B 2 -A 3Repository: GetStream/stream-chat-android
Length of output: 2897
🏁 Script executed:
# Look for animateItem modifier usage
rg "animateItem\(\)" -B 3 -A 3Repository: GetStream/stream-chat-android
Length of output: 13940
🏁 Script executed:
# Check the actual CustomChatComponentFactory file content
cat -n stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt | head -100Repository: GetStream/stream-chat-android
Length of output: 4193
🏁 Script executed:
# Get the full SwipeableChannelItem implementation
cat -n stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.ktRepository: GetStream/stream-chat-android
Length of output: 6433
🏁 Script executed:
# Get the ChatComponentFactory implementation around the SwipeableChannelItem usage
sed -n '1,150p' stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt | tail -100Repository: GetStream/stream-chat-android
Length of output: 8000
🏁 Script executed:
# Check ChannelItem composable to understand how it uses the modifier
rg "fun ChannelItem" -A 20Repository: GetStream/stream-chat-android
Length of output: 50385
🏁 Script executed:
# Check how ChannelListItemContent is used in the lazy list (context around line 60-80)
grep -r "ChannelListItemContent" --include="*.kt" -B 5 -A 5 | head -100Repository: GetStream/stream-chat-android
Length of output: 10005
🏁 Script executed:
# Look for LazyItemScope usage to understand the context better
rg "LazyItemScope\.ChannelListItemContent" -B 10 -A 10Repository: GetStream/stream-chat-android
Length of output: 9754
🏁 Script executed:
# Check the full implementation of ChannelItem to see where modifier is applied
sed -n '1,200p' stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.ktRepository: GetStream/stream-chat-android
Length of output: 8338
🏁 Script executed:
# Get the default ChatComponentFactory ChannelListItemContent implementation
sed -n '1,2000p' stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt | grep -A 50 "fun LazyItemScope.ChannelListItemContent"Repository: GetStream/stream-chat-android
Length of output: 1972
🏁 Script executed:
# Verify the exact signature of SwipeableChannelItem to confirm modifier parameter accepts Modifier
grep -A 10 "public fun SwipeableChannelItem" stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt | head -15Repository: GetStream/stream-chat-android
Length of output: 451
🏁 Script executed:
# Check how animateItem() is documented/defined
rg "fun.*animateItem" -A 5 --maxcount=3Repository: GetStream/stream-chat-android
Length of output: 512
🏁 Script executed:
# Confirm animateItem() is a LazyItemScope extension and understand its purpose
rg "animateItem" --max-count=5 -B 2 -A 2 | head -40Repository: GetStream/stream-chat-android
Length of output: 5128
Move animateItem() to SwipeableChannelItem modifier; keep pinned background on ChannelItem.
Modifier.animateItem() is a LazyItemScope extension that only works when applied to the direct child of a lazy list item. In the swipe-enabled path, SwipeableChannelItem is the direct child, but animateItem() is currently on the nested ChannelItem, where the animation will be silently ignored.
The same issue exists in the default ChatComponentFactory.ChannelListItemContent. Pass Modifier.animateItem() to SwipeableChannelItem's modifier parameter and apply only the pinned background to ChannelItem:
Proposed fix
if (swipeEnabled) {
+ val pinnedModifier = if (channelItem.channel.isPinned()) {
+ Modifier.background(color = ChatTheme.colors.backgroundCoreHighlight)
+ } else {
+ Modifier
+ }
SwipeableChannelItem(
channelCid = channelItem.channel.cid,
+ modifier = Modifier.animateItem(),
backgroundColor = ChatTheme.colors.backgroundCoreApp,
swipeActions = { Row { ChannelSwipeActions(channelItem) } },
) {
ChannelItem(
- modifier = itemModifier,
+ modifier = pinnedModifier,
channelItem = channelItem,
currentUser = currentUser,
onChannelClick = onChannelClick,
onChannelLongClick = onChannelLongClick,
)
}
} else {
+ val itemModifier = Modifier
+ .animateItem()
+ .run {
+ if (channelItem.channel.isPinned()) {
+ background(color = ChatTheme.colors.backgroundCoreHighlight)
+ } else {
+ this
+ }
+ }
ChannelItem(
modifier = itemModifier,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt`
around lines 50 - 73, The animateItem() modifier is applied to ChannelItem (so
animation is ignored) but must be applied to the direct lazy child; move
animateItem() into the SwipeableChannelItem modifier parameter and keep only the
pinned background on ChannelItem. Specifically, pass Modifier.animateItem() (and
any other base modifiers such as backgroundCoreApp) into SwipeableChannelItem’s
modifier argument (SwipeableChannelItem(... modifier = Modifier.animateItem()
...)), and update ChannelItem to receive itemModifier that only sets the pinned
background when channelItem.channel.isPinned() without calling animateItem();
apply the same change in ChannelListItemContent to ensure animations work when
swipeEnabled is true.
| 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; | ||
| } | ||
|
|
||
| public final class io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter$Companion { | ||
| public final fun defaultFormatter (Landroid/content/Context;ZLio/getstream/chat/android/compose/ui/theme/StreamDesign$Typography;Ljava/util/List;Lio/getstream/chat/android/compose/ui/theme/StreamDesign$Colors;)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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for MessagePreviewFormatter in the source code
find . -type f -name "*.kt" | xargs grep -l "MessagePreviewFormatter" | head -20Repository: GetStream/stream-chat-android
Length of output: 694
🏁 Script executed:
# Let's also check the directory structure to understand the project layout
ls -la stream-chat-android-compose/ | head -20Repository: GetStream/stream-chat-android
Length of output: 812
🏁 Script executed:
# Find the exact file containing MessagePreviewFormatter
find . -type f -name "*MessagePreviewFormatter*"Repository: GetStream/stream-chat-android
Length of output: 300
🏁 Script executed:
# Read the actual MessagePreviewFormatter implementation
cat -n ./stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.ktRepository: GetStream/stream-chat-android
Length of output: 21141
🏁 Script executed:
# Check the API file around the lines mentioned
sed -n '4049,4059p' stream-chat-android-compose/api/stream-chat-android-compose.apiRepository: GetStream/stream-chat-android
Length of output: 1123
🏁 Script executed:
# Check git history to understand when this change was made
git log --oneline -20 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt 2>/dev/null | head -10Repository: GetStream/stream-chat-android
Length of output: 136
🏁 Script executed:
# Search for any migration guides or deprecation notices in the codebase
find . -type f \( -name "CHANGELOG*" -o -name "MIGRATION*" -o -name "BREAKING*" \) | head -10Repository: GetStream/stream-chat-android
Length of output: 108
🏁 Script executed:
# Check if there are any deprecation annotations on the old method
grep -r "@Deprecated" stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.ktRepository: GetStream/stream-chat-android
Length of output: 55
🏁 Script executed:
# Check the CHANGELOG for any mentions of MessagePreviewFormatter
grep -A 5 -B 5 "MessagePreviewFormatter\|isDirectMessage" ./CHANGELOG.md | head -50Repository: GetStream/stream-chat-android
Length of output: 4753
🏁 Script executed:
# Also check if there are any existing tests or usages that show how external implementations would be affected
grep -r "class.*MessagePreviewFormatter" --include="*.kt" | grep -v "DefaultMessagePreviewFormatter\|// "Repository: GetStream/stream-chat-android
Length of output: 55
🏁 Script executed:
# Check the commit details to understand the context of the change
git show 054f8ad4:stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt 2>/dev/null | grep -A 10 "formatMessagePreview" | head -20Repository: GetStream/stream-chat-android
Length of output: 813
MessagePreviewFormatter.formatMessagePreview() signature change breaks existing custom formatters.
The interface method now requires a third parameter isDirectMessaging: Boolean (line 4049). While callers can omit this parameter due to its default value, implementations of this interface must be updated to include the new parameter—this breaks any external formatter implementations.
💡 Backward-compatible overload pattern
interface MessagePreviewFormatter {
+ `@Deprecated`("Use overload with isDirectMessaging parameter")
+ fun formatMessagePreview(
+ message: Message,
+ currentUser: User?,
+ ): AnnotatedString = formatMessagePreview(
+ message = message,
+ currentUser = currentUser,
+ isDirectMessaging = false,
+ )
+
fun formatMessagePreview(
message: Message,
currentUser: User?,
isDirectMessaging: Boolean,
): AnnotatedString
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@stream-chat-android-compose/api/stream-chat-android-compose.api` around lines
4049 - 4059, The change to MessagePreviewFormatter.formatMessagePreview added a
new parameter isDirectMessaging, which breaks external implementations; restore
backward compatibility by adding an overload with the previous signature
(formatMessagePreview(Message, User): AnnotatedString) that delegates to the new
signature (formatMessagePreview(Message, User, Boolean)) using the same default
boolean you intend (match the default used in formatMessagePreview$default), so
existing custom formatters continue to work; add this overload either as a
concrete default implementation in MessagePreviewFormatter (or in
MessagePreviewFormatter.DefaultImpls) and ensure Companion.defaultFormatter and
callers still resolve to the new three-arg method.
| val swipeActionHandler: (ChannelAction) -> Unit = remember(viewModel) { | ||
| { | ||
| action -> | ||
| scope.launch { swipeCoordinator.closeAll() } | ||
| when (action) { | ||
| is MuteChannel -> viewModel.muteChannel(action.channel) | ||
| is UnmuteChannel -> viewModel.unmuteChannel(action.channel) | ||
| is PinChannel -> viewModel.pinChannel(action.channel) | ||
| is UnpinChannel -> viewModel.unpinChannel(action.channel) | ||
| is ArchiveChannel -> viewModel.archiveChannel(action.channel) | ||
| is UnarchiveChannel -> viewModel.unarchiveChannel(action.channel) | ||
| else -> viewModel.executeOrConfirm(action) | ||
| } | ||
| } |
There was a problem hiding this comment.
Swipe dispatch bypasses ChannelAction.onAction for built-ins.
Line 183–Line 188 hard-code ViewModel calls for known action types. Any custom behavior injected into PinChannel/MuteChannel/etc. onAction will never run.
💡 Suggested fix
- val swipeActionHandler: (ChannelAction) -> Unit = remember(viewModel) {
- {
- action ->
- scope.launch { swipeCoordinator.closeAll() }
- when (action) {
- is MuteChannel -> viewModel.muteChannel(action.channel)
- is UnmuteChannel -> viewModel.unmuteChannel(action.channel)
- is PinChannel -> viewModel.pinChannel(action.channel)
- is UnpinChannel -> viewModel.unpinChannel(action.channel)
- is ArchiveChannel -> viewModel.archiveChannel(action.channel)
- is UnarchiveChannel -> viewModel.unarchiveChannel(action.channel)
- else -> viewModel.executeOrConfirm(action)
- }
- }
- }
+ val swipeActionHandler: (ChannelAction) -> Unit = remember(viewModel, swipeCoordinator, scope) {
+ { action ->
+ scope.launch {
+ swipeCoordinator.closeAll()
+ viewModel.executeOrConfirm(action)
+ }
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val swipeActionHandler: (ChannelAction) -> Unit = remember(viewModel) { | |
| { | |
| action -> | |
| scope.launch { swipeCoordinator.closeAll() } | |
| when (action) { | |
| is MuteChannel -> viewModel.muteChannel(action.channel) | |
| is UnmuteChannel -> viewModel.unmuteChannel(action.channel) | |
| is PinChannel -> viewModel.pinChannel(action.channel) | |
| is UnpinChannel -> viewModel.unpinChannel(action.channel) | |
| is ArchiveChannel -> viewModel.archiveChannel(action.channel) | |
| is UnarchiveChannel -> viewModel.unarchiveChannel(action.channel) | |
| else -> viewModel.executeOrConfirm(action) | |
| } | |
| } | |
| val swipeActionHandler: (ChannelAction) -> Unit = remember(viewModel, swipeCoordinator, scope) { | |
| { action -> | |
| scope.launch { | |
| swipeCoordinator.closeAll() | |
| viewModel.executeOrConfirm(action) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt`
around lines 178 - 191, The swipeActionHandler currently hard-codes calls to
viewModel methods for specific ChannelAction subclasses (MuteChannel,
UnmuteChannel, PinChannel, UnpinChannel, ArchiveChannel, UnarchiveChannel),
which bypasses any custom behavior implemented in ChannelAction.onAction; change
the handler to invoke action.onAction(context) (or the action's onAction API)
for all actions, delegating to the action itself, and only fall back to
viewModel.executeOrConfirm(action) if the action does not implement/onAction
returns a sentinel; keep the existing scope.launch { swipeCoordinator.closeAll()
} and ensure the handler still closes the swipe before delegating so custom
onAction logic runs while maintaining current cleanup.
| if (swipeEnabled) { | ||
| SwipeableChannelItem( | ||
| channelCid = channelItem.channel.cid, | ||
| backgroundColor = ChatTheme.colors.backgroundCoreApp, | ||
| swipeActions = { Row { ChannelSwipeActions(channelItem) } }, | ||
| ) { | ||
| ChannelItem( | ||
| modifier = Modifier.animateItem(), | ||
| channelItem = channelItem, | ||
| currentUser = currentUser, | ||
| onChannelClick = onChannelClick, | ||
| onChannelLongClick = onChannelLongClick, | ||
| ) |
There was a problem hiding this comment.
Animate the swipe container, not only the foreground ChannelItem.
In the swipe-enabled branch, animateItem() is attached to the inner content. This can leave swipe background/actions out of sync during list item placement animations.
🎯 Proposed fix
if (swipeEnabled) {
SwipeableChannelItem(
+ modifier = Modifier.animateItem(),
channelCid = channelItem.channel.cid,
backgroundColor = ChatTheme.colors.backgroundCoreApp,
swipeActions = { Row { ChannelSwipeActions(channelItem) } },
) {
ChannelItem(
- modifier = Modifier.animateItem(),
+ modifier = Modifier,
channelItem = channelItem,
currentUser = currentUser,
onChannelClick = onChannelClick,
onChannelLongClick = onChannelLongClick,
)
}
} else {Based on learnings: In Compose UI components within the stream-chat-android-compose module, always apply modifiers to the outermost composable in the hierarchy.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt`
around lines 419 - 431, The animated placement modifier animateItem() is applied
only to the inner ChannelItem, causing the swipe background/actions in
SwipeableChannelItem to remain unsynchronized; move the Modifier.animateItem()
to the outer composable by applying it to SwipeableChannelItem (in the branch
where swipeEnabled is true) instead of inside ChannelItem so the whole swipe
container (including ChannelSwipeActions/background) participates in placement
animations while keeping ChannelItem’s existing modifiers and parameters
unchanged.
| <string name="stream_compose_poll_created_preview" translatable="false">Poll created: %s</string> | ||
| <string name="stream_compose_poll_closed_preview" translatable="false">Poll closed: %s</string> |
There was a problem hiding this comment.
Poll preview strings look user-facing but are still non-translatable.
If shown in channel/message previews, keeping translatable="false" will force English text in localized apps.
🌐 Proposed localization fix
- <string name="stream_compose_poll_created_preview" translatable="false">Poll created: %s</string>
- <string name="stream_compose_poll_closed_preview" translatable="false">Poll closed: %s</string>
+ <string name="stream_compose_poll_created_preview">Poll created: %s</string>
+ <string name="stream_compose_poll_closed_preview">Poll closed: %s</string>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <string name="stream_compose_poll_created_preview" translatable="false">Poll created: %s</string> | |
| <string name="stream_compose_poll_closed_preview" translatable="false">Poll closed: %s</string> | |
| <string name="stream_compose_poll_created_preview">Poll created: %s</string> | |
| <string name="stream_compose_poll_closed_preview">Poll closed: %s</string> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@stream-chat-android-compose/src/main/res/values/strings.xml` around lines 323
- 324, The two string resources stream_compose_poll_created_preview and
stream_compose_poll_closed_preview are marked translatable="false" but are
user-facing; remove the translatable="false" attribute (or set it to true) so
these resources become localizable and ensure corresponding translations are
added to the appropriate values-<locale>/strings.xml files; update any
documentation/comments referencing these resource names if necessary to indicate
they require translation.
| @Test | ||
| fun `muted channel trailing bottom`() { | ||
| snapshot { | ||
| ChatTheme( | ||
| config = ChatConfig( | ||
| channelList = ChannelListConfig( | ||
| muteIndicatorPosition = MuteIndicatorPosition.TRAILING_BOTTOM, | ||
| ), | ||
| ), | ||
| ) { | ||
| ChannelItemMutedTrailingBottom() | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing dark mode snapshot variant.
All other tests in this class use snapshotWithDarkMode { } but this new test uses snapshot { }. Was the dark mode variant intentionally omitted? If the trailing-bottom mute indicator should look correct in both themes, consider using snapshotWithDarkMode for parity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt`
around lines 57 - 70, The test muting snapshot uses snapshot { } instead of the
dark-mode variant; update the test function muting snapshot by replacing
snapshot { } with snapshotWithDarkMode { } (the test function `fun \`muted
channel trailing bottom\`()` that renders `ChannelItemMutedTrailingBottom()`
inside `ChatTheme` with `ChatConfig(channelList =
ChannelListConfig(muteIndicatorPosition =
MuteIndicatorPosition.TRAILING_BOTTOM))`) so the component is validated in both
light and dark themes.
| /** | ||
| * Represents the list of actions users can take with selected channels. | ||
| * A self-describing channel action that carries display info, capability requirements, and an | ||
| * execution handler. Works for swipe actions, options menus, and any future action surface. | ||
| * | ||
| * @property channel The selected channel. | ||
| * @property channel The channel this action targets. | ||
| * @property icon Drawable resource for the action icon. | ||
| * @property label Human-readable label for the action. | ||
| * @property requiredCapability Optional channel capability required to show this action. | ||
| * @property confirmationPopup Optional confirmation dialog to show before executing. | ||
| * @property isDestructive Whether this action is destructive (e.g. delete). | ||
| * @property onAction The handler to execute when the action is confirmed. | ||
| */ |
There was a problem hiding this comment.
Public KDocs are missing explicit thread/state expectations.
The docs describe behavior, but they don’t state threading expectations for onAction execution or state notes for action immutability/lifecycle (for example around Line 34 and each public action class block).
As per coding guidelines, "**/*.kt: Document public APIs with KDoc, including thread expectations and state notes."
Also applies to: 48-162
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt`
around lines 24 - 35, Update the KDoc for the ChannelAction
sealed/interface/class and its public properties (e.g., ChannelAction, onAction,
confirmationPopup, requiredCapability, isDestructive, icon, label) to include
explicit thread and state expectations: state whether instances are immutable
after creation, which thread or dispatcher onAction will be invoked on (UI/main
thread, IO, or caller thread), whether callers must marshal to a specific thread
to call onAction, and any lifecycle/ownership notes for confirmationPopup and
channel references; add similar KDoc sentences to each public concrete action
class/block so every public API in this file documents threading and state
guarantees clearly.
| channelOptionTextView.text = action.label | ||
| channelOptionTextView.setStartDrawable( | ||
| requireContext().getDrawableCompat(action.icon)!!, | ||
| ) |
There was a problem hiding this comment.
Force-unwrap on getDrawableCompat could crash for custom ChannelAction implementations.
getDrawableCompat(action.icon) returns Drawable?. If a consumer provides a custom ChannelAction with an invalid icon resource, the !! will throw a NPE. Consider a safe fallback or a requireNotNull with a descriptive message.
Proposed fix
- channelOptionTextView.setStartDrawable(
- requireContext().getDrawableCompat(action.icon)!!,
- )
+ val icon = requireNotNull(requireContext().getDrawableCompat(action.icon)) {
+ "No drawable found for ChannelAction icon resource: ${action.icon}"
+ }
+ channelOptionTextView.setStartDrawable(icon)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelActionsDialogFragment.kt`
around lines 165 - 168, channelOptionTextView.setStartDrawable currently
force-unwraps requireContext().getDrawableCompat(action.icon)!! which can NPE
for custom ChannelAction icons; change this to handle null safely by obtaining
the drawable from requireContext().getDrawableCompat(action.icon) and if null
use a safe fallback drawable or call requireNotNull(...) with a descriptive
message referencing the ChannelAction icon (e.g., include action.id or
action.label) before passing to setStartDrawable; update
ChannelActionsDialogFragment where channelOptionTextView and setStartDrawable
are used to avoid the !! and ensure a non-null Drawable is always provided.


Goal
Redesign the channel list screen to match the updated Figma spec and add swipe-to-reveal actions. Covers layout, tokens, typography, icons, interaction states, bottom sheet redesign, and swipe gesture support.
Implementation
1. Channel List Item Layout
MuteIndicatorPositionenum (INLINE_TITLE/TRAILING_BOTTOM)2. Interaction States
borderCorePrimary, 2dp,radiusLg)backgroundCoreSelected)isSelectedproperty onChannelItemState, wired fromChannelListViewModel.selectedChannel3. Icons & Design System
stream_ic_action_{archive,delete,leave,more,mute,pin,unarchive,unmute,unpin,view_info}ic_add,ic_clock,ic_error,ic_more_options,ic_muted,message_seen,message_sent4. ChannelAction Refactor
ChannelActionis now a self-describing interface withicon,label,confirmationPopupChannelOptionState— actions are now the single source of truthConfirmationPopupfor destructive action confirmation5. Channel Options Bottom Sheet
StreamTokens6. Swipe-to-Reveal Actions
SwipeableChannelItemwraps channel items withAnchoredDraggableState-based swipe gestureSwipeRevealCoordinatorensures only one item is open at a timeSwipeActionStyleenum: Primary / Secondary / DestructiveChannelListConfig(swipeActionsEnabled = false)Customization via
ChatComponentFactory:ChannelSwipeActions()— replace which actions appear (keeps swipe mechanism)ChannelListItemContent()— full control over swipe wrapper + itemNew files:
SwipeableChannelItem.kt,SwipeRevealCoordinator.kt,SwipeRevealValue.kt,SwipeActionItem.kt,SwipeActionStyle.kt,DefaultChannelSwipeActions.kt7. Supporting Components
UnreadCountIndicator: removed white border, switched tonumericMedium(10sp)Timestamp, delivery status tokensSearchResultItemandSelectedChannelMenutokensInputField, updated radius and iconsize16design token and channel-list typing string resourcesTesting
muted_channel_trailing_bottomCustomChatComponentFactoryupdated to include swipe wrapperSummary by CodeRabbit
Release Notes
New Features
UI/Design Improvements
Bug Fixes