feat: add basic support for file upload via blossom #32

Merged
reya merged 3 commits from feat/image-upload into master 2026-06-30 08:11:20 +00:00
3 changed files with 36 additions and 24 deletions
Showing only changes of commit 43fd61c693 - Show all commits

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M440,680L520,680L520,520L680,520L680,440L520,440L520,280L440,280L440,440L280,440L280,520L440,520L440,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
</vector>

View File

@@ -64,10 +64,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res 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_arrow_back
import coop.composeapp.generated.resources.ic_cancel import coop.composeapp.generated.resources.ic_cancel
import coop.composeapp.generated.resources.ic_check_circle import coop.composeapp.generated.resources.ic_check_circle
import coop.composeapp.generated.resources.ic_plus
import coop.composeapp.generated.resources.ic_send import coop.composeapp.generated.resources.ic_send
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@@ -99,10 +99,10 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val context = LocalContext.current val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val accountViewModel = LocalAccountViewModel.current
val chatViewModel = LocalChatViewModel.current val chatViewModel = LocalChatViewModel.current
val profileViewModel = LocalProfileViewModel.current val profileViewModel = LocalProfileViewModel.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
// Get chat room by ID // Get chat room by ID
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle() val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
@@ -123,10 +123,9 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
return return
} }
val currentUser = accountViewModel.nostr.signer.currentUser
val displayName by remember(room, currentUser) { val displayName by remember(room) {
room?.nameFlow(profileViewModel, currentUser) ?: flowOf("Loading...") room?.nameFlow(profileViewModel) ?: flowOf("Loading...")
}.collectAsStateWithLifecycle("Loading...") }.collectAsStateWithLifecycle("Loading...")
val picture by remember(room) { val picture by remember(room) {
@@ -137,22 +136,28 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
var newOtherMessages by remember { mutableIntStateOf(0) } var newOtherMessages by remember { mutableIntStateOf(0) }
var requireScreening by remember { mutableStateOf(screening) } var requireScreening by remember { mutableStateOf(screening) }
var upload by remember { mutableStateOf<Uri?>(null) }
val listState = rememberLazyListState()
val messages = remember { mutableStateListOf<UnsignedEvent>() } val messages = remember { mutableStateListOf<UnsignedEvent>() }
val groupedMessages = remember(messages.toList()) { val groupedMessages = remember(messages.toList()) {
messages.groupBy { it.createdAt().formatAsGroupHeader() } messages.groupBy { it.createdAt().formatAsGroupHeader() }
} }
val sendFile = { uri: Uri -> val sendFile = { uri: Uri ->
scope.launch { scope.launch {
val bytes = withContext(Dispatchers.IO) { try {
context.contentResolver.openInputStream(uri)?.use { it.readBytes() } // 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) { LaunchedEffect(id) {
// Start loading spinner
loading = true
// Get messages // Get messages
val initialMessages = chatViewModel.getChatRoomMessages(id) val initialMessages = chatViewModel.getChatRoomMessages(id)
messages.clear() messages.clear()
@@ -590,7 +592,7 @@ fun ChatInput(
leadingIcon = { leadingIcon = {
IconButton(onClick = onUpload) { IconButton(onClick = onUpload) {
Icon( Icon(
painter = painterResource(Res.drawable.ic_plus), painter = painterResource(Res.drawable.ic_add_circle),
contentDescription = "Upload", contentDescription = "Upload",
) )
} }

View File

@@ -177,20 +177,21 @@ class ChatViewModel(
} }
} }
fun sendFileMessage( suspend fun sendFileMessage(
roomId: Long, roomId: Long,
file: ByteArray?, file: ByteArray?,
contentType: String? = "image/jpeg", contentType: String? = "image/jpeg",
replies: List<EventId> = emptyList() replies: List<EventId> = emptyList()
) { ) {
if (file == null) return if (file == null) return
viewModelScope.launch {
try { try {
val uri = appViewModel.blossomUpload(file, contentType) ?: return@launch val uri = appViewModel.blossomUpload(file, contentType)
sendMessage(roomId, uri, replies) ?: throw IllegalArgumentException("Failed to upload file")
} catch (e: Exception) {
appViewModel.showError("Error: ${e.message}") sendMessage(roomId, uri, replies)
} } catch (e: Exception) {
throw IllegalArgumentException("Error: ${e.message}")
} }
} }