Skip to content

Redesign channel list, swipe actions and actions bottom sheet#6181

Open
aleksandar-apostolov wants to merge 58 commits intov7from
redesign/channel-list
Open

Redesign channel list, swipe actions and actions bottom sheet#6181
aleksandar-apostolov wants to merge 58 commits intov7from
redesign/channel-list

Conversation

@aleksandar-apostolov
Copy link
Contributor

@aleksandar-apostolov aleksandar-apostolov commented Feb 23, 2026

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

  • Restructured layout: avatar | name + mute + timestamp + badge | trailing padding
  • Message preview row: delivery status icon + preview text (or typing indicator)
  • Configurable mute icon position via MuteIndicatorPosition enum (INLINE_TITLE / TRAILING_BOTTOM)
  • Attachment type icons (photo, video, file, link, voice, poll, location) replace emoji in previews
  • Deleted message preview support with proper sender attribution
  • DM outgoing messages no longer show "You:" prefix (delivery icon suffices)
  • DM deleted outgoing still shows "You: Message deleted" explicitly
  • Typing indicator: DM shows "Typing", group shows proper name-based formats (1, 2, many)

2. Interaction States

  • Focus state: blue rounded border (borderCorePrimary, 2dp, radiusLg)
  • Selected state: tinted background (backgroundCoreSelected)
  • isSelected property on ChannelItemState, wired from ChannelListViewModel.selectedChannel
  • 4dp card inset so rounded shape has visible margins from list edges

3. Icons & Design System

  • New action drawables in ui-common: stream_ic_action_{archive,delete,leave,more,mute,pin,unarchive,unmute,unpin,view_info}
  • Updated Compose icons: ic_add, ic_clock, ic_error, ic_more_options, ic_muted, message_seen, message_sent

4. ChannelAction Refactor

  • ChannelAction is now a self-describing interface with icon, label, confirmationPopup
  • Each action carries its own metadata for rendering in swipe actions, bottom sheets, and dialogs
  • Removed ChannelOptionState — actions are now the single source of truth
  • Added ConfirmationPopup for destructive action confirmation

5. Channel Options Bottom Sheet

  • Redesigned header with group avatar and channel name
  • Token-based dimensions via StreamTokens
  • Removed dividers between action items
  • Aligned padding and sizing with Figma List Item spec
  • Leave action moved to end as destructive

6. Swipe-to-Reveal Actions

  • SwipeableChannelItem wraps channel items with AnchoredDraggableState-based swipe gesture
  • SwipeRevealCoordinator ensures only one item is open at a time
  • Default actions:
    • DM: More (gray, left) + Archive/Unarchive (blue, right)
    • Group: More (gray, left) + Mute/Unmute (blue, right)
  • Fallback priority: DM = Archive → Mute → Pin, Group = Mute → Archive → Pin
  • "More" opens the channel options bottom sheet (same as long-press)
  • Slot-based SwipeActionStyle enum: Primary / Secondary / Destructive
  • Kill switch: ChannelListConfig(swipeActionsEnabled = false)

Customization via ChatComponentFactory:

  1. ChannelSwipeActions() — replace which actions appear (keeps swipe mechanism)
  2. ChannelListItemContent() — full control over swipe wrapper + item

New files: SwipeableChannelItem.kt, SwipeRevealCoordinator.kt, SwipeRevealValue.kt, SwipeActionItem.kt, SwipeActionStyle.kt, DefaultChannelSwipeActions.kt

7. Supporting Components

  • Updated UnreadCountIndicator: removed white border, switched to numericMedium (10sp)
  • Updated Timestamp, delivery status tokens
  • Updated SearchResultItem and SelectedChannelMenu tokens
  • Channel list header: swapped pencil icon for plus icon, updated paddings
  • Search input: decoupled from InputField, updated radius and icon
  • Added size16 design token and channel-list typing string resources

Testing

  • All Paparazzi snapshots re-recorded
  • New snapshot: muted_channel_trailing_bottom
  • On-device tested across DM and group conversations, light and dark theme
  • Verified focus/selected states, typing indicators, attachment previews, mute positioning
  • Verified swipe gestures, single-open coordination, action execution, auto-close
  • Verified fallback priority when capabilities are restricted
  • Sample app's CustomChatComponentFactory updated to include swipe wrapper

Summary by CodeRabbit

Release Notes

  • New Features

    • Added swipe-to-reveal actions for channel items (archive, mute, pin, delete)
    • Introduced configurable mute indicator positioning and swipe action controls
    • Added channel pinning and selection visual indicators
    • Enhanced empty state messaging and skeleton loading animation
  • UI/Design Improvements

    • Redesigned channel list layout with improved spacing and typography
    • Refined message preview formatting with better sender attribution
    • Enhanced channel action menu with clearer visual hierarchy
    • Improved unread count badge styling and colors
  • Bug Fixes

    • Fixed message deletion indicators and error state visibility
    • Improved read status icon display

- 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.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.69 MB 0.44 MB 🟡
stream-chat-android-ui-components 10.60 MB 10.97 MB 0.37 MB 🟡
stream-chat-android-compose 12.81 MB 11.95 MB -0.87 MB 🚀

@aleksandar-apostolov aleksandar-apostolov changed the title feat(compose): channel list visual redesign — Figma alignment Channel list redesign Feb 23, 2026
Fix detekt MaxLineLength/LongMethod/MagicNumber violations, spotless
formatting, and docs compile error from formatMessagePreview signature.
@aleksandar-apostolov aleksandar-apostolov changed the title Channel list redesign Apply new Channel list design Feb 24, 2026
…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)
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.
@aleksandar-apostolov aleksandar-apostolov changed the title Apply new Channel list design feat(compose): redesign channel list with swipe actions Feb 25, 2026
@aleksandar-apostolov aleksandar-apostolov changed the title feat(compose): redesign channel list with swipe actions Redesign channel list, swipe actions and actions bottom sheet Feb 25, 2026
# 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
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

- 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
@aleksandar-apostolov aleksandar-apostolov marked this pull request as ready for review February 25, 2026 15:59
@aleksandar-apostolov aleksandar-apostolov requested a review from a team as a code owner February 25, 2026 15:59
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Walkthrough

This 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

Cohort / File(s) Summary
Planning & Documentation
.planning/PROJECT.md, .planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md, .planning/config.json, redesign/channel-list/GAPS.md
New planning documentation for release process migration, project roadmap phases, requirements traceability, and design gap analysis for channel list redesign.
Channel List State & Configuration
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt
Added isPinned and isSelected properties to ChannelItemState; introduced MuteIndicatorPosition enum and ChannelListConfig data class with swipe actions and mute indicator position settings; added size16 token.
Channel List UI Redesign
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt
Major visual overhaul: elevated avatar sizing, refactored center content with inline title row and mute indicator placement options, enhanced message preview row with draft/typing indicators, updated header layout with divider, improved spacing using StreamTokens, simplified trailing content, added selection/focus states.
Swipe Actions Infrastructure
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionItem.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt
New swipe-to-reveal system with coordinator for managing single-open item state, anchored draggable states, default swipe actions (Mute/Unmute/Pin/Unpin/Archive/Unarchive/More), and three action styles (Primary/Secondary/Destructive).
Channel Menu & Actions UI
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
Replaced isMuted and ChannelOptionState with channelActions list; introduced buildDefaultChannelActions helper; updated menu signature to accept actions; wired confirmation flow via executeOrConfirm with ConfirmationPopup; added empty state content customization.
Channel Action System Refactoring
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt, stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt, stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItemsFactory.kt
Converted ChannelAction from sealed class to public interface with extended metadata (icon, label, onAction callback, requiredCapability, confirmationPopup, isDestructive); introduced ConfirmationPopup data class; updated all action implementations (ViewInfo, LeaveGroup, DeleteConversation, Pin\/Archive\, Mute\*) with explicit UI/behavior fields; refactored factories to return actions instead of option items.
Removed/Deleted Types
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ChannelOptionState.kt, stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItem.kt
Deleted ChannelOptionState and ChannelOptionItem classes; replaced by action-based system.
Message Preview & Formatting
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt
Added isDirectMessaging parameter to message preview and sender name formatting; enhanced preview types (system messages, polls, audio, location, attachments) with typed attachment rendering; introduced getLastMessageIncludingDeleted utility; updated sender name logic for DM contexts.
ViewModel & Lifecycle Updates
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt
Replaced performChannelAction with executeOrConfirm for conditional confirmation flow; added confirmPendingAction for post-confirmation execution; populated isPinned in ChannelItemState.
Component Factory & Theme
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt
Refactored SearchInput to BasicTextField with internal state sync; updated Timestamp styling (captionDefault + StreamTokens); changed divider color to borderCoreSubtle; added IsErrorIcon to sync failure feedback; updated badge colors and shapes (Primary, pill-shaped).
Sample App & Documentation
stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt, stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt, stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt, stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/channels/SelectedChannelMenu.kt, stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt
Updated sample activity to use buildDefaultChannelActions and executeOrConfirm; added conditional swipe wrapping in CustomChatComponentFactory; refactored documentation examples to match new channel actions API; updated E2E test assertions for message preview sender logic.
Visual Assets
stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_*.xml, stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_*.xml, stream-chat-android-compose/src/main/res/values/strings.xml
New/updated drawable icons for add, clock, error, search, message status, more options, muted state; new action icons (archive, delete, leave, more, mute, pin, unarchive, unmute, unpin, view info); expanded string resources for typing, swipe actions, empty states, and message previews (audio, photo, video, file, link, location, polls).
API & Manifest
stream-chat-android-compose/api/stream-chat-android-compose.api, stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
API surface expansions documenting public additions: new state properties (isPinned, isSelected, endOfChannels, loading, loadingMore), swipe coordinator/value types, ChannelListConfig enum/properties, new composables (DefaultChannelItemDivider, SwipeableChannelItem, ChannelSwipeActions), factory method updates, and ChannelAction interface refactoring.

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
Loading
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
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 The channel list hops with new swipes so fine,
Actions dance freely on configurable spine,
Pinned states and selections bring clarity bright,
While mute icons find their perfect light—
From sealed to interface, the redesign takes flight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main changes: channel list redesign, swipe actions, and bottom sheet redesign.
Description check ✅ Passed The PR description comprehensively covers Goal, Implementation, and Testing sections with detailed subsections for each major change area.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch redesign/channel-list

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

KDoc on the public overload does not document the newly handled FAILED_PERMANENTLY state.

The @param block 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 | 🟡 Minor

Use local text state for clear/placeholder visibility.

The clear button and placeholder are keyed off query, but typing first updates textState. 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: strokeColor should use a placeholder color, not a semantic gray.

The project convention for stream_compose_ic_*.xml drawables 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's src_in tint 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_*.xml in the stream-chat-android-compose module, 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 @OptIn annotations 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 develop and note release-owner coordination when touching changelog/release-process artifacts.

Based on learnings: Target develop branch 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: isErrorIcon slot 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-handled FAILED_PERMANENTLY branch (line 108) hard-wires IsErrorIcon with 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 the color parameter.

The public function signature exposes color as an overridable parameter whose default has just changed (from accentError to accentPrimary). 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$d instead of %d makes 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 that spacedBy plus inner padding matches the intended Figma gap.

Arrangement.spacedBy(spacingMd) (16dp) applies between all three children. Combined with the center content's own start = 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 Timestamp composable's default textStyle is ChatTheme.typography.captionDefault.copy(color = ChatTheme.colors.textTertiary, lineHeight = StreamTokens.lineHeightNormal). Since StreamTokens.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 undocumented LongMethod suppression.

DefaultChannelItemCenterContent is 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 @OptIn annotations 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 for isDirectMessaging and avoid undocumented suppression.

The new public parameter is missing KDoc, and the new LongMethod suppression 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 undocumented LongMethod suppression.

Consider extracting parts of SearchInput (state sync / decoration) into helpers instead of suppressing the rule directly.

As per coding guidelines: **/*.kt: Use @OptIn annotations 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 to firstNotNullOfOrNull { it } — but more importantly, for DMs the archive action is always returned.

Since archiveAction and pinAction are never null, the first non-null in the DM list [archiveAction, muteAction, pinAction] is always archiveAction, and in the group list [muteAction, archiveAction, pinAction], it's muteAction if available, otherwise archiveAction. The fallback priority works, but the function can never return null overall (pin is always non-null). Consider simplifying the return type to non-nullable if that's the intent, or use filterNotNull().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 on buildDefaultChannelActions.

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

LeaveGroup and DeleteConversation are destructive, but confirmationPopup defaults to null (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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +34 to +38
val expectedPreview = when (fromCurrentUser) {
true -> "You: $text"
false -> "${ParticipantRobot.name}: $text"
null -> text
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -A2

Repository: GetStream/stream-chat-android

Length of output: 99


🏁 Script executed:

# Find all usages of assertMessageInChannelPreview without the problematic --type flag
rg -n 'assertMessageInChannelPreview' -B1 -A1

Repository: 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 = trueprefix = PreviewAuthorPrefix.CURRENT_USER, fromCurrentUser = falseprefix = 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).

Comment on lines +50 to +73
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,
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the SwipeableChannelItem definition to understand its signature
fd -t f "SwipeableChannelItem" --type kt

Repository: 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 3

Repository: 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 2

Repository: GetStream/stream-chat-android

Length of output: 99


🏁 Script executed:

# Check the ChatComponentFactory for the same swipe pattern
fd -t f "ChatComponentFactory.kt" | head -20

Repository: GetStream/stream-chat-android

Length of output: 455


🏁 Script executed:

# Search for SwipeableChannelItem definition and usage
rg "fun SwipeableChannelItem" -A 10

Repository: GetStream/stream-chat-android

Length of output: 3327


🏁 Script executed:

# Search for SwipeableChannelItem composable signature
rg "SwipeableChannelItem\(" -B 2 -A 3

Repository: GetStream/stream-chat-android

Length of output: 2897


🏁 Script executed:

# Look for animateItem modifier usage
rg "animateItem\(\)" -B 3 -A 3

Repository: 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 -100

Repository: 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.kt

Repository: 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 -100

Repository: GetStream/stream-chat-android

Length of output: 8000


🏁 Script executed:

# Check ChannelItem composable to understand how it uses the modifier
rg "fun ChannelItem" -A 20

Repository: 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 -100

Repository: 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 10

Repository: 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.kt

Repository: 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 -15

Repository: GetStream/stream-chat-android

Length of output: 451


🏁 Script executed:

# Check how animateItem() is documented/defined
rg "fun.*animateItem" -A 5 --maxcount=3

Repository: 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 -40

Repository: 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.

Comment on lines +4049 to +4059
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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for MessagePreviewFormatter in the source code
find . -type f -name "*.kt" | xargs grep -l "MessagePreviewFormatter" | head -20

Repository: 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 -20

Repository: 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.kt

Repository: 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.api

Repository: 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 -10

Repository: 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 -10

Repository: 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.kt

Repository: 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 -50

Repository: 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 -20

Repository: 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.

Comment on lines +178 to +191
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)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +419 to +431
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,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +323 to +324
<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>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Comment on lines +57 to +70
@Test
fun `muted channel trailing bottom`() {
snapshot {
ChatTheme(
config = ChatConfig(
channelList = ChannelListConfig(
muteIndicatorPosition = MuteIndicatorPosition.TRAILING_BOTTOM,
),
),
) {
ChannelItemMutedTrailingBottom()
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines 24 to 35
/**
* 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.
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +165 to +168
channelOptionTextView.text = action.label
channelOptionTextView.setStartDrawable(
requireContext().getDrawableCompat(action.icon)!!,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

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

Labels

pr:breaking-change Breaking change pr:new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant