From 01e10f4680383e7a562414054c308a7bf27e9699 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 30 Jun 2026 09:20:56 +0700 Subject: [PATCH 1/3] update chat screen spacing --- .../src/androidMain/AndroidManifest.xml | 3 +- .../kotlin/su/reya/coop/screens/ChatScreen.kt | 73 +++++++++++-------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 58fb629..a25745c 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -36,7 +36,8 @@ + android:theme="@style/Theme.App.Starting" + android:windowSoftInputMode="adjustResize"> diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index 793fd9d..63a552c 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -19,6 +22,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalIconButton @@ -29,6 +33,7 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -81,6 +86,7 @@ import su.reya.coop.shared.nameFlow import su.reya.coop.shared.pictureFlow import su.reya.coop.short +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ChatScreen(id: Long, screening: Boolean = false) { val snackbarHostState = LocalSnackbarHostState.current @@ -165,6 +171,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) { } Scaffold( + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime), containerColor = MaterialTheme.colorScheme.surfaceContainer, snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { @@ -291,12 +298,14 @@ fun ChatScreen(id: Long, screening: Boolean = false) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Button( onClick = { navigator.goBack() }, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .size(ButtonDefaults.MediumContainerHeight) ) { Text( text = "Reject", @@ -305,7 +314,9 @@ fun ChatScreen(id: Long, screening: Boolean = false) { } FilledTonalButton( onClick = { requireScreening = false }, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .size(ButtonDefaults.MediumContainerHeight) ) { Text( text = "Accept", @@ -534,36 +545,34 @@ fun ChatInput( onValueChange: (String) -> Unit, onSend: () -> Unit ) { - Surface(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.Bottom - ) { - TextField( - value = value, - onValueChange = onValueChange, - placeholder = { Text("Message") }, - shape = RoundedCornerShape(28.dp), - colors = TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ), - modifier = Modifier.weight(1f) + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.Bottom + ) { + TextField( + value = value, + onValueChange = onValueChange, + placeholder = { Text("Message") }, + shape = RoundedCornerShape(28.dp), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.size(8.dp)) + FilledTonalIconButton( + onClick = onSend, + modifier = Modifier.size(56.dp), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(Res.drawable.ic_send), + contentDescription = "Send" ) - Spacer(modifier = Modifier.size(8.dp)) - FilledTonalIconButton( - onClick = onSend, - modifier = Modifier.size(56.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Icon( - painter = painterResource(Res.drawable.ic_send), - contentDescription = "Send" - ) - } } } } -- 2.49.1 From 0c6b23b9e65c9866cc7b10a37bf83aa6d2f46780 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 30 Jun 2026 10:12:13 +0700 Subject: [PATCH 2/3] add upload button --- .../kotlin/su/reya/coop/screens/ChatScreen.kt | 46 +++++++++++++++++-- .../su/reya/coop/screens/NewIdentityScreen.kt | 2 +- .../su/reya/coop/shared/ProfileEditor.kt | 5 +- .../reya/coop/viewmodels/AccountViewModel.kt | 6 +-- .../su/reya/coop/viewmodels/AppViewModel.kt | 28 +++++++++++ .../su/reya/coop/viewmodels/ChatViewModel.kt | 23 +++++++++- .../reya/coop/viewmodels/ProfileViewModel.kt | 38 ++------------- 7 files changed, 101 insertions(+), 47 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index 63a552c..a569767 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -1,5 +1,8 @@ package su.reya.coop.screens +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -55,6 +58,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -63,9 +67,12 @@ import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_cancel import coop.composeapp.generated.resources.ic_check_circle +import coop.composeapp.generated.resources.ic_plus import coop.composeapp.generated.resources.ic_send +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.painterResource import rust.nostr.sdk.PublicKey import rust.nostr.sdk.Timestamp @@ -89,11 +96,13 @@ import su.reya.coop.short @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ChatScreen(id: Long, screening: Boolean = false) { + val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current val navigator = LocalNavigator.current val accountViewModel = LocalAccountViewModel.current val chatViewModel = LocalChatViewModel.current val profileViewModel = LocalProfileViewModel.current + val scope = rememberCoroutineScope() // Get chat room by ID val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle() @@ -128,6 +137,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) { var loading by remember { mutableStateOf(true) } var newOtherMessages by remember { mutableIntStateOf(0) } var requireScreening by remember { mutableStateOf(screening) } + var upload by remember { mutableStateOf(null) } val listState = rememberLazyListState() val messages = remember { mutableStateListOf() } @@ -136,6 +146,20 @@ fun ChatScreen(id: Long, screening: Boolean = false) { messages.groupBy { it.createdAt().formatAsGroupHeader() } } + val sendFile = { uri: Uri -> + scope.launch { + val bytes = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + } + val type = context.contentResolver.getType(uri) + chatViewModel.sendFileMessage(id, bytes, type) + } + } + + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { sendFile(it) } + } + LaunchedEffect(id) { // Start loading spinner loading = true @@ -333,6 +357,9 @@ fun ChatScreen(id: Long, screening: Boolean = false) { onSend = { chatViewModel.sendMessage(id, text) text = "" + }, + onUpload = { + launcher.launch("image/*") } ) } @@ -543,22 +570,31 @@ fun ChatMessage( fun ChatInput( value: String, onValueChange: (String) -> Unit, - onSend: () -> Unit + onSend: () -> Unit, + onUpload: () -> Unit ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.Bottom ) { TextField( - value = value, - onValueChange = onValueChange, - placeholder = { Text("Message") }, + modifier = Modifier.weight(1f), shape = RoundedCornerShape(28.dp), colors = TextFieldDefaults.colors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent ), - modifier = Modifier.weight(1f) + value = value, + onValueChange = onValueChange, + placeholder = { Text("Message") }, + leadingIcon = { + IconButton(onClick = onUpload) { + Icon( + painter = painterResource(Res.drawable.ic_plus), + contentDescription = "Upload", + ) + } + }, ) Spacer(modifier = Modifier.size(8.dp)) FilledTonalIconButton( diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt index 88eca83..4b98253 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt @@ -28,7 +28,7 @@ fun NewIdentityScreen() { onBack = { navigator.goBack() }, onConfirm = { name, bio, bytes, type -> scope.launch { - accountViewModel.createIdentity(name, bio, bytes, type, profileViewModel) + accountViewModel.createIdentity(name, bio, bytes, type) navigator.navigate(Screen.Home) } } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt index 2267b2c..9aab16a 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.Composable 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.Alignment import androidx.compose.ui.Modifier @@ -53,7 +54,6 @@ import coil3.compose.AsyncImage import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_plus -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -75,6 +75,8 @@ fun ProfileEditor( val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() + var name by remember(initialName) { mutableStateOf(initialName) } var bio by remember(initialBio) { mutableStateOf(initialBio) } var picture by remember(initialPicture) { mutableStateOf(initialPicture) } @@ -264,7 +266,6 @@ fun ProfileEditor( .fillMaxWidth() .size(ButtonDefaults.MediumContainerHeight), onClick = { - val scope = CoroutineScope(Dispatchers.Main) scope.launch { val bytes = withContext(Dispatchers.IO) { (picture as? Uri)?.let { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AccountViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AccountViewModel.kt index e8b8d2e..1fe5b7f 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AccountViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AccountViewModel.kt @@ -133,6 +133,7 @@ class AccountViewModel( suspend fun importIdentity(secret: String) { appViewModel.setBusy(true) + try { val signer = createSigner(secret) nostr.setSigner(signer) @@ -150,15 +151,14 @@ class AccountViewModel( bio: String?, picture: ByteArray?, contentType: String? = "image/jpeg", - profileViewModel: ProfileViewModel ) { appViewModel.setBusy(true) - + val keys = Keys.generate() val secret = keys.secretKey().toBech32() try { - val avatarUrl = picture?.let { profileViewModel.blossomUpload(it, contentType) } + val avatarUrl = picture?.let { appViewModel.blossomUpload(it, contentType) } nostr.profiles.createIdentity(keys = keys, name = name, bio, picture = avatarUrl) // Set credentials in persistent storage secretStore.set("user_signer", secret) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AppViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AppViewModel.kt index ae383bf..e2eff28 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AppViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/AppViewModel.kt @@ -2,6 +2,9 @@ package su.reya.coop.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -9,6 +12,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import su.reya.coop.blossom.BlossomClient import su.reya.coop.nostr.Nostr import su.reya.coop.storage.SecretStorage @@ -53,6 +58,29 @@ class AppViewModel( } } + suspend fun blossomUpload(file: ByteArray, contentType: String? = "image/jpeg"): String? { + val blossom = BlossomClient( + url = "https://blossom.band", + client = HttpClient { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + prettyPrint = true + isLenient = true + }) + } + } + ) + + val descriptor = blossom.upload( + file = file, + contentType = contentType, + signer = nostr.signer.get() + ) + + return descriptor?.url + } + override fun onCleared() { super.onCleared() viewModelScope.launch { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt index 74dc5b9..4a3a729 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt @@ -149,7 +149,11 @@ class ChatViewModel( } } - fun sendMessage(roomId: Long, message: String, replies: List = emptyList()) { + fun sendMessage( + roomId: Long, + message: String, + replies: List = emptyList() + ) { if (message.isEmpty()) { appViewModel.showError("Message cannot be empty") return @@ -173,6 +177,23 @@ class ChatViewModel( } } + fun sendFileMessage( + roomId: Long, + file: ByteArray?, + contentType: String? = "image/jpeg", + replies: List = emptyList() + ) { + if (file == null) return + viewModelScope.launch { + try { + val uri = appViewModel.blossomUpload(file, contentType) ?: return@launch + sendMessage(roomId, uri, replies) + } catch (e: Exception) { + appViewModel.showError("Error: ${e.message}") + } + } + } + fun isMessageSent(id: EventId): Boolean { val giftWrapId = nostr.messages.rumorMap[id] return if (giftWrapId != null) { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ProfileViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ProfileViewModel.kt index b015e22..3553c50 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ProfileViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ProfileViewModel.kt @@ -2,9 +2,6 @@ package su.reya.coop.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.ktor.client.HttpClient -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -13,11 +10,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.Json import rust.nostr.sdk.Metadata import rust.nostr.sdk.PublicKey import rust.nostr.sdk.Timestamp -import su.reya.coop.blossom.BlossomClient import su.reya.coop.nostr.Nostr import kotlin.time.Clock import kotlin.time.Duration.Companion.milliseconds @@ -110,45 +105,18 @@ class ProfileViewModel( } } - suspend fun blossomUpload(file: ByteArray, contentType: String?): String? { - try { - val blossom = BlossomClient( - url = "https://blossom.band", - client = HttpClient { - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - prettyPrint = true - isLenient = true - }) - } - } - ) - - val descriptor = blossom.upload( - file = file, - contentType = contentType, - signer = nostr.signer.get() - ) - - return descriptor?.url - } catch (e: Exception) { - appViewModel.showError("Error: ${e.message}") - return null - } - } - suspend fun updateProfile( name: String? = null, bio: String? = null, picture: ByteArray? = null, - contentType: String? = null + contentType: String? = "image/jpeg" ) { appViewModel.setBusy(true) try { - val avatarUrl = picture?.let { blossomUpload(it, contentType ?: "image/jpeg") } + val avatarUrl = picture?.let { appViewModel.blossomUpload(it, contentType) } val newMetadata = nostr.profiles.updateProfile(name, bio, avatarUrl) val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found") + updateMetadata(currentUser, newMetadata) } catch (e: Exception) { appViewModel.showError("Error: ${e.message}") -- 2.49.1 From 43fd61c693a3c3f046d33c444302cea9dd1d173a Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 30 Jun 2026 15:05:33 +0700 Subject: [PATCH 3/3] update upload feature --- .../drawable/ic_add_circle.xml | 9 +++++ .../kotlin/su/reya/coop/screens/ChatScreen.kt | 34 ++++++++++--------- .../su/reya/coop/viewmodels/ChatViewModel.kt | 17 +++++----- 3 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 composeApp/src/androidMain/composeResources/drawable/ic_add_circle.xml diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_add_circle.xml b/composeApp/src/androidMain/composeResources/drawable/ic_add_circle.xml new file mode 100644 index 0000000..9e0acc8 --- /dev/null +++ b/composeApp/src/androidMain/composeResources/drawable/ic_add_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index a569767..6df1cae 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -64,10 +64,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res +import coop.composeapp.generated.resources.ic_add_circle import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_cancel import coop.composeapp.generated.resources.ic_check_circle -import coop.composeapp.generated.resources.ic_plus import coop.composeapp.generated.resources.ic_send import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOf @@ -99,10 +99,10 @@ fun ChatScreen(id: Long, screening: Boolean = false) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current val navigator = LocalNavigator.current - val accountViewModel = LocalAccountViewModel.current val chatViewModel = LocalChatViewModel.current val profileViewModel = LocalProfileViewModel.current val scope = rememberCoroutineScope() + val listState = rememberLazyListState() // Get chat room by ID val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle() @@ -123,10 +123,9 @@ fun ChatScreen(id: Long, screening: Boolean = false) { return } - val currentUser = accountViewModel.nostr.signer.currentUser - val displayName by remember(room, currentUser) { - room?.nameFlow(profileViewModel, currentUser) ?: flowOf("Loading...") + val displayName by remember(room) { + room?.nameFlow(profileViewModel) ?: flowOf("Loading...") }.collectAsStateWithLifecycle("Loading...") val picture by remember(room) { @@ -137,22 +136,28 @@ fun ChatScreen(id: Long, screening: Boolean = false) { var loading by remember { mutableStateOf(true) } var newOtherMessages by remember { mutableIntStateOf(0) } var requireScreening by remember { mutableStateOf(screening) } - var upload by remember { mutableStateOf(null) } - val listState = rememberLazyListState() val messages = remember { mutableStateListOf() } - val groupedMessages = remember(messages.toList()) { messages.groupBy { it.createdAt().formatAsGroupHeader() } } val sendFile = { uri: Uri -> scope.launch { - val bytes = withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + try { + // Read file + val file = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + } + + // Parse the file content type + val type = context.contentResolver.getType(uri) + + // Send message + chatViewModel.sendFileMessage(id, file, type) + } catch (e: Exception) { + snackbarHostState.showSnackbar("Error: ${e.message}") } - val type = context.contentResolver.getType(uri) - chatViewModel.sendFileMessage(id, bytes, type) } } @@ -161,9 +166,6 @@ fun ChatScreen(id: Long, screening: Boolean = false) { } LaunchedEffect(id) { - // Start loading spinner - loading = true - // Get messages val initialMessages = chatViewModel.getChatRoomMessages(id) messages.clear() @@ -590,7 +592,7 @@ fun ChatInput( leadingIcon = { IconButton(onClick = onUpload) { Icon( - painter = painterResource(Res.drawable.ic_plus), + painter = painterResource(Res.drawable.ic_add_circle), contentDescription = "Upload", ) } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt index 4a3a729..897044d 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodels/ChatViewModel.kt @@ -177,20 +177,21 @@ class ChatViewModel( } } - fun sendFileMessage( + suspend fun sendFileMessage( roomId: Long, file: ByteArray?, contentType: String? = "image/jpeg", replies: List = emptyList() ) { if (file == null) return - viewModelScope.launch { - try { - val uri = appViewModel.blossomUpload(file, contentType) ?: return@launch - sendMessage(roomId, uri, replies) - } catch (e: Exception) { - appViewModel.showError("Error: ${e.message}") - } + + try { + val uri = appViewModel.blossomUpload(file, contentType) + ?: throw IllegalArgumentException("Failed to upload file") + + sendMessage(roomId, uri, replies) + } catch (e: Exception) { + throw IllegalArgumentException("Error: ${e.message}") } } -- 2.49.1