Telegram-like composer: voice/circle toggle and unified attach actions
Some checks failed
Android CI / android (push) Failing after 5m23s
Android Release / release (push) Failing after 6m10s
CI / test (push) Failing after 3m11s

This commit is contained in:
2026-03-11 23:41:47 +03:00
parent 28cb80fbb8
commit b40dea18f1
3 changed files with 133 additions and 34 deletions

View File

@@ -82,6 +82,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -590,7 +591,9 @@ private fun ChatScreen(
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var showInlineSearch by remember { mutableStateOf(false) } var showInlineSearch by remember { mutableStateOf(false) }
var showEmojiPicker by remember { mutableStateOf(false) } var showEmojiPicker by remember { mutableStateOf(false) }
var showAttachmentSheet by remember { mutableStateOf(false) }
var emojiPickerTab by remember { mutableStateOf(ComposerPickerTab.Emoji) } var emojiPickerTab by remember { mutableStateOf(ComposerPickerTab.Emoji) }
var useCircleRecording by rememberSaveable(state.chatId) { mutableStateOf(false) }
var gifSearchQuery by remember { mutableStateOf("") } var gifSearchQuery by remember { mutableStateOf("") }
var giphySearchItems by remember { mutableStateOf<List<RemotePickerItem>>(emptyList()) } var giphySearchItems by remember { mutableStateOf<List<RemotePickerItem>>(emptyList()) }
var isGiphyLoading by remember { mutableStateOf(false) } var isGiphyLoading by remember { mutableStateOf(false) }
@@ -605,6 +608,7 @@ private fun ChatScreen(
val actionSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val actionSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val forwardSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val forwardSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val chatInfoSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val chatInfoSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val attachmentSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
val adaptiveHorizontalPadding = if (isTabletLayout) 72.dp else 0.dp val adaptiveHorizontalPadding = if (isTabletLayout) 72.dp else 0.dp
val chatInfoEntries = remember(state.messages, context) { buildChatInfoEntries(state.messages, context) } val chatInfoEntries = remember(state.messages, context) { buildChatInfoEntries(state.messages, context) }
@@ -1769,40 +1773,7 @@ private fun ChatScreen(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), color = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f),
) { ) {
IconButton( IconButton(
onClick = onCapturePhoto, onClick = { showAttachmentSheet = true },
enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice,
) {
Icon(imageVector = Icons.Filled.AddAPhoto, contentDescription = "Capture photo")
}
}
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f),
) {
IconButton(
onClick = onCaptureVideo,
enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice,
) {
Icon(imageVector = Icons.Filled.Movie, contentDescription = "Capture video")
}
}
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f),
) {
IconButton(
onClick = onCaptureCircleVideo,
enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice,
) {
Icon(imageVector = Icons.Filled.RadioButtonChecked, contentDescription = "Record circle video")
}
}
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f),
) {
IconButton(
onClick = onPickMedia,
enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice, enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice,
) { ) {
if (state.isUploadingMedia) { if (state.isUploadingMedia) {
@@ -1812,6 +1783,28 @@ private fun ChatScreen(
} }
} }
} }
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f),
) {
IconButton(
onClick = { useCircleRecording = !useCircleRecording },
enabled = state.canSendMessages && !state.isUploadingMedia && !state.isRecordingVoice,
) {
Icon(
imageVector = if (useCircleRecording) {
Icons.Filled.RadioButtonChecked
} else {
Icons.Filled.Mic
},
contentDescription = if (useCircleRecording) {
"Circle mode enabled"
} else {
"Voice mode enabled"
},
)
}
}
val canSend = state.canSendMessages && val canSend = state.canSendMessages &&
!state.isSending && !state.isSending &&
!state.isUploadingMedia && !state.isUploadingMedia &&
@@ -1828,6 +1821,21 @@ private fun ChatScreen(
Icon(imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = "Send") Icon(imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
} }
} }
} else if (useCircleRecording) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.18f),
) {
IconButton(
onClick = onCaptureCircleVideo,
enabled = state.canSendMessages && !state.isUploadingMedia,
) {
Icon(
imageVector = Icons.Filled.RadioButtonChecked,
contentDescription = "Record circle video",
)
}
}
} else { } else {
VoiceHoldToRecordButton( VoiceHoldToRecordButton(
enabled = state.canSendMessages && !state.isUploadingMedia, enabled = state.canSendMessages && !state.isUploadingMedia,
@@ -1843,6 +1851,89 @@ private fun ChatScreen(
} }
} }
if (showAttachmentSheet) {
ModalBottomSheet(
onDismissRequest = { showAttachmentSheet = false },
sheetState = attachmentSheetState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = stringResource(id = R.string.chat_attachment_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable {
showAttachmentSheet = false
onPickMedia()
},
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.AttachFile, contentDescription = null)
Text(stringResource(id = R.string.chat_attachment_gallery_file))
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable {
showAttachmentSheet = false
onCapturePhoto()
},
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.AddAPhoto, contentDescription = null)
Text(stringResource(id = R.string.chat_attachment_take_photo))
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable {
showAttachmentSheet = false
onCaptureVideo()
},
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.Movie, contentDescription = null)
Text(stringResource(id = R.string.chat_attachment_take_video))
}
}
}
}
}
if (!state.errorMessage.isNullOrBlank()) { if (!state.errorMessage.isNullOrBlank()) {
Text( Text(
text = state.errorMessage, text = state.errorMessage,

View File

@@ -147,6 +147,10 @@
<string name="chat_member_action_kick">Кикнуть</string> <string name="chat_member_action_kick">Кикнуть</string>
<string name="chat_member_action_unban">Разбанить</string> <string name="chat_member_action_unban">Разбанить</string>
<string name="chat_media_badge_video">Видео</string> <string name="chat_media_badge_video">Видео</string>
<string name="chat_attachment_title">Вложение</string>
<string name="chat_attachment_gallery_file">Галерея / Файл</string>
<string name="chat_attachment_take_photo">Сделать фото</string>
<string name="chat_attachment_take_video">Снять видео</string>
<string name="chat_media_badge_gif">GIF</string> <string name="chat_media_badge_gif">GIF</string>
<string name="chat_media_badge_sticker">Стикер</string> <string name="chat_media_badge_sticker">Стикер</string>
<string name="chat_picker_tab_emoji">Эмодзи</string> <string name="chat_picker_tab_emoji">Эмодзи</string>

View File

@@ -147,6 +147,10 @@
<string name="chat_member_action_kick">Kick</string> <string name="chat_member_action_kick">Kick</string>
<string name="chat_member_action_unban">Unban</string> <string name="chat_member_action_unban">Unban</string>
<string name="chat_media_badge_video">Video</string> <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_take_photo">Take photo</string>
<string name="chat_attachment_take_video">Record video</string>
<string name="chat_media_badge_gif">GIF</string> <string name="chat_media_badge_gif">GIF</string>
<string name="chat_media_badge_sticker">Sticker</string> <string name="chat_media_badge_sticker">Sticker</string>
<string name="chat_picker_tab_emoji">Emoji</string> <string name="chat_picker_tab_emoji">Emoji</string>