mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Working image sending
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
package ru.fromchat.ui.chat
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
actual fun getFilenameFromUri(uri: String): String {
|
||||||
|
val context = com.pr0gramm3r101.utils.UtilsLibrary.context
|
||||||
|
context.contentResolver.query(Uri.parse(uri), null, null, null, null)?.use { cursor ->
|
||||||
|
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (nameIndex >= 0 && cursor.moveToFirst()) {
|
||||||
|
val name = cursor.getString(nameIndex)
|
||||||
|
if (!name.isNullOrBlank()) return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uri.substringAfterLast('/').takeIf { it.isNotBlank() } ?: "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberImagePicker(onResult: (List<String>) -> Unit): () -> Unit {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.PickMultipleVisualMedia()
|
||||||
|
) { uris: List<Uri> ->
|
||||||
|
onResult(uris.map { it.toString() })
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
launcher.launch(
|
||||||
|
PickVisualMediaRequest.Builder()
|
||||||
|
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberFilePicker(onResult: (List<String>) -> Unit): () -> Unit {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenMultipleDocuments()
|
||||||
|
) { uris: List<Uri> ->
|
||||||
|
onResult(uris.map { it.toString() })
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
launcher.launch(arrayOf("*/*"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package ru.fromchat.ui.chat
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
data class SelectedAttachment(
|
||||||
|
val id: String,
|
||||||
|
val uri: String,
|
||||||
|
val filename: String,
|
||||||
|
val sizeBytes: Long?,
|
||||||
|
val isImage: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific image picker. Returns a launch function.
|
||||||
|
* On Android: PickVisualMedia (images only, multiple).
|
||||||
|
* On iOS: Placeholder (no-op until Phase 4).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
expect fun rememberImagePicker(onResult: (List<String>) -> Unit): () -> Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific file picker. Returns a launch function.
|
||||||
|
* On Android: OpenDocument/OpenMultipleDocuments (SAF).
|
||||||
|
* On iOS: Placeholder (no-op until Phase 4).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
expect fun rememberFilePicker(onResult: (List<String>) -> Unit): () -> Unit
|
||||||
|
|
||||||
|
/** Resolve display filename from content URI. Platform-specific. */
|
||||||
|
expect fun getFilenameFromUri(uri: String): String
|
||||||
@@ -11,6 +11,7 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -22,13 +23,17 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
|
import androidx.compose.material.icons.filled.AttachFile
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Image
|
||||||
import androidx.compose.material3.FilledIconButton
|
import androidx.compose.material3.FilledIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -50,12 +55,14 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
|
import kotlin.time.Clock
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
@@ -135,30 +142,93 @@ private fun PreviewBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AttachmentChip(
|
||||||
|
attachment: SelectedAttachment,
|
||||||
|
onRemove: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (attachment.isImage) Icons.Default.Image else Icons.Default.AttachFile,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = attachment.filename,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.widthIn(max = 120.dp)
|
||||||
|
)
|
||||||
|
IconButton(onClick = onRemove, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Remove",
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInput(
|
fun ChatInput(
|
||||||
text: String,
|
text: String,
|
||||||
onTextChange: (String) -> Unit,
|
onTextChange: (String) -> Unit,
|
||||||
onSend: (String) -> Unit,
|
onSend: (String, List<SelectedAttachment>) -> Unit,
|
||||||
typingHandler: TypingHandler,
|
typingHandler: TypingHandler,
|
||||||
replyTo: Message? = null,
|
replyTo: Message? = null,
|
||||||
editingMessage: Message? = null,
|
editingMessage: Message? = null,
|
||||||
onClearReply: () -> Unit,
|
onClearReply: () -> Unit,
|
||||||
onClearEdit: () -> Unit,
|
onClearEdit: () -> Unit,
|
||||||
hazeState: HazeState
|
hazeState: HazeState,
|
||||||
|
recipientId: Int? = null
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var typingJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
|
var typingJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
|
||||||
|
var attachments by remember { mutableStateOf<List<SelectedAttachment>>(emptyList()) }
|
||||||
|
|
||||||
|
val launchImagePicker = rememberImagePicker { uris ->
|
||||||
|
attachments = attachments + uris.map { uri ->
|
||||||
|
SelectedAttachment(
|
||||||
|
id = "img_${Clock.System.now().toEpochMilliseconds()}_${attachments.size}",
|
||||||
|
uri = uri,
|
||||||
|
filename = getFilenameFromUri(uri),
|
||||||
|
sizeBytes = null,
|
||||||
|
isImage = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val launchFilePicker = rememberFilePicker { uris ->
|
||||||
|
attachments = attachments + uris.map { uri ->
|
||||||
|
SelectedAttachment(
|
||||||
|
id = "file_${Clock.System.now().toEpochMilliseconds()}_${attachments.size}",
|
||||||
|
uri = uri,
|
||||||
|
filename = getFilenameFromUri(uri),
|
||||||
|
sizeBytes = null,
|
||||||
|
isImage = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle typing indicator
|
|
||||||
LaunchedEffect(text) {
|
LaunchedEffect(text) {
|
||||||
if (text.isNotBlank()) {
|
if (text.isNotBlank()) {
|
||||||
typingJob?.cancel()
|
typingJob?.cancel()
|
||||||
typingHandler.sendTyping()
|
typingHandler.sendTyping()
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
typingJob = scope.launch {
|
typingJob = scope.launch {
|
||||||
delay(3000) // 3 seconds
|
delay(3000)
|
||||||
typingHandler.stopTyping()
|
typingHandler.stopTyping()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -167,6 +237,8 @@ fun ChatInput(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canSend = text.isNotBlank() || attachments.isNotEmpty()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -208,65 +280,110 @@ fun ChatInput(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
AnimatedVisibility(
|
||||||
value = text,
|
visible = attachments.isNotEmpty(),
|
||||||
onValueChange = onTextChange,
|
enter = fadeIn() + expandVertically(),
|
||||||
modifier = Modifier
|
exit = fadeOut() + shrinkVertically()
|
||||||
.fillMaxWidth()
|
) {
|
||||||
.animateContentSize(),
|
Row(
|
||||||
placeholder = {
|
modifier = Modifier
|
||||||
Text(
|
.fillMaxWidth()
|
||||||
text = stringResource(Res.string.message_placeholder),
|
.horizontalScroll(rememberScrollState())
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
.padding(start = 12.dp, end = 12.dp, top = 8.dp),
|
||||||
)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
},
|
) {
|
||||||
shape = shape,
|
attachments.forEach { attachment ->
|
||||||
maxLines = 5,
|
AttachmentChip(
|
||||||
singleLine = false,
|
attachment = attachment,
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
onRemove = { attachments = attachments.filter { it.id != attachment.id } }
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
errorContainerColor = Color.Transparent,
|
|
||||||
disabledContainerColor = Color.Transparent,
|
|
||||||
focusedBorderColor = Color.Transparent,
|
|
||||||
errorBorderColor = Color.Transparent,
|
|
||||||
disabledBorderColor = Color.Transparent,
|
|
||||||
unfocusedBorderColor = Color.Transparent
|
|
||||||
),
|
|
||||||
trailingIcon = {
|
|
||||||
val offset = with(LocalDensity.current) { 20.dp.toPx().toInt() }
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = text.isNotBlank(),
|
|
||||||
enter = slideInHorizontally(
|
|
||||||
initialOffsetX = { it + offset },
|
|
||||||
animationSpec = tween(durationMillis = 300)
|
|
||||||
),
|
|
||||||
exit = slideOutHorizontally(
|
|
||||||
targetOffsetX = { it + offset },
|
|
||||||
animationSpec = tween(durationMillis = 200)
|
|
||||||
)
|
)
|
||||||
) {
|
}
|
||||||
Box(Modifier.padding(end = 5.dp)) {
|
}
|
||||||
FilledIconButton(
|
}
|
||||||
onClick = {
|
|
||||||
onSend(text.trim())
|
Row(
|
||||||
onTextChange("")
|
modifier = Modifier.fillMaxWidth(),
|
||||||
typingHandler.stopTyping()
|
verticalAlignment = Alignment.Bottom
|
||||||
},
|
) {
|
||||||
modifier = Modifier.size(36.dp)
|
if (recipientId != null) {
|
||||||
) {
|
IconButton(onClick = { launchImagePicker() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
imageVector = Icons.Default.Image,
|
||||||
contentDescription = "Send",
|
contentDescription = "Pick image",
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
modifier = Modifier.size(18.dp)
|
)
|
||||||
)
|
}
|
||||||
|
IconButton(onClick = { launchFilePicker() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.AttachFile,
|
||||||
|
contentDescription = "Pick file",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = onTextChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.animateContentSize(),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.message_placeholder),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = shape,
|
||||||
|
maxLines = 5,
|
||||||
|
singleLine = false,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
errorContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
focusedBorderColor = Color.Transparent,
|
||||||
|
errorBorderColor = Color.Transparent,
|
||||||
|
disabledBorderColor = Color.Transparent,
|
||||||
|
unfocusedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
val offset = with(LocalDensity.current) { 20.dp.toPx().toInt() }
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = canSend,
|
||||||
|
enter = slideInHorizontally(
|
||||||
|
initialOffsetX = { it + offset },
|
||||||
|
animationSpec = tween(durationMillis = 300)
|
||||||
|
),
|
||||||
|
exit = slideOutHorizontally(
|
||||||
|
targetOffsetX = { it + offset },
|
||||||
|
animationSpec = tween(durationMillis = 200)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(Modifier.padding(end = 5.dp)) {
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = {
|
||||||
|
val plaintext = text.trim().ifBlank { "" }
|
||||||
|
onSend(plaintext, attachments)
|
||||||
|
onTextChange("")
|
||||||
|
attachments = emptyList()
|
||||||
|
typingHandler.stopTyping()
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||||
|
contentDescription = "Send",
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,6 +317,9 @@ abstract class ChatPanel(
|
|||||||
abstract fun showCallButton(): Boolean
|
abstract fun showCallButton(): Boolean
|
||||||
abstract fun getTypingHandler(): TypingHandler
|
abstract fun getTypingHandler(): TypingHandler
|
||||||
|
|
||||||
|
/** DM recipient ID for attachment uploads; null for non-DM panels. */
|
||||||
|
open fun getRecipientId(): Int? = null
|
||||||
|
|
||||||
open val showUsernamesInMessages: Boolean
|
open val showUsernamesInMessages: Boolean
|
||||||
get() = true
|
get() = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import dev.chrisbanes.haze.hazeSource
|
|||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.chrisbanes.haze.rememberHazeState
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
|
import kotlin.time.Clock
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@@ -69,6 +70,8 @@ import kotlinx.serialization.json.jsonPrimitive
|
|||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
|
import ru.fromchat.api.AttachmentUploadJob
|
||||||
|
import ru.fromchat.api.AttachmentUploadQueue
|
||||||
import ru.fromchat.api.Message
|
import ru.fromchat.api.Message
|
||||||
import ru.fromchat.api.UserStatusStore
|
import ru.fromchat.api.UserStatusStore
|
||||||
import ru.fromchat.api.WebSocketManager
|
import ru.fromchat.api.WebSocketManager
|
||||||
@@ -426,7 +429,7 @@ fun ChatScreen(
|
|||||||
ChatInput(
|
ChatInput(
|
||||||
text = inputText,
|
text = inputText,
|
||||||
onTextChange = { inputText = it },
|
onTextChange = { inputText = it },
|
||||||
onSend = { text ->
|
onSend = { text, attachments ->
|
||||||
if (editingMessage != null) {
|
if (editingMessage != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
panel.handleEditMessage(editingMessage!!.id, text)
|
panel.handleEditMessage(editingMessage!!.id, text)
|
||||||
@@ -434,7 +437,25 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
panel.sendMessageWithImmediateDisplay(text, replyTo?.id)
|
val replyToId = replyTo?.id
|
||||||
|
val recipientId = panel.getRecipientId()
|
||||||
|
if (attachments.isNotEmpty() && recipientId != null) {
|
||||||
|
val plaintext = text.ifBlank { "" }
|
||||||
|
attachments.forEach { att ->
|
||||||
|
AttachmentUploadQueue.enqueue(
|
||||||
|
AttachmentUploadJob(
|
||||||
|
jobId = "dm_${Clock.System.now().toEpochMilliseconds()}_${att.id}",
|
||||||
|
fileUri = att.uri,
|
||||||
|
filename = att.filename,
|
||||||
|
recipientId = recipientId,
|
||||||
|
plaintext = plaintext.ifBlank { att.filename },
|
||||||
|
replyToId = replyToId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (text.isNotBlank()) {
|
||||||
|
panel.sendMessageWithImmediateDisplay(text, replyToId)
|
||||||
|
}
|
||||||
replyTo = null
|
replyTo = null
|
||||||
haptic(HapticFeedbackEvent.MessageSent)
|
haptic(HapticFeedbackEvent.MessageSent)
|
||||||
}
|
}
|
||||||
@@ -449,7 +470,8 @@ fun ChatScreen(
|
|||||||
editingMessage = null
|
editingMessage = null
|
||||||
inputText = ""
|
inputText = ""
|
||||||
},
|
},
|
||||||
hazeState = hazeState
|
hazeState = hazeState,
|
||||||
|
recipientId = panel.getRecipientId()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ class DmPanel(
|
|||||||
|
|
||||||
override fun getTypingHandler(): TypingHandler = typingHandler
|
override fun getTypingHandler(): TypingHandler = typingHandler
|
||||||
|
|
||||||
|
override fun getRecipientId(): Int? = otherUserId
|
||||||
|
|
||||||
override val showUsernamesInMessages: Boolean
|
override val showUsernamesInMessages: Boolean
|
||||||
get() = false
|
get() = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ru.fromchat.ui.chat
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
actual fun getFilenameFromUri(uri: String): String {
|
||||||
|
return uri.substringAfterLast('/').takeIf { it.isNotBlank() } ?: "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberImagePicker(onResult: (List<String>) -> Unit): () -> Unit {
|
||||||
|
return { /* Phase 4: PHPickerViewController */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberFilePicker(onResult: (List<String>) -> Unit): () -> Unit {
|
||||||
|
return { /* Phase 4: UIDocumentPickerViewController */ }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user