Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
107c363
Update `AttachmentTypePicker` to only select first mode if none are s…
andremion Mar 9, 2026
b17c8c9
First refactor `AttachmentsPickerViewModel` to persist selection stat…
andremion Mar 9, 2026
f1cc4d2
Refactor attachment selection state management between `AttachmentsPi…
andremion Mar 9, 2026
ef85d12
Review `AttachmentPicker`
andremion Mar 9, 2026
1770d71
Review `MessagesViewModelFactory`
andremion Mar 9, 2026
8a23cd0
Review `AttachmentPickerActions`
andremion Mar 9, 2026
ae92f40
Review `AttachmentsPickerViewModel`
andremion Mar 9, 2026
bb35cea
Move attachment staging ownership to the controller and persist selec…
andremion Mar 9, 2026
04b822f
Add missed unit tests for attachment management and picker logic in `…
andremion Mar 9, 2026
c1546f8
Update `AttachmentMetaData.toAttachment` to include the original URI …
andremion Mar 10, 2026
6b22b8d
Disable system attachment picker in XML sample app's so it matches wi…
andremion Mar 10, 2026
1e5e568
Update `UserRobot` and `AttachmentsTests` to match with the new UX
andremion Mar 10, 2026
8de33c5
Add `ComposerSessionRepository` to persist and restore message compos…
andremion Mar 13, 2026
1c4c2b2
Move attachment and edit-mode state persistence from `MessageComposer…
andremion Mar 13, 2026
086546e
Improve attachment management in `MessageComposerController`
andremion Mar 13, 2026
c4628ea
Remove unnecessary `clearAttachments` call from `MessagesScreen`
andremion Mar 13, 2026
34494fc
Add setMessageInputInternal(message.text, MessageInput.Source.Edit) s…
andremion Mar 13, 2026
b811121
Include `name` in attachment serialization in `ComposerSessionReposit…
andremion Mar 13, 2026
0ccd8f3
Use full message from channel state in `restoreEditMode`
andremion Mar 13, 2026
e7ef154
Add tests for `MessageComposerController` edit session restoration
andremion Mar 13, 2026
a2889b2
Add a feature flag to enable the system attachment picker in the Comp…
andremion Mar 16, 2026
7660fe9
Fix Attachment type mismatch in test step
andremion Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,10 @@ class UserRobot {
return this
}

fun uploadAttachment(type: AttachmentType, multiple: Boolean = false, send: Boolean = true): UserRobot {
fun attachFile(type: AttachmentType, multiple: Boolean = false): UserRobot {
val count = if (multiple) 2 else 1
Composer.attachmentsButton.waitToAppear().click()
repeat(count) {
Composer.attachmentsButton.waitToAppear().click()
AttachmentPicker.filesTab.waitToAppear().click()
AttachmentPicker.findFilesButton.waitToAppear().click()

Expand All @@ -351,19 +351,12 @@ class UserRobot {
.click()
}

if (type == AttachmentType.FILE) AttachmentPicker.pdf1 else AttachmentPicker.image1

if (it == 0) {
val attachment = if (type == AttachmentType.FILE) AttachmentPicker.pdf1 else AttachmentPicker.image1
attachment.waitToAppear().click()
val attachment = if (it == 0) {
if (type == AttachmentType.FILE) AttachmentPicker.pdf1 else AttachmentPicker.image1
} else {
val attachment = if (type == AttachmentType.FILE) AttachmentPicker.pdf2 else AttachmentPicker.image2
attachment.waitToAppear().click()
if (type == AttachmentType.FILE) AttachmentPicker.pdf2 else AttachmentPicker.image2
}
}

if (send) {
Composer.sendButton.waitToAppear().click()
attachment.waitToAppear().click()
}

return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class AttachmentsTests : StreamTestCase() {
userRobot.login().openChannel()
}
step("WHEN user attaches an image") {
userRobot.uploadAttachment(type = AttachmentType.IMAGE, send = false)
userRobot.attachFile(type = AttachmentType.IMAGE)
}
step("THEN image is displayed in preview") {
userRobot.assertMediaAttachmentInPreview(isDisplayed = true)
Expand All @@ -59,7 +59,7 @@ class AttachmentsTests : StreamTestCase() {
userRobot.login().openChannel()
}
step("WHEN user attaches multiple images") {
userRobot.uploadAttachment(type = AttachmentType.IMAGE, multiple = true, send = false)
userRobot.attachFile(type = AttachmentType.IMAGE, multiple = true)
}
step("THEN images are displayed in preview") {
userRobot.assertMediaAttachmentInPreview(isDisplayed = true, count = 2)
Expand All @@ -79,7 +79,10 @@ class AttachmentsTests : StreamTestCase() {
userRobot.login().openChannel()
}
step("WHEN user sends an image") {
userRobot.uploadAttachment(type = AttachmentType.IMAGE)
userRobot.attachFile(type = AttachmentType.IMAGE)
}
step("AND user sends the image") {
userRobot.tapOnSendButton()
}
step("AND user deletes an image") {
userRobot.deleteMessage()
Expand All @@ -98,7 +101,7 @@ class AttachmentsTests : StreamTestCase() {
userRobot.login().openChannel()
}
step("WHEN user sends a file") {
userRobot.uploadAttachment(type = AttachmentType.FILE, send = false)
userRobot.attachFile(type = AttachmentType.FILE)
}
step("THEN file is displayed in preview") {
userRobot.assertFileAttachmentInPreview(isDisplayed = true)
Expand All @@ -118,7 +121,7 @@ class AttachmentsTests : StreamTestCase() {
userRobot.login().openChannel()
}
step("WHEN user attaches multiple files") {
userRobot.uploadAttachment(type = AttachmentType.FILE, multiple = true, send = false)
userRobot.attachFile(type = AttachmentType.FILE, multiple = true)
}
step("THEN files are displayed in preview") {
userRobot.assertFileAttachmentInPreview(isDisplayed = true, count = 2)
Expand All @@ -137,15 +140,18 @@ class AttachmentsTests : StreamTestCase() {
step("GIVEN user opens the channel") {
userRobot.login().openChannel()
}
step("WHEN user sends a file") {
userRobot.uploadAttachment(type = AttachmentType.IMAGE)
step("WHEN user attaches a file") {
userRobot.attachFile(type = AttachmentType.FILE)
}
step("AND user sends the file") {
userRobot.tapOnSendButton()
}
step("AND user deletes a file") {
userRobot.deleteMessage()
}
step("THEN user can see deleted message") {
userRobot
.assertImage(isDisplayed = false)
.assertFile(isDisplayed = false)
.assertDeletedMessage()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class CustomSettings(private val context: Context) {
var isAdaptiveLayoutEnabled: Boolean by booleanPref(AdaptiveLayout)

var isComposerFloatingStyleEnabled: Boolean by booleanPref(ComposerFloatingStyle)
var isSystemAttachmentPickerEnabled: Boolean by booleanPref(SystemAttachmentPicker)

private fun booleanPref(key: String, default: Boolean = false) =
object : ReadWriteProperty<Any?, Boolean> {
Expand All @@ -47,5 +48,6 @@ class CustomSettings(private val context: Context) {

private const val AdaptiveLayout = "adaptive_layout"
private const val ComposerFloatingStyle = "composer_floating_style"
private const val SystemAttachmentPicker = "system_attachment_picker"

fun Context.customSettings(): CustomSettings = CustomSettings(this)
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class MessagesActivity : ComponentActivity() {
floatingStyleEnabled = settings.isComposerFloatingStyleEnabled,
),
translation = TranslationConfig(enabled = ChatApp.autoTranslationEnabled),
attachmentPicker = AttachmentPickerConfig(useSystemPicker = false),
attachmentPicker = AttachmentPickerConfig(useSystemPicker = settings.isSystemAttachmentPickerEnabled),
),
allowUIAutomationTest = true,
messageComposerTheme = messageComposerTheme,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ class CustomLoginActivity : AppCompatActivity() {
var isComposerFloatingStyleEnabled by remember {
mutableStateOf(settings.isComposerFloatingStyleEnabled)
}
var isSystemAttachmentPickerEnabled by remember {
mutableStateOf(settings.isSystemAttachmentPickerEnabled)
}

val isLoginButtonEnabled = apiKeyText.isNotEmpty() &&
userIdText.isNotEmpty() &&
Expand All @@ -153,6 +156,17 @@ class CustomLoginActivity : AppCompatActivity() {
settings.isComposerFloatingStyleEnabled = it
},
),
FeatureFlag(
label = stringResource(R.string.custom_login_flag_system_attachment_picker_label),
description = stringResource(
R.string.custom_login_flag_system_attachment_picker_description,
),
value = isSystemAttachmentPickerEnabled,
onValueChange = {
isSystemAttachmentPickerEnabled = it
settings.isSystemAttachmentPickerEnabled = it
},
),
)

CustomLoginInputField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<string name="custom_login_flag_adaptive_layout_description">Adjust layout based on screen sizes</string>
<string name="custom_login_flag_composer_floating_style_label">Message composer floating style</string>
<string name="custom_login_flag_composer_floating_style_description">Message list scrolls behind some composer elements</string>
<string name="custom_login_flag_system_attachment_picker_label">System attachment picker</string>
<string name="custom_login_flag_system_attachment_picker_description">Use the system\'s native file/media picker</string>

<!-- Pinned Messages -->
<string name="pinned_messages_title">Pinned Messages</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4339,22 +4339,17 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/Attachme
public static final field $stable I
public fun <init> (Lio/getstream/chat/android/ui/common/helper/internal/AttachmentStorageHelper;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;)V
public synthetic fun <init> (Lio/getstream/chat/android/ui/common/helper/internal/AttachmentStorageHelper;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun clearSelection ()V
public final fun deselectAttachment (Lio/getstream/chat/android/models/Attachment;)V
public final fun getAttachments ()Ljava/util/List;
public final fun getAttachmentsFromMetadata (Ljava/util/List;)Ljava/util/List;
public final fun getChannel ()Lio/getstream/chat/android/models/Channel;
public final fun getPickerMode ()Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentPickerMode;
public final fun getSelectedAttachments ()Ljava/util/List;
public final fun getSubmittedAttachments ()Lkotlinx/coroutines/flow/Flow;
public final fun isPickerVisible ()Z
public final fun loadAttachments ()V
public final fun resolveAndSubmitUris (Ljava/util/List;)V
public final fun setPickerMode (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentPickerMode;)V
public final fun setPickerVisible (Z)V
public final fun togglePickerVisibility ()V
public final fun toggleSelection (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentPickerItemState;Z)V
public static synthetic fun toggleSelection$default (Lio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel;Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentPickerItemState;ZILjava/lang/Object;)V
}

public final class io/getstream/chat/android/compose/viewmodel/messages/AudioPlayerViewModel : androidx/lifecycle/ViewModel {
Expand All @@ -4372,12 +4367,13 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/AudioPla
public final class io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel : androidx/lifecycle/ViewModel {
public static final field $stable I
public fun <init> (Lio/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController;Lio/getstream/chat/android/ui/common/helper/internal/AttachmentStorageHelper;)V
public final fun addSelectedAttachments (Ljava/util/List;)V
public final fun addAttachments (Ljava/util/List;)V
public final fun buildNewMessage (Ljava/lang/String;Ljava/util/List;)Lio/getstream/chat/android/models/Message;
public static synthetic fun buildNewMessage$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/chat/android/models/Message;
public final fun cancelLinkPreview ()V
public final fun cancelRecording ()V
public final fun clearActiveCommand ()V
public final fun clearAttachments ()V
public final fun clearData ()V
public final fun completeRecording ()V
public final fun createPoll (Lio/getstream/chat/android/models/PollConfig;)V
Expand All @@ -4398,7 +4394,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageC
public final fun lockRecording ()V
public final fun pauseRecording ()V
public final fun performMessageAction (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;)V
public final fun removeSelectedAttachment (Lio/getstream/chat/android/models/Attachment;)V
public final fun removeAttachment (Lio/getstream/chat/android/models/Attachment;)V
public final fun seekRecordingTo (F)V
public final fun selectCommand (Lio/getstream/chat/android/models/Command;)V
public final fun selectMention (Lio/getstream/chat/android/models/User;)V
Expand All @@ -4414,7 +4410,6 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageC
public final fun stopRecording ()V
public final fun toggleCommandsVisibility ()V
public final fun toggleRecordingPlayback ()V
public final fun updateSelectedAttachments (Ljava/util/List;)V
}

public final class io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel : androidx/lifecycle/ViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.ReactionSorting
import io.getstream.chat.android.models.ReactionSortingByFirstReactionAt
import io.getstream.chat.android.models.User
import io.getstream.chat.android.ui.common.helper.internal.AttachmentStorageHelper.Companion.EXTRA_SOURCE_URI
import io.getstream.chat.android.ui.common.state.messages.Delete
import io.getstream.chat.android.ui.common.state.messages.Edit
import io.getstream.chat.android.ui.common.state.messages.Flag
Expand Down Expand Up @@ -350,8 +351,10 @@ internal fun DefaultBottomBarContent(
isAttachmentPickerVisible = attachmentsPickerViewModel.isPickerVisible,
onAttachmentsClick = attachmentsPickerViewModel::togglePickerVisibility,
onAttachmentRemoved = { attachment ->
composerViewModel.removeSelectedAttachment(attachment)
attachmentsPickerViewModel.deselectAttachment(attachment)
attachment.extraData[EXTRA_SOURCE_URI]
?.let { it as? String }
?.let(attachmentsPickerViewModel::removeFromSelection)
composerViewModel.removeAttachment(attachment)
},
onCancelAction = {
listViewModel.dismissAllMessageActions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import io.getstream.chat.android.compose.R
import io.getstream.chat.android.compose.state.messages.attachments.AttachmentPickerMode
import io.getstream.chat.android.compose.state.messages.attachments.CameraPickerMode
import io.getstream.chat.android.compose.state.messages.attachments.FilePickerMode
import io.getstream.chat.android.compose.state.messages.attachments.GalleryPickerMode
import io.getstream.chat.android.compose.state.messages.attachments.PollPickerMode
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel
import io.getstream.chat.android.ui.common.state.messages.MessageMode
Expand Down Expand Up @@ -70,7 +66,9 @@ public fun AttachmentPicker(
attachmentsPickerViewModel: AttachmentsPickerViewModel,
modifier: Modifier = Modifier,
messageMode: MessageMode = MessageMode.Normal,
actions: AttachmentPickerActions = AttachmentPickerActions.pickerDefaults(attachmentsPickerViewModel),
actions: AttachmentPickerActions = remember(attachmentsPickerViewModel) {
AttachmentPickerActions.pickerDefaults(attachmentsPickerViewModel)
},
) {
BackHandler(onBack = actions.onDismiss)

Expand Down Expand Up @@ -130,17 +128,3 @@ public fun AttachmentPicker(
}
}
}

/**
* A helper property to check if the current [AttachmentPickerMode] supports multiple selections.
*
* This will return:
* - `true` or `false` for [FilePickerMode] and [GalleryPickerMode], based on their `allowMultipleSelection` property.
* - `null` for other modes like [CameraPickerMode] or [PollPickerMode] that do not support this concept.
*/
internal val AttachmentPickerMode.allowMultipleSelection: Boolean?
get() = when (this) {
is FilePickerMode -> allowMultipleSelection
is GalleryPickerMode -> allowMultipleSelection
else -> null
}
Loading
Loading