Skip to content

Attachment picker components API update#6130

Merged
andremion merged 33 commits intov7from
redesign/AND-1030-attachment-picker-api-update
Feb 6, 2026
Merged

Attachment picker components API update#6130
andremion merged 33 commits intov7from
redesign/AND-1030-attachment-picker-api-update

Conversation

@andremion
Copy link
Contributor

@andremion andremion commented Feb 4, 2026

🎯 Goal

Simplify and modernize the Attachment Picker API.

The existing attachment picker used a factory pattern with 7 factory classes (AttachmentsPickerTabFactory + 6 implementations), scattered configuration, and a sealed class hierarchy that limited extensibility. This PR introduces a cleaner, more intuitive API that:

  • Reduces complexity by ~70% (~1,400 lines removed vs ~600 added)
  • Centralizes configuration in a single AttachmentPickerConfig data class
  • Replaces the factory pattern with direct ChatComponentFactory composable overrides
  • Uses typed mode classes with meaningful configuration properties
  • Removes deprecated/unused code and improves KDoc documentation

🛠 Implementation details

New API Structure:

  1. AttachmentPickerMode interface - Replaces the AttachmentsPickerMode sealed class

    • GalleryPickerMode(allowMultipleSelection, mediaType) - Pick images/videos from gallery
    • FilePickerMode(allowMultipleSelection) - Pick files from storage
    • CameraPickerMode(captureMode) - Capture photos/videos
    • PollPickerMode(autoShowCreateDialog) - Create polls
    • CommandPickerMode - Select commands
  2. AttachmentPickerConfig - Centralized picker configuration

    • useSystemPicker: Boolean - Toggle between in-app and system picker
    • modes: List<AttachmentPickerMode> - Configure available picker modes
  3. ChatComponentFactory extensions - New factory methods for customization:

    • AttachmentPickerMenu(), AttachmentPicker(), AttachmentTypePicker()
    • AttachmentPickerContent(), AttachmentMediaPicker(), AttachmentFilePicker()
    • AttachmentCameraPicker(), AttachmentPollPicker(), AttachmentCommandPicker()
    • AttachmentSystemPicker(), AttachmentTypeSystemPicker()

Removed:

  • AttachmentsPickerTabFactory interface and all implementations
  • AttachmentsPickerTabFactories static factory methods
  • AttachmentsProcessingViewModel from factory package (moved to viewmodel package)
  • CaptureMediaLauncher.kt
  • Legacy AttachmentsPickerMode sealed class
  • ChatTheme.useDefaultSystemMediaPicker parameter and property
  • ChatComponentFactory.AttachmentsPickerSendButton()
  • DefaultAttachmentsPickerSendButton()
  • SystemAttachmentsPickerConfig

Refactored:

  • Permission-related components moved to dedicated permission sub-package
  • AttachmentsPickerViewModel updated to use nullable pickerMode for proper initialization
  • Improved permission state observation to trigger callbacks only on actual state changes
  • Comprehensive KDoc updates for all public attachment picker components

Permissions:

  • Declared permissions in the common module are now declared in the samples only, so that customer apps won't contain those permissions by default, making the system picker the default option.

🎨 UI Changes

No visual changes - this is an API refactoring. The attachment picker UI remains identical.

🧪 Testing

Manual Testing:

  1. Open the Compose sample app
  2. Navigate to a channel and tap the attachment button
  3. Verify all picker tabs work correctly (Gallery, Files, Camera, Poll)
  4. Test system picker mode by setting useSystemPicker = true in AttachmentPickerConfig
  5. Verify location sharing works in the sample (updated for new API)

Automated Testing:

  • Unit tests updated for RequiredPermission logic
  • Removed obsolete factory tests (AttachmentsPickerTabFactoriesTest, AttachmentsPickerPollTabFactoryTest, AttachmentsProcessingViewModelTest)
  • Paparazzi snapshots updated

🎉 GIF

KISS

Summary by CodeRabbit

  • New Features

    • Location sharing integrated into the composer.
    • Redesigned, mode-based attachment picker with unified menu (gallery, files, camera, polls, commands) and improved single/multi-select behavior.
    • Composer exposes an attachments button to toggle the picker; picker coordinates keyboard/auto-dismiss flows.
  • Breaking Changes

    • Simplified attachment picker configuration and UI; legacy tab-factory customization removed.
    • Permission prompts updated for modern media access.

@andremion andremion changed the base branch from develop to v7 February 4, 2026 17:16
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.26 MB 5.26 MB 0.00 MB 🟢
stream-chat-android-offline 5.48 MB 5.48 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.63 MB 10.62 MB -0.01 MB 🚀
stream-chat-android-compose 12.84 MB 11.68 MB -1.16 MB 🚀

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Walkthrough

Redesigns the attachment picker from a tab-factory model to a mode/config-driven system: introduces AttachmentPickerMode and AttachmentPickerConfig, replaces tab factories with AttachmentPickerMenu and component-factory wiring, updates view models and composables, removes legacy tab-factory API, and updates samples, tests, and theming to the new mode-based surface.

Changes

Cohort / File(s) Summary
Core mode & config
stream-chat-android-compose/src/main/java/.../attachments/AttachmentPickerMode.kt, .../AttachmentPickerConfig.kt, deleted .../AttachmentsPickerMode.kt
Adds AttachmentPickerMode and concrete modes (Gallery, File, Camera, Poll, Command), MediaType/CaptureMode enums, and AttachmentPickerConfig; removes legacy AttachmentsPickerMode sealed class.
Attachment picker UI (high-level)
.../ui/messages/attachments/AttachmentPicker.kt, AttachmentPickerContent.kt, AttachmentPickerMenu.kt, AttachmentTypePicker.kt
Reworks picker UI to use mode-based routing and ChatTheme.componentFactory delegates; changes API from attachmentsPickerMode → pickerMode and replaces tab-clicks with mode-selection callbacks.
Individual picker implementations
.../AttachmentCameraPicker.kt, .../AttachmentFilePicker.kt, .../AttachmentMediaPicker.kt, .../AttachmentPollPicker.kt, .../AttachmentCommandPicker.kt, .../AttachmentSystemPicker.kt
Updates each picker to accept the new mode types, threads allowMultipleSelection where applicable, revises launchers and permission handling, and adapts previews/tests.
Component factory & theme API
.../ui/theme/ChatComponentFactory.kt, .../ui/theme/ChatTheme.kt, api/stream-chat-android-compose.api
Expands ChatComponentFactory with AttachmentPicker* composables, introduces attachmentPickerConfig in ChatTheme, removes tab-factory APIs and SystemAttachmentsPickerConfig usage.
ViewModels & processing
.../viewmodel/messages/AttachmentProcessingViewModel.kt, .../viewmodel/messages/AttachmentsPickerViewModel.kt
Renames AttachmentsProcessingViewModel → AttachmentProcessingViewModel; AttachmentsPickerViewModel gains pickerMode: AttachmentPickerMode?, changePickerMode(...), channelState parameter, new selection logic supporting multi-select.
Removed tab-factory files & tests
.../attachments/factory/* (many files deleted)
Deletes AttachmentsPickerTabFactory and all concrete tab-factory implementations and factory builders; removes related tests and previews.
Permissions & helpers reorganized
.../attachments/permission/*, .../media/CaptureMediaLauncher.kt (deleted)
Moves permission helpers to a permission package, renames snackbar, replaces onResume callbacks with onAccessChange, and removes the old CaptureMediaLauncher.
Samples & location integration
stream-chat-android-compose-sample/.../MessagesActivity.kt, CustomChatComponentFactory.kt, location/LocationComponentFactory.kt, location/LocationPickerMode.kt
Updates sample wiring to use AttachmentPickerMenu, introduces locationViewModelFactory and LocationComponentFactory, and removes direct tab-factory usage.
Platform / contracts
stream-chat-android-ui-common/.../SelectFilesContract.kt, .../SystemAttachmentsPickerConfig.kt (removed)
Makes file picker contract accept a Boolean for multi-select; removes SystemAttachmentsPickerConfig public data class.
Manifests & permissions
sample AndroidManifests
Adds READ_MEDIA_* / READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE entries to samples for in-app picker and legacy downloads.

Sequence Diagram(s)

sequenceDiagram
    participant Composer as MessageComposer
    participant Menu as AttachmentPickerMenu
    participant VM as AttachmentsPickerViewModel
    participant Factory as ChatComponentFactory
    participant System as SystemPicker/Launchers
    participant Location as LocationViewModelFactory

    Composer->>Menu: onAttachmentsClick()
    Menu->>VM: changePickerMode(mode) / changeAttachmentState(show)
    VM->>Factory: request AttachmentPicker (pickerMode, attachments, commands)
    Factory->>System: launch system pickers or in-app pickers (File/Media/Camera/Poll)
    System-->>Factory: attachments metadata
    Factory-->>VM: onAttachmentsSubmitted(meta)
    VM-->>Composer: getSelectedAttachmentsAsync -> deliver attachments to composer
    Note right of Location: LocationComponentFactory hooks into Factory when location mode selected
Loading

(Note: colored rectangles not applicable in this simple sequence.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

compose, ui-components

Suggested reviewers

  • gpunto
  • VelikovPetar

Poem

🐰 Hopping through code with clever cheer,

Modes replaced the tabs — the path is clear.
Factories hum and pickers bounce along,
Composer sings a short, delightful song.
Tiny paws delivered a cleaner UI throng.

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While most changes align with the picker API redesign objective, some changes (e.g., changes to SelectFilesContract, default useSystemPicker flag, permission manifest updates) may warrant clarification as they appear tangential to the core redesign. Verify that permission manifest changes and SelectFilesContract modifications are intentional and necessary for the picker redesign; clarify scope if needed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Attachment picker components API update' clearly and concisely describes the main change—a modernization of the attachment picker API.
Linked Issues check ✅ Passed The PR fully implements the objectives from AND-1030: introduces AttachmentPickerMode interface with typed modes, centralizes configuration in AttachmentPickerConfig, replaces factory pattern with ChatComponentFactory overrides, and updates samples/docs for the redesigned picker.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering goal, implementation details, UI changes, testing, and provides clear context for the API modernization.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch redesign/AND-1030-attachment-picker-api-update

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.

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@andremion andremion marked this pull request as ready for review February 5, 2026 14:16
@andremion andremion requested a review from a team as a code owner February 5, 2026 14:16
Copy link
Contributor

@gpunto gpunto left a comment

Choose a reason for hiding this comment

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

Left some comments, but looks good to me!

@andremion andremion force-pushed the redesign/AND-1030-attachment-picker-api-update branch from 7b83148 to d916300 Compare February 5, 2026 16:51
@andremion
Copy link
Contributor Author

@CodeRabbit full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

✅ Actions performed

Full review triggered.

Renamed `selectedAttachmentPickerMode` to `selectedMode` and `additionalContent` to `trailingContent` in the `AttachmentTypePicker` and `AttachmentTypeSystemPicker` composables for better clarity and consistency.
The default for `useSystemPicker` and `useDefaultSystemMediaPicker` has been changed from `false` to `true`. This makes the system's native file/media picker the default option, which doesn't require explicit storage permissions in the app.

The in-app picker can still be used by setting this flag to `false`. Documentation and sample apps have been updated to reflect this change and include the necessary permissions for when the in-app picker is enabled.
This change introduces support for single file selection within the attachment picker, in addition to the existing multiple selection functionality.

Key changes include:
*   The `FilesPicker` now accepts an `allowMultipleSelection` flag, which determines whether to display checkboxes (for multi-select) or radio buttons (for single-select).
*   The `AttachmentsPickerViewModel` has been updated to handle both single and multiple selection logic when an item is selected.
*   The `SelectFilesContract` is now passed a boolean to control whether the system's file picker allows multiple file selection.
*   Added and updated tests and snapshot previews for both single and multiple selection modes.
Adds an `allowMultipleSelection` parameter to the `ImagesPicker` and `AttachmentMediaPicker` Composables.

When `allowMultipleSelection` is `false`, the selected item indicator changes from a numbered circle to a checkmark, providing a better user experience for single-selection scenarios.

The `pickerMode` parameter in `AttachmentMediaPicker` is now used to control this behavior.
The `AttachmentPicker` composable now accepts a `modifier` parameter, allowing for easier customization of its layout from the outside.
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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/AttachmentsPicker.kt (1)

119-157: ⚠️ Potential issue | 🟡 Minor

Customization snippet no longer demonstrates any customization.

The "Customization" section (linked to the Customization docs) now shows the exact same AttachmentPicker usage as the "Usage" snippet — there's no custom tab, custom factory, or any configuration override. If the old AttachmentsPickerCustomTabFactory / FullScreenPickerExample were removed because the factory pattern is gone, this snippet should be updated to showcase the new customization surface (e.g., providing a custom AttachmentPickerConfig with specific modes, or overriding ChatComponentFactory composables). Otherwise the docs section will be misleading.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentFilePicker.kt (1)

261-303: ⚠️ Potential issue | 🟡 Minor

Update preview pattern to match documented guidelines.

The preview does not follow the pattern documented in stream-chat-android-compose/GUIDELINES.md. Per guidelines, previews should use @Preview(name = "...") and wrap with ChatTheme instead of ChatPreviewTheme(showBackground = true):

`@Preview`(name = "AttachmentFilePickerSingleSelection Preview")
`@Composable`
private fun AttachmentFilePickerSingleSelectionPreview() {
    ChatTheme {
        AttachmentFilePickerSingleSelection()
    }
}
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt (1)

53-59: ⚠️ Potential issue | 🟡 Minor

Align preview theme with similar attachment pickers: use ChatPreviewTheme instead of ChatTheme.

This preview uses ChatTheme { ... } directly, but AttachmentFilePicker, AttachmentMediaPicker, and AttachmentSystemPicker all use ChatPreviewTheme { ... }, which initializes ChatClient for preview rendering. Aligning with this established pattern ensures consistency across attachment picker previews.

Switch to ChatPreviewTheme
 `@Preview`(showBackground = true)
 `@Composable`
 private fun AttachmentCommandPickerPreview() {
-    ChatTheme {
+    ChatPreviewTheme {
         AttachmentCommandPicker()
     }
 }
stream-chat-android-compose/api/stream-chat-android-compose.api (1)

356-459: ⚠️ Potential issue | 🟡 Minor

Fallback handling for custom AttachmentPickerMode implementations is safe but silent.

The code includes an else -> Unit fallback in AttachmentPickerContent (lines 43–71), so custom implementations won't crash the UI. However, this means unknown modes silently render nothing. The sample app demonstrates the correct pattern: override ChatComponentFactory.AttachmentPickerContent() to handle custom modes. While this design is documented in the code comments, custom mode support is not immediately discoverable. Consider making the override pattern more prominent in public documentation or KDoc.

🤖 Fix all issues with AI agents
In
`@stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/location/LocationComponentFactory.kt`:
- Around line 57-59: Update the incorrect KDoc above the
LocationComponentFactory: replace the copy-pasted "Factory for creating
components related to deleting messages for the current user." with a concise
docstring that accurately describes this factory as creating location-sharing
components (e.g., location picker, map preview, or location message UI) for the
sample app; ensure the KDoc is placed immediately above the
LocationComponentFactory declaration and references its purpose for location
sharing.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentTypePicker.kt`:
- Around line 89-91: The LaunchedEffect currently does
onModeSelected(modes.first()) which will throw if modes is empty and will re-run
on every modes change, resetting selection; update the LaunchedEffect to guard
for modes.isNotEmpty() before calling onModeSelected and only auto-select once
on initial appearance (e.g., use LaunchedEffect(Unit) or track a remembered flag
like hasAutoSelected) so subsequent changes to modes (from filterByCapabilities
or capability updates) don't overwrite the user's current selection; reference
the AttachmentTypePicker.kt LaunchedEffect, onModeSelected, modes.first(),
filterByCapabilities, AttachmentPickerConfig and PollPickerMode when locating
where to add the empty-check and the one-time auto-select guard.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel.kt`:
- Around line 95-102: The KDoc for changePickerMode refers to `@param` pickerMode
but the function signature uses attachmentPickerMode; update either the
parameter name or the KDoc so they match. Specifically, in the function
changePickerMode(attachmentPickerMode: AttachmentPickerMode) make the parameter
name pickerMode to align with the existing KDoc, or change the KDoc `@param` to
`@param` attachmentPickerMode — ensure the parameter identifier in the function
signature and the KDoc entry are identical.

In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/AttachmentsPickerDialogStyle.kt`:
- Around line 98-99: The default for AttachmentsPickerDialogStyle has been
changed so useDefaultSystemMediaPicker = true which reverses prior in-app picker
behavior and causes a breaking default; restore backward compatibility by
changing the default back to false on the AttachmentsPickerDialogStyle data
class (symbol: AttachmentsPickerDialogStyle, parameter:
useDefaultSystemMediaPicker) so existing callers keep the original in-app picker
behavior, and note the intentional change in the migration/changelog or add a
clear comment on the class if you intend to keep the new default.

In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/MessageComposerViewStyle.kt`:
- Around line 1439-1442: The default for the XML attribute
MessageComposerView_streamUiMessageComposerAttachmentsPickerSystemPickerEnabled
was changed to true causing a breaking behavioral change; restore the previous
default of false in MessageComposerViewStyle by setting the fallback value for
useDefaultSystemMediaPicker to false (reference the val
useDefaultSystemMediaPicker in MessageComposerViewStyle and the
R.styleable.MessageComposerView_streamUiMessageComposerAttachmentsPickerSystemPickerEnabled
attribute), and if you still want Compose-aligned defaults keep that change only
in Compose codepaths and add a clear upgrade/migration note documenting the
default difference for XML-based integrations.
🧹 Nitpick comments (17)
stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/AttachmentsPicker.kt (1)

145-145: Unused attachments lambda parameter.

The named parameter attachments on line 145 is captured but never referenced in the body. Either use _ (like the usage snippet on line 51 which omits the name entirely) or show a usage example for consistency.

-                                onAttachmentsSelected = { attachments ->
+                                onAttachmentsSelected = {
stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/MessageComposer.kt (1)

89-92: contentPadding is unused — consider suppressing or prefixing with underscore.

The named parameter improves readability for the docs, but it will generate an "unused variable" warning. Since this is a documentation snippet, a minor nit:

-            ) { contentPadding ->
+            ) { _ ->

Or keep the name for documentation clarity and add @Suppress("UNUSED_PARAMETER") — either way is fine for a snippet file.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentTypePicker.kt (2)

76-78: Selection comparison via ::class may match unintended mode instances.

selectedMode::class == mode::class treats all instances of the same mode class as identical. Today this is fine because only one instance of each mode class appears in the list. However, if a future configuration included two GalleryPickerMode instances with different mediaType values, the selection highlight would match both. Consider comparing by equality (selectedMode == mode) if the mode types are data classes, which would be both simpler and more precise.

Suggested simplification
-            val isSelected = selectedMode != null &&
-                selectedMode::class == mode::class
+            val isSelected = selectedMode == mode

This works correctly when selectedMode is null (evaluates to false) and gives exact-instance matching if modes are data classes.


55-92: Wrap modes computation with remember to avoid recomputing the filtered list on every recomposition.

ChatTheme.attachmentPickerConfig.modes.filterByCapabilities(...) creates a new list instance on each recomposition via the filter() call. Since modes is used as the LaunchedEffect key in AttachmentTypePicker, this causes the effect to re-fire unnecessarily, resetting the selection on every recomposition. Even in AttachmentTypeSystemPicker (which lacks the effect), the allocation is wasteful. Both functions should use remember(channel, messageMode) to memoize the filtered list.

Suggested change for AttachmentTypePicker (apply similarly to AttachmentTypeSystemPicker)
+    val modes = remember(channel, messageMode) {
+        ChatTheme.attachmentPickerConfig.modes.filterByCapabilities(
+            channel = channel,
+            messageMode = messageMode,
+        )
+    }
-    val modes = ChatTheme.attachmentPickerConfig.modes.filterByCapabilities(
-        channel = channel,
-        messageMode = messageMode,
-    )
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/images/ImagesPicker.kt (1)

199-211: Consider adding contentDescription to the checkmark icon for accessibility.

Line 208: contentDescription = null on the checkmark means screen readers won't announce the selected state in single-selection mode. The multi-selection branch (position number as Text) has a similar gap, but numeric text is at least readable. A short description like "Selected" (via stringResource) on the Icon would improve the experience for assistive technology users.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/permission/RequiredPermission.kt (1)

133-152: Previews should use @StreamPreview helpers.

The preview functions use @Preview with ChatTheme { ... } instead of the @StreamPreview helpers. As per coding guidelines for Compose files in stream-chat-android-compose: "Compose previews should use StreamPreview helpers."

stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/cookbook/ui/CustomComposerAndAttachmentsPicker.kt (1)

413-421: Preview function visibility could be narrowed to private.

PreviewCustomAttachmentPickerOptions is currently public. Preview composables are typically private to avoid polluting the public API surface.

Suggested fix
 `@Preview`(showBackground = true)
 `@Composable`
-fun PreviewCustomAttachmentPickerOptions() {
+private fun PreviewCustomAttachmentPickerOptions() {
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/permission/VisualMediaAccessAsState.kt (1)

45-69: onAccessChange can become stale if the caller recomposes with a new lambda.

The onAccessChange lambda is captured by the produceState coroutine but is not included as a key. If the caller recomposes with a different lambda instance, the stale closure will continue to be invoked from the lifecycle observer. Consider wrapping it with rememberUpdatedState to keep the reference current:

Proposed fix
+import androidx.compose.runtime.rememberUpdatedState
+
 `@Composable`
 internal fun visualMediaAccessAsState(
     context: Context,
     lifecycleOwner: LifecycleOwner,
     onAccessChange: (VisualMediaAccess) -> Unit,
-): State<VisualMediaAccess> =
-    produceState(
+): State<VisualMediaAccess> {
+    val currentOnAccessChange by rememberUpdatedState(onAccessChange)
+    return produceState(
         initialValue = resolveVisualMediaAccessState(context),
         context,
         lifecycleOwner,
     ) {
-        onAccessChange(value)
+        currentOnAccessChange(value)
 
         val eventObserver = LifecycleEventObserver { _, event ->
             if (event == Lifecycle.Event.ON_RESUME) {
                 val newAccess = resolveVisualMediaAccessState(context)
                 if (newAccess != value) {
                     value = newAccess
-                    onAccessChange(newAccess)
+                    currentOnAccessChange(newAccess)
                 }
             }
         }
         lifecycleOwner.lifecycle.addObserver(eventObserver)
         awaitDispose {
             lifecycleOwner.lifecycle.removeObserver(eventObserver)
         }
     }
+}

The same pattern applies to FilesAccessAsState.kt.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/attachments/files/FilesPicker.kt (1)

196-257: Consider using @StreamPreview helpers for previews.

The preview functions use @Preview(showBackground = true) directly. The project's coding guidelines suggest using @StreamPreview helpers for Compose previews. This is a minor consistency nit—the current approach works fine. As per coding guidelines: "Compose previews should use StreamPreview helpers."

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCameraPicker.kt (1)

94-98: Unnecessary safe call on non-nullable launcher.

rememberCaptureMediaLauncher (Line 158) accepts a non-nullable CaptureMediaContract.Mode and returns a non-nullable ManagedActivityResultLauncher. The ?. on Line 97 is therefore redundant. Note the difference with the variant in AttachmentSystemPicker.kt which takes a nullable mode and can return null.

Use non-null call
     if (showRequiredCameraPermission) {
         RequiredCameraPermission()
     } else {
-        AttachmentCameraPickerContent { captureMediaLauncher?.launch(Unit) }
+        AttachmentCameraPickerContent { captureMediaLauncher.launch(Unit) }
     }
stream-chat-android-compose/api/stream-chat-android-compose.api (2)

2218-2218: Align AttachmentPicker parameter order with factory override.
The composable uses (viewModel, modifier, …) while ChatComponentFactory.AttachmentPicker uses (modifier, viewModel, …). This mismatch increases the risk of mis‑ordered args in custom factory implementations; consider aligning the order or calling out named‑argument usage.


301-315: Consider defensively copying modes in AttachmentPickerConfig for stricter immutability.

While the parameter is typed as List (immutable interface) and the default uses listOf(), a caller could theoretically pass a MutableList implementation and modify it externally, bypassing the data class's equality contract. Defensively copying to an immutable list in the constructor would provide stricter guarantees, though the current type signature and defaults already provide reasonable protection.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt (2)

114-119: Consider adding a brief comment explaining the else -> true default.

The fallback to true is safe because only FilePickerMode and GalleryPickerMode drive item selection callbacks, while camera/poll/command modes use different interaction paths. A short inline comment would help future maintainers (and custom-mode authors) understand this implicit contract.

📝 Suggested comment
                 val allowMultipleSelection = when (val mode = attachmentsPickerViewModel.pickerMode) {
                     is FilePickerMode -> mode.allowMultipleSelection
                     is GalleryPickerMode -> mode.allowMultipleSelection
-                    else -> true
+                    // Other modes (camera, poll, command) don't use item selection;
+                    // default to multiple for any custom modes.
+                    else -> true
                 }

128-141: AttachmentPickerCreatePollClick falls through the when silently — intentional but opaque.

The when statement handles AttachmentPickerPollCreation and AttachmentPickerCommandSelect, while AttachmentPickerCreatePollClick intentionally bypasses both branches and only sets isShowingDialog = true on Line 140. This state-machine flow is correct but easy to misread. An explicit branch (even if empty/commented) would make the contract clearer.

📝 Suggested explicit branch
             onAttachmentPickerAction = { action ->
                 when (action) {
                     is AttachmentPickerPollCreation -> {
                         attachmentsPickerViewModel.changeAttachmentState(showAttachments = false)
                         composerViewModel.createPoll(action.pollConfig)
                     }
-
                     is AttachmentPickerCommandSelect -> {
                         attachmentsPickerViewModel.changeAttachmentState(showAttachments = false)
                         composerViewModel.selectCommand(action.command)
                     }
+                    // Poll-create click only opens the dialog; no picker action needed.
+                    is AttachmentPickerCreatePollClick -> Unit
                 }
                 isShowingDialog = action is AttachmentPickerCreatePollClick
             },
stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/location/LocationComponentFactory.kt (1)

86-91: Hardcoded content description string.

"Share Location" is hardcoded in English. For a sample app this is acceptable, but if this pattern is expected to be lifted into the main library or localized, it should use a string resource.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel.kt (1)

178-188: Duplicated deselection logic with removeSelectedAttachment.

deselectAttachment (Lines 178–188) replicates the same position-shifting deselection logic that appears in removeSelectedAttachment (Lines 130–150). Consider extracting a shared private helper to avoid the duplication.

♻️ Sketch — shared deselection helper
+    private fun deselectAttachmentAt(itemIndex: Int): List<AttachmentPickerItemState> {
+        val removedPosition = (attachments[itemIndex].selection as Selection.Selected).position
+        return attachments.mapIndexed { index, item ->
+            when {
+                index == itemIndex -> item.copy(selection = Selection.Unselected)
+                item.selection is Selection.Selected && item.selection.position > removedPosition ->
+                    item.copy(selection = Selection.Selected(position = item.selection.position - 1))
+                else -> item
+            }
+        }
+    }

     public fun removeSelectedAttachment(attachment: Attachment) {
         val itemIndex = attachments.indexOfFirst { item ->
             item.attachmentMetaData.title == attachment.name
         }
         if (itemIndex == -1) return
         val currentItem = attachments[itemIndex]
         if (currentItem.selection !is Selection.Selected) return
-        val removedPosition = currentItem.selection.position
-        attachments = attachments.mapIndexed { index, item ->
-            when {
-                index == itemIndex -> item.copy(selection = Selection.Unselected)
-                item.selection is Selection.Selected && item.selection.position > removedPosition ->
-                    item.copy(selection = Selection.Selected(position = item.selection.position - 1))
-                else -> item
-            }
-        }
+        attachments = deselectAttachmentAt(itemIndex)
     }

-    private fun deselectAttachment(itemIndex: Int): List<AttachmentPickerItemState> {
-        val removedPosition = (attachments[itemIndex].selection as Selection.Selected).position
-        return attachments.mapIndexed { index, item ->
-            when {
-                index == itemIndex -> item.copy(selection = Selection.Unselected)
-                item.selection is Selection.Selected && item.selection.position > removedPosition ->
-                    item.copy(selection = Selection.Selected(position = item.selection.position - 1))
-                else -> item
-            }
-        }
-    }
+    private fun deselectAttachment(itemIndex: Int): List<AttachmentPickerItemState> =
+        deselectAttachmentAt(itemIndex)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt (1)

262-270: Minor duplication with AttachmentCameraPicker.kt:156-162.

This rememberCaptureMediaLauncher has nearly identical logic to the one in AttachmentCameraPicker.kt, differing only in the nullable wrapper. Consider extracting a shared helper if the two continue to evolve together, though the current duplication is small and justified by the different nullability semantics.

@andremion andremion force-pushed the redesign/AND-1030-attachment-picker-api-update branch from d916300 to 59dbdba Compare February 6, 2026 08:54
Copy link
Contributor

@VelikovPetar VelikovPetar left a comment

Choose a reason for hiding this comment

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

Left some discussion points, maybe not directly to this PR.

But looks pretty good, nice job!

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 6, 2026

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@andremion andremion merged commit ae62c2a into v7 Feb 6, 2026
13 of 14 checks passed
@andremion andremion deleted the redesign/AND-1030-attachment-picker-api-update branch February 6, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants