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.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<Uri?>(null) }
val listState = rememberLazyListState()
val messages = remember { mutableStateListOf<UnsignedEvent>() }
val groupedMessages = remember(messages.toList()) {
messages.groupBy { it.createdAt().formatAsGroupHeader() }
}
val sendFile = { uri: Uri ->
scope.launch {
val bytes = withContext(Dispatchers.IO) {
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)
chatViewModel.sendFileMessage(id, bytes, type)
// Send message
chatViewModel.sendFileMessage(id, file, type)
} catch (e: Exception) {
snackbarHostState.showSnackbar("Error: ${e.message}")
}
}
}
@@ -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",
)
}

View File

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