|
|
|
|
@ -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 }, |
|
|
|
|
|