Browse Source

Keep Android media preparation status scoped to its conversation

master
evgeny 1 day ago
parent
commit
79f0f2a07f
  1. 7
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationComposer.kt
  2. 25
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

7
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 = {

25
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<String> = emptySet(), val failed: Boolean = false)
private val _attachmentPreparations = MutableStateFlow<Map<String, AttachmentPreparation>>(emptyMap())
val attachmentPreparations: StateFlow<Map<String, AttachmentPreparation>> = _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

Loading…
Cancel
Save