diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationComposer.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationComposer.kt index e2452d8e..516181b4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationComposer.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationComposer.kt @@ -41,6 +41,9 @@ fun ConversationComposer(target: ConversationTarget, viewModel: ChatViewModel, o val duration by viewModel.recordingDurationMs.collectAsState() val videoMode by viewModel.isVideoMode.collectAsState() var level by remember { mutableStateOf(0f) } + val preparations by viewModel.attachmentPreparations.collectAsState() + val preparation = preparations[target.nativeId] + DisposableEffect(target) { onDispose { viewModel.cancelVoiceRecording(target) } } LaunchedEffect(recording) { while (recording) { level = NativeLib.voiceGetPeakLevel(); delay(80) } level = 0f @@ -64,7 +67,9 @@ fun ConversationComposer(target: ConversationTarget, viewModel: ChatViewModel, o } Column { if (error.isNotEmpty()) Text(error, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(8.dp)) - if (preparing) Text("Подготовка вложения…", modifier = Modifier.padding(8.dp)) + if (preparing || preparation?.jobs?.isNotEmpty() == true) Text("Подготовка медиа…", modifier = Modifier.padding(8.dp)) + else if (preparation?.failed == true) Text("Не удалось подготовить вложение", color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(8.dp)) InputBar( text = text, onTextChange = { text = it }, enabled = !preparing, onSend = { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index da1d52c3..f65215cb 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -23,6 +23,18 @@ import java.io.FileOutputStream private val IMAGE_EXTENSIONS = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp") class ChatViewModel : ViewModel() { + data class AttachmentPreparation(val jobs: Set = emptySet(), val failed: Boolean = false) + private val _attachmentPreparations = MutableStateFlow>(emptyMap()) + val attachmentPreparations: StateFlow> = _attachmentPreparations + + private fun preparationState(target: String, job: String, state: Int) { + _attachmentPreparations.update { map -> + val previous = map[target] ?: AttachmentPreparation() + val value = if (state == 0) AttachmentPreparation(previous.jobs + job) + else previous.copy(jobs = previous.jobs - job, failed = previous.failed || state == 2) + if (value.jobs.isEmpty() && !value.failed) map - target else map + (target to value) + } + } data class PlaybackState( val filePath: String? = null, @@ -525,6 +537,15 @@ class ChatViewModel : ViewModel() { if (data == null) return _dmConversations.value = repo?.parseDmConversations(String(data, Charsets.UTF_8)) ?: emptyList() } + 46 -> { + if (data == null || data.size < 18) return + val length = data[0].toInt() and 255 + val state = data.last().toInt() and 255 + if (data.size != length + 18 || state > 2) { LogManager.addLog("ERROR", "VM", "invalid preparation event"); return } + val target = String(data, 1, length, Charsets.UTF_8) + val job = android.util.Base64.encodeToString(data.copyOfRange(1 + length, 17 + length), android.util.Base64.NO_WRAP) + preparationState(target, job, state) + } 43 -> { /* RADIO_TALK: [group_id:8][src:8][stream:2][on:1] */ if (data == null || data.size < 19) return val bb = java.nio.ByteBuffer.wrap(data, 0, 19).order(java.nio.ByteOrder.LITTLE_ENDIAN) @@ -985,8 +1006,8 @@ class ChatViewModel : ViewModel() { } } - fun cancelVoiceRecording() { - if (recordingStopping) return + fun cancelVoiceRecording(target: ConversationTarget? = null) { + if (recordingStopping || !_isRecording.value || (target != null && target != recordingTarget)) return recordingStopping = true _isRecording.value = false _isRecordingLocked.value = false