Add Android PM attachment selection, saving and delivery labels
This commit is contained in:
1 parent
1457275139
commit
dc1110af4d
3 files changed
+72
-3
No files matched your search
@@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in new issue
Block a user