Browse Source

Add Android PM attachment selection, saving and delivery labels

master
evgeny 1 day ago
parent
commit
dc1110af4d
  1. 1
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  2. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  3. 72
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/DmChatScreen.kt

1
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -325,6 +325,7 @@ class MainActivity : ComponentActivity() {
conv = dm,
messages = dmMessages,
onSendMessage = { vm.sendDmMessage(it) },
onSendFile = { conv, path, name -> vm.sendDmFile(conv, path, name) },
onBack = { vm.closeDm(); screen = Screen.ChannelList }
)
}

2
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt

@ -137,7 +137,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
if (!isVoice) {
Spacer(Modifier.height(2.dp))
Text(
formatTime(message.ts),
listOf(formatTime(message.ts), message.deliveryStatus).filter { it.isNotEmpty() }.joinToString(" · "),
fontSize = 11.sp,
color = ChatColors.Secondary,
modifier = Modifier.align(Alignment.End)

72
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/DmChatScreen.kt

@ -1,5 +1,9 @@
package com.utun.chat.ui.screens
import android.provider.OpenableColumns
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -16,6 +20,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -28,12 +33,18 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.utun.chat.data.DmConversation
import com.utun.chat.data.Message
import com.utun.chat.ui.components.MessageBubble
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -41,9 +52,61 @@ fun DmChatScreen(
conv: DmConversation,
messages: List<Message>,
onSendMessage: (String) -> Unit,
onSendFile: (String, String, String) -> Unit,
onBack: () -> Unit
) {
var text by remember { mutableStateOf("") }
var text by remember(conv.convId) { mutableStateOf("") }
var fileError by remember(conv.convId) { mutableStateOf("") }
var preparing by remember(conv.convId) { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
var savePath by remember { mutableStateOf<String?>(null) }
val saveFile = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri ->
val path = savePath
savePath = null
if (uri != null && path != null) scope.launch {
try {
withContext(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use { output -> File(path).inputStream().use { it.copyTo(output) } }
?: error("Не удалось открыть файл для записи")
}
} catch (e: Exception) { Log.e("utun-gui", "PM file save failed", e); fileError = "Не удалось сохранить файл" }
}
}
val attachFile = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
val targetConv = conv.convId
scope.launch {
preparing = true; fileError = ""
try {
val attachment = withContext(Dispatchers.IO) {
val name = context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use {
if (it.moveToFirst()) it.getString(0) else null
} ?: "attachment.bin"
val temp = File.createTempFile("pm-upload-", ".file", context.cacheDir)
try {
context.contentResolver.openInputStream(uri)?.use { input ->
temp.outputStream().use { output ->
val buffer = ByteArray(65536)
var size = 0L
while (true) {
val count = input.read(buffer)
if (count < 0) break
size += count
check(size <= 1024L * 1024 * 1024) { "Файл больше 1 ГиБ" }
output.write(buffer, 0, count)
}
}
} ?: error("Не удалось открыть вложение")
temp.absolutePath to name
} catch (e: Exception) { temp.delete(); throw e }
}
onSendFile(targetConv, attachment.first, attachment.second)
} catch (e: Exception) { Log.e("utun-gui", "PM attachment failed", e); fileError = e.message ?: "Ошибка вложения" }
finally { preparing = false }
}
}
}
val listState = rememberLazyListState()
LaunchedEffect(messages.size) {
@ -66,9 +129,11 @@ fun DmChatScreen(
) {
item { Spacer(Modifier.height(8.dp)) }
items(messages.reversed()) { msg ->
MessageBubble(msg)
MessageBubble(msg, onOpenFile = { savePath = it.filePath; saveFile.launch(it.text) })
}
}
if (fileError.isNotEmpty()) Text(fileError, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(8.dp))
if (preparing) Text("Подготовка вложения…", modifier = Modifier.padding(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
@ -76,6 +141,9 @@ fun DmChatScreen(
.navigationBarsPadding(),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
IconButton(onClick = { attachFile.launch(arrayOf("*/*")) }, enabled = !preparing) {
Icon(Icons.Default.AttachFile, "Прикрепить файл")
}
OutlinedTextField(
value = text,
onValueChange = { text = it },

Loading…
Cancel
Save