feat: improve media picking and attachment previews
Show images and GIFs fully inside chat bubbles instead of cropping them. Split attachment flow into a visual picker for photos and videos and a separate system file picker. Add batch visual sending with a shared caption applied to the first item until album support exists.
This commit is contained in:
@@ -16,6 +16,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -320,8 +321,16 @@ fun ChatRoute(
|
||||
AppAudioFocusCoordinator.release("voice-recording")
|
||||
}
|
||||
}
|
||||
val pickMediaLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
val pickVisualMediaLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = 10),
|
||||
) { uris ->
|
||||
val picked = uris.mapNotNull { it.readMediaPayload(context) }
|
||||
if (picked.isNotEmpty()) {
|
||||
viewModel.onMediaBatchPicked(picked)
|
||||
}
|
||||
}
|
||||
val pickFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
) { uri ->
|
||||
val picked = uri?.readMediaPayload(context) ?: return@rememberLauncherForActivityResult
|
||||
viewModel.onMediaPicked(
|
||||
@@ -351,7 +360,14 @@ fun ChatRoute(
|
||||
onToggleReaction = viewModel::onToggleReaction,
|
||||
onCancelComposeAction = viewModel::onCancelComposeAction,
|
||||
onLoadMore = viewModel::loadMore,
|
||||
onPickMedia = { pickMediaLauncher.launch("*/*") },
|
||||
onPickVisualMedia = {
|
||||
pickVisualMediaLauncher.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo),
|
||||
)
|
||||
},
|
||||
onPickFile = {
|
||||
pickFileLauncher.launch(arrayOf("*/*"))
|
||||
},
|
||||
onCapturePhoto = {
|
||||
if (hasCameraPermission) {
|
||||
cameraCaptureMode = CameraCaptureMode.Photo
|
||||
@@ -517,7 +533,8 @@ private data class ChatScreenActions(
|
||||
val onToggleReaction: (String) -> Unit,
|
||||
val onCancelComposeAction: () -> Unit,
|
||||
val onLoadMore: () -> Unit,
|
||||
val onPickMedia: () -> Unit,
|
||||
val onPickVisualMedia: () -> Unit,
|
||||
val onPickFile: () -> Unit,
|
||||
val onCapturePhoto: () -> Unit,
|
||||
val onCaptureVideo: () -> Unit,
|
||||
val onCaptureCircleVideo: () -> Unit,
|
||||
@@ -571,7 +588,8 @@ private fun ChatScreen(
|
||||
val onToggleReaction = actions.onToggleReaction
|
||||
val onCancelComposeAction = actions.onCancelComposeAction
|
||||
val onLoadMore = actions.onLoadMore
|
||||
val onPickMedia = actions.onPickMedia
|
||||
val onPickVisualMedia = actions.onPickVisualMedia
|
||||
val onPickFile = actions.onPickFile
|
||||
val onCapturePhoto = actions.onCapturePhoto
|
||||
val onCaptureVideo = actions.onCaptureVideo
|
||||
val onCaptureCircleVideo = actions.onCaptureCircleVideo
|
||||
@@ -2082,7 +2100,28 @@ private fun ChatScreen(
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable {
|
||||
showAttachmentSheet = false
|
||||
onPickMedia()
|
||||
onPickVisualMedia()
|
||||
},
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Image, contentDescription = null)
|
||||
Text(stringResource(id = R.string.chat_attachment_photos_videos))
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable {
|
||||
showAttachmentSheet = false
|
||||
onPickFile()
|
||||
},
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f),
|
||||
) {
|
||||
@@ -2094,7 +2133,7 @@ private fun ChatScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.AttachFile, contentDescription = null)
|
||||
Text(stringResource(id = R.string.chat_attachment_gallery_file))
|
||||
Text(stringResource(id = R.string.chat_attachment_files))
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
@@ -2963,7 +3002,7 @@ private fun EmojiPickerSheet(
|
||||
}
|
||||
}
|
||||
|
||||
private data class PickedMediaPayload(
|
||||
data class PickedMediaPayload(
|
||||
val fileName: String,
|
||||
val mimeType: String,
|
||||
val bytes: ByteArray,
|
||||
@@ -3926,11 +3965,16 @@ private fun MessageBubble(
|
||||
}
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)),
|
||||
)
|
||||
AsyncImage(
|
||||
model = textUrl,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = if (isLegacyStickerMessage) ContentScale.Fit else ContentScale.Crop,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
if (!badgeLabel.isNullOrBlank()) {
|
||||
MediaTypeBadge(
|
||||
@@ -4022,11 +4066,16 @@ private fun MessageBubble(
|
||||
if (openable) base.clickable { onAttachmentImageClick(single.fileUrl) } else base
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)),
|
||||
)
|
||||
AsyncImage(
|
||||
model = single.fileUrl,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = if (isStickerImage) ContentScale.Fit else ContentScale.Crop,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
if (!badgeLabel.isNullOrBlank()) {
|
||||
MediaTypeBadge(
|
||||
@@ -4059,11 +4108,16 @@ private fun MessageBubble(
|
||||
if (openable) base.clickable { onAttachmentImageClick(image.fileUrl) } else base
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)),
|
||||
)
|
||||
AsyncImage(
|
||||
model = image.fileUrl,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
if (!badgeLabel.isNullOrBlank()) {
|
||||
MediaTypeBadge(
|
||||
|
||||
@@ -662,6 +662,46 @@ class ChatViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onMediaBatchPicked(items: List<PickedMediaPayload>) {
|
||||
if (items.isEmpty()) return
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isUploadingMedia = true, errorMessage = null) }
|
||||
val caption = uiState.value.inputText.trim().ifBlank { null }
|
||||
val replyToMessageId = uiState.value.replyToMessage?.id
|
||||
items.forEachIndexed { index, item ->
|
||||
when (
|
||||
val result = sendMediaMessageUseCase(
|
||||
chatId = chatId,
|
||||
fileName = item.fileName,
|
||||
mimeType = item.mimeType,
|
||||
bytes = item.bytes,
|
||||
caption = if (index == 0) caption else null,
|
||||
replyToMessageId = if (index == 0) replyToMessageId else null,
|
||||
)
|
||||
) {
|
||||
is AppResult.Success -> Unit
|
||||
is AppResult.Error -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUploadingMedia = false,
|
||||
errorMessage = result.reason.toUiMessage(),
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUploadingMedia = false,
|
||||
inputText = "",
|
||||
replyToMessage = null,
|
||||
editingMessage = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSendPresetMediaUrl(url: String) {
|
||||
val normalizedUrl = url.trim()
|
||||
if (normalizedUrl.isBlank()) return
|
||||
|
||||
@@ -164,6 +164,8 @@
|
||||
<string name="chat_media_badge_video">Видео</string>
|
||||
<string name="chat_attachment_title">Вложение</string>
|
||||
<string name="chat_attachment_gallery_file">Галерея / Файл</string>
|
||||
<string name="chat_attachment_photos_videos">Фото и видео</string>
|
||||
<string name="chat_attachment_files">Файлы</string>
|
||||
<string name="chat_attachment_take_photo">Сделать фото</string>
|
||||
<string name="chat_attachment_take_video">Снять видео</string>
|
||||
<string name="chat_media_badge_gif">GIF</string>
|
||||
|
||||
@@ -164,6 +164,8 @@
|
||||
<string name="chat_media_badge_video">Video</string>
|
||||
<string name="chat_attachment_title">Attachment</string>
|
||||
<string name="chat_attachment_gallery_file">Gallery / File</string>
|
||||
<string name="chat_attachment_photos_videos">Photos & videos</string>
|
||||
<string name="chat_attachment_files">Files</string>
|
||||
<string name="chat_attachment_take_photo">Take photo</string>
|
||||
<string name="chat_attachment_take_video">Record video</string>
|
||||
<string name="chat_media_badge_gif">GIF</string>
|
||||
|
||||
Reference in New Issue
Block a user