Skip to content

Commit 930e090

Browse files
authored
Message composer attachments improvements (part 1) (#6175)
* Remove `selectedAttachments` `MutableStateFlow` from `MessageComposerController`. The list of attachments is now managed directly within the `MessageComposerState`. This change centralizes state management, removing the need to separately observe `selectedAttachments` and update the main state. All functions that previously interacted with `selectedAttachments` now update the `attachments` list within the `MessageComposerState`. * Remove legacy saveAttachmentsOnDismiss flow * Introduce AttachmentPickerActions for picker events Replaced the `AttachmentPickerAction` sealed interface with a dedicated `AttachmentPickerActions` data class. This change centralizes all attachment picker event handlers into a single, cohesive class, improving the API's clarity and simplifying its usage. Key changes: - Introduced the `AttachmentPickerActions` data class to hold all picker-related callbacks. - Added default factory methods (`defaultActions`, `pickerDefaults`) for easy instantiation. - Removed the now-obsolete `AttachmentPickerAction` sealed interface and its implementations. - Updated `AttachmentPicker`, `AttachmentPickerContent`, and related components to accept the new `AttachmentPickerActions` class instead of multiple lambda parameters. * Refactor attachment handling to `AttachmentStorageHelper` The `StorageHelperWrapper` has been replaced by `AttachmentStorageHelper` and moved from the Compose module to the `ui-common` module. This refactoring separates concerns more clearly: `AttachmentStorageHelper` now manages converting between `AttachmentMetaData` and the `Attachment` model, while the lower-level `StorageHelper` continues to handle direct interactions with the device's storage. This change also introduces a deferred file resolution mechanism. Attachments are now created without an `upload` file first and are resolved just before sending. This is more efficient as it avoids unnecessary file copying until the user confirms the action. * Replace `StorageHelper` with `AttachmentStorageHelper` in ui-components module * Replace `StorageHelper` with `AttachmentStorageHelper` in compose module * Refactor attachment handling to `AttachmentStorageHelper` The `StorageHelperWrapper` has been replaced by `AttachmentStorageHelper` and moved from the Compose module to the `ui-common` module. This refactoring separates concerns more clearly: `AttachmentStorageHelper` now manages converting between `AttachmentMetaData` and the `Attachment` model, while the lower-level `StorageHelper` continues to handle direct interactions with the device's storage. This change also introduces a deferred file resolution mechanism. Attachments are now created without an `upload` file first and are resolved just before sending. This is more efficient as it avoids unnecessary file copying until the user confirms the action. * Resolve deferred attachments before sending message * Refactor attachment processing out of ViewModels. Leaf composables emit events upward via simple callbacks, the parent wires them to ViewModel operations, and data flows back down through Compose state. * Use stable keys for attachment previews The `LazyRow`s for media and file attachment previews now use stable keys, derived from the attachment's source URI or file path. This improves performance and prevents recomposition issues when the list of attachments changes. * Animate composer attachment preview items * Update attachment picker selection by replacing the `AttachmentPickerItemState.Selection` sealed interface with a simple `isSelected` boolean. This change simplifies the state management for both single and multiple selection modes. Key changes: - Replaced `Selection.Selected(position)` and `Selection.Unselected` with a `isSelected` boolean in `AttachmentPickerItemState`. - Removed the `allowMultipleSelection` parameter from `ImagesPicker`, as it is no longer needed. - The selected item indicator now consistently displays a checkmark, removing the numeric position badge. - The `AttachmentsPickerViewModel` logic has been updated to work with the new `isSelected` state, simplifying selection, deselection, and cross-tab state synchronization. * Auto-scroll attachment previews on new item * detekt * Update location picker icon * Refactor AttachmentsPickerViewModel API This change refactors the public API of `AttachmentsPickerViewModel` to improve clarity and consistency. Key changes include: - Renamed `isShowingAttachments` to `isPickerVisible`. - Renamed `changeAttachmentState()` to `setPickerVisible()`. - Renamed `toggleAttachmentState()` to `togglePickerVisibility()`. - Renamed `changePickerMode()` to `setPickerMode()`. - Renamed `changeSelectedAttachments()` to `toggleSelection()`. - Renamed `removeSelectedAttachment()` to `deselectAttachment()`. - Renamed `getAttachmentsFromMetaData()` to `getAttachmentsFromMetadata()`. * Keep attachment selection after closing the picker. Clear only after sending a message. * Fixes after merge * Revert `rememberCaptureMediaLauncher` exclusion * typo * fix kdoc * Drop attachments that fail to resolve * Persist attachment picker state The attachment picker's visibility and active tab are now persisted using `SavedStateHandle`. This ensures the UI state is restored after process death, such as when returning from a system file picker with "Don't keep activities" enabled. * Refactor `submittedAttachments` to use `Channel` Convert the `submittedAttachments` `SharedFlow` into a `Channel`. This change ensures that attachment submission events are retained until they are consumed. * Persist camera capture file paths The file paths for photos and videos captured with the camera are now saved and restored. This allows the captured media to be recovered after process death. Fix camera picker launching multiple times * Only call `onAttachmentsSelected` when attachments are not empty * merge AttachmentPickerActions initialization * No need toMutableList * Remove `EXTRA_SOURCE_URI` after resolving attachment * Enforce attachment selection order * Cancel previous attachment loading job * Refactor `sendMessage` to resolve attachments internally * Fix detekt issues
1 parent 510f706 commit 930e090

60 files changed

Lines changed: 1810 additions & 1909 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ class MessagesActivity : ComponentActivity() {
359359
)
360360
},
361361
trailingContent = { Spacer(modifier = Modifier.size(8.dp)) },
362-
onAttachmentsClick = attachmentsPickerViewModel::toggleAttachmentState,
362+
onAttachmentsClick = attachmentsPickerViewModel::togglePickerVisibility,
363363
)
364364
}
365365

stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/location/LocationComponentFactory.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
package io.getstream.chat.android.compose.sample.ui.location
1818

19+
import android.net.Uri
1920
import androidx.compose.foundation.layout.RowScope
2021
import androidx.compose.foundation.layout.size
2122
import androidx.compose.foundation.layout.widthIn
2223
import androidx.compose.material.icons.Icons
23-
import androidx.compose.material.icons.rounded.ShareLocation
24+
import androidx.compose.material.icons.outlined.LocationOn
2425
import androidx.compose.material3.FilledIconToggleButton
2526
import androidx.compose.material3.Icon
2627
import androidx.compose.material3.IconButtonDefaults
@@ -36,8 +37,7 @@ import io.getstream.chat.android.compose.sample.vm.SharedLocationViewModelFactor
3637
import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult
3738
import io.getstream.chat.android.compose.state.messages.attachments.AttachmentPickerItemState
3839
import io.getstream.chat.android.compose.state.messages.attachments.AttachmentPickerMode
39-
import io.getstream.chat.android.compose.ui.messages.attachments.factory.AttachmentPickerAction
40-
import io.getstream.chat.android.compose.ui.messages.attachments.factory.AttachmentPickerBack
40+
import io.getstream.chat.android.compose.ui.messages.attachments.AttachmentPickerActions
4141
import io.getstream.chat.android.compose.ui.theme.ChatComponentFactory
4242
import io.getstream.chat.android.compose.ui.theme.ChatTheme
4343
import io.getstream.chat.android.models.Channel
@@ -84,7 +84,7 @@ class LocationComponentFactory(
8484
),
8585
) {
8686
Icon(
87-
imageVector = Icons.Rounded.ShareLocation,
87+
imageVector = Icons.Outlined.LocationOn,
8888
contentDescription = "Share Location",
8989
)
9090
}
@@ -96,24 +96,24 @@ class LocationComponentFactory(
9696
pickerMode: AttachmentPickerMode?,
9797
commands: List<Command>,
9898
attachments: List<AttachmentPickerItemState>,
99-
onAttachmentsChanged: (List<AttachmentPickerItemState>) -> Unit,
100-
onAttachmentItemSelected: (AttachmentPickerItemState) -> Unit,
101-
onAttachmentPickerAction: (AttachmentPickerAction) -> Unit,
99+
onLoadAttachments: () -> Unit,
100+
onUrisSelected: (List<Uri>) -> Unit,
101+
actions: AttachmentPickerActions,
102102
onAttachmentsSubmitted: (List<AttachmentMetaData>) -> Unit,
103103
) {
104104
if (pickerMode == LocationPickerMode && locationViewModelFactory != null) {
105105
LocationPicker(
106106
viewModelFactory = locationViewModelFactory,
107-
onDismiss = { onAttachmentPickerAction(AttachmentPickerBack) },
107+
onDismiss = actions.onDismiss,
108108
)
109109
} else {
110110
super.AttachmentPickerContent(
111111
pickerMode,
112112
commands,
113113
attachments,
114-
onAttachmentsChanged,
115-
onAttachmentItemSelected,
116-
onAttachmentPickerAction,
114+
onLoadAttachments,
115+
onUrisSelected,
116+
actions,
117117
onAttachmentsSubmitted,
118118
)
119119
}

stream-chat-android-compose/api/stream-chat-android-compose.api

Lines changed: 86 additions & 119 deletions
Large diffs are not rendered by default.

stream-chat-android-compose/src/main/baseline-prof.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2302,7 +2302,7 @@ HSPLio/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory
23022302
HSPLio/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object;
23032303
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel;-><clinit>()V
23042304
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel;-><init>(Lio/getstream/chat/android/compose/ui/util/StorageHelperWrapper;Lkotlinx/coroutines/flow/StateFlow;)V
2305-
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel;->isShowingAttachments()Z
2305+
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel;->isPickerVisible()Z
23062306
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel$special$$inlined$map$1;-><init>(Lkotlinx/coroutines/flow/Flow;)V
23072307
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
23082308
PLio/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel$special$$inlined$map$1$2;-><init>(Lkotlinx/coroutines/flow/FlowCollector;)V

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentPickerItemState.kt

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,37 +26,9 @@ import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMet
2626
*
2727
* @param attachmentMetaData The metadata describing the attachment, including its URI, file name,
2828
* size, MIME type, and other relevant information.
29-
* @param selection The current selection state of the item. See [Selection] for possible states.
29+
* @param isSelected Whether this item is currently selected.
3030
*/
3131
public data class AttachmentPickerItemState(
3232
val attachmentMetaData: AttachmentMetaData,
33-
val selection: Selection = Selection.Unselected,
34-
) {
35-
36-
/**
37-
* Represents the selection state of an attachment picker item.
38-
*/
39-
public sealed interface Selection {
40-
41-
/**
42-
* Indicates that the attachment is selected.
43-
*
44-
* @param position The 1-based position of this attachment in the selection order.
45-
* This is displayed as a badge on the item to show the selection order when
46-
* multiple attachments are selected.
47-
*/
48-
public data class Selected(val position: Int) : Selection
49-
50-
/**
51-
* Indicates that the attachment is not selected.
52-
*/
53-
public data object Unselected : Selection
54-
}
55-
56-
/**
57-
* Convenience property to check if this item is currently selected.
58-
*
59-
* @return `true` if the item is selected, `false` otherwise.
60-
*/
61-
public val isSelected: Boolean get() = selection is Selection.Selected
62-
}
33+
val isSelected: Boolean = false,
34+
)

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentPreviewContent.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import androidx.compose.ui.unit.dp
3939
import io.getstream.chat.android.compose.ui.components.ComposerCancelIcon
4040
import io.getstream.chat.android.compose.ui.components.composer.MessageInput
4141
import io.getstream.chat.android.compose.ui.theme.ChatTheme
42+
import io.getstream.chat.android.compose.ui.util.extensions.internal.stableKey
43+
import io.getstream.chat.android.compose.ui.util.rememberAutoScrollLazyListState
4244
import io.getstream.chat.android.models.Attachment
4345
import io.getstream.chat.android.ui.common.utils.MediaStringUtil
4446

@@ -57,16 +59,22 @@ public fun FileAttachmentPreviewContent(
5759
modifier: Modifier = Modifier,
5860
) {
5961
LazyRow(
62+
state = rememberAutoScrollLazyListState(attachments.size),
6063
modifier = modifier
6164
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
6265
.testTag("Stream_FileAttachmentPreviewContent"),
6366
verticalAlignment = Alignment.CenterVertically,
6467
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start),
6568
contentPadding = PaddingValues(12.dp),
6669
) {
67-
items(attachments) { attachment ->
70+
items(
71+
items = attachments,
72+
key = Attachment::stableKey,
73+
) { attachment ->
6874
Surface(
69-
modifier = Modifier.padding(1.dp),
75+
modifier = Modifier
76+
.animateItem()
77+
.padding(1.dp),
7078
color = ChatTheme.colors.appBackground,
7179
shape = RoundedCornerShape(16.dp),
7280
border = BorderStroke(1.dp, ChatTheme.colors.borders),
@@ -98,9 +106,9 @@ public fun FileAttachmentPreviewContent(
98106
color = ChatTheme.colors.textHighEmphasis,
99107
)
100108

101-
val fileSize = attachment.upload?.length()?.let { length ->
102-
MediaStringUtil.convertFileSizeByteCount(length)
103-
}
109+
val fileSize = attachment.fileSize
110+
.takeIf { it > 0 }
111+
?.let { MediaStringUtil.convertFileSizeByteCount(it.toLong()) }
104112
if (fileSize != null) {
105113
Text(
106114
modifier = Modifier.testTag("Stream_FileSizeInPreview"),

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme
4545
import io.getstream.chat.android.compose.ui.theme.StreamTokens
4646
import io.getstream.chat.android.compose.ui.util.AsyncImagePreviewHandler
4747
import io.getstream.chat.android.compose.ui.util.StreamAsyncImage
48+
import io.getstream.chat.android.compose.ui.util.extensions.internal.localPreviewData
49+
import io.getstream.chat.android.compose.ui.util.extensions.internal.stableKey
50+
import io.getstream.chat.android.compose.ui.util.rememberAutoScrollLazyListState
4851
import io.getstream.chat.android.models.Attachment
4952
import io.getstream.chat.android.models.AttachmentType
50-
import io.getstream.chat.android.ui.common.utils.extensions.imagePreviewUrl
5153

5254
/**
5355
* UI for currently selected image and video attachments, within the [MessageInput].
@@ -70,16 +72,21 @@ public fun MediaAttachmentPreviewContent(
7072
},
7173
) {
7274
LazyRow(
75+
state = rememberAutoScrollLazyListState(attachments.size),
7376
modifier = modifier
7477
.clip(ChatTheme.shapes.attachment)
7578
.testTag("Stream_MediaAttachmentPreviewContent"),
7679
verticalAlignment = Alignment.CenterVertically,
7780
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs, Alignment.Start),
7881
contentPadding = PaddingValues(horizontal = StreamTokens.spacingSm),
7982
) {
80-
items(attachments) { image ->
83+
items(
84+
items = attachments,
85+
key = Attachment::stableKey,
86+
) { attachment ->
8187
MediaAttachmentPreviewItem(
82-
mediaAttachment = image,
88+
modifier = Modifier.animateItem(),
89+
mediaAttachment = attachment,
8390
onAttachmentRemoved = onAttachmentRemoved,
8491
overlayContent = previewItemOverlayContent,
8592
)
@@ -100,11 +107,12 @@ private fun MediaAttachmentPreviewItem(
100107
mediaAttachment: Attachment,
101108
onAttachmentRemoved: (Attachment) -> Unit,
102109
overlayContent: @Composable (attachmentType: String?) -> Unit,
110+
modifier: Modifier = Modifier,
103111
) {
104-
val data = mediaAttachment.upload ?: mediaAttachment.imagePreviewUrl
112+
val data = mediaAttachment.localPreviewData
105113

106114
Box(
107-
modifier = Modifier
115+
modifier = modifier
108116
.size(95.dp)
109117
.clip(ChatTheme.shapes.attachment)
110118
.testTag("Stream_MediaAttachmentPreviewItem"),

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ private fun DefaultDetailBottomBarContent(viewModelFactory: MessagesViewModelFac
726726

727727
MessageComposer(
728728
viewModel = composerViewModel,
729-
onAttachmentsClick = { attachmentsPickerViewModel.toggleAttachmentState() },
729+
onAttachmentsClick = { attachmentsPickerViewModel.togglePickerVisibility() },
730730
onCancelAction = {
731731
listViewModel.dismissAllMessageActions()
732732
composerViewModel.dismissMessageActions()

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import androidx.compose.ui.tooling.preview.Preview
4646
import androidx.compose.ui.unit.dp
4747
import io.getstream.chat.android.compose.R
4848
import io.getstream.chat.android.compose.state.messages.attachments.AttachmentPickerItemState
49-
import io.getstream.chat.android.compose.state.messages.attachments.AttachmentPickerItemState.Selection
5049
import io.getstream.chat.android.compose.ui.theme.ChatTheme
5150
import io.getstream.chat.android.compose.ui.util.clickable
5251
import io.getstream.chat.android.ui.common.contract.internal.SelectFilesContract
@@ -215,7 +214,7 @@ internal fun FilesPickerSingleSelection() {
215214
attachmentMetaData = AttachmentMetaData(mimeType = MimeType.MIME_TYPE_DOC).apply {
216215
size = 100_000
217216
},
218-
selection = Selection.Selected(position = 1),
217+
isSelected = true,
219218
),
220219
),
221220
onItemSelected = {},
@@ -241,13 +240,13 @@ internal fun FilesPickerMultipleSelection() {
241240
attachmentMetaData = AttachmentMetaData(mimeType = MimeType.MIME_TYPE_PDF).apply {
242241
size = 10_000
243242
},
244-
selection = Selection.Selected(position = 1),
243+
isSelected = true,
245244
),
246245
AttachmentPickerItemState(
247246
attachmentMetaData = AttachmentMetaData(mimeType = MimeType.MIME_TYPE_DOC).apply {
248247
size = 100_000
249248
},
250-
selection = Selection.Selected(position = 2),
249+
isSelected = true,
251250
),
252251
),
253252
onItemSelected = {},

0 commit comments

Comments
 (0)