Private
Public Access
1
0

app: MessageEntry: Adds UI support for uploading attachments

Still need backend support to finish this.
This commit is contained in:
2024-04-04 23:52:17 -07:00
parent b160baae3e
commit b5eccbd000
6 changed files with 184 additions and 22 deletions

View File

@@ -11,12 +11,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import net.buzzert.kordophonedroid.R import net.buzzert.kordophonedroid.R
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListItem import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListItem
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen
import net.buzzert.kordophonedroid.ui.conversationlist.NoContentView import net.buzzert.kordophonedroid.ui.conversationlist.NoContentView
import net.buzzert.kordophonedroid.ui.messagelist.AttachmentRowItem
import net.buzzert.kordophonedroid.ui.messagelist.MessageEntry import net.buzzert.kordophonedroid.ui.messagelist.MessageEntry
import net.buzzert.kordophonedroid.ui.messagelist.MessageListItem import net.buzzert.kordophonedroid.ui.messagelist.MessageListItem
import net.buzzert.kordophonedroid.ui.messagelist.MessageMetadata import net.buzzert.kordophonedroid.ui.messagelist.MessageMetadata
@@ -77,11 +79,19 @@ private fun MessageListScreenPreview() {
).reversed() ).reversed()
Scaffold() { Scaffold() {
MessageTranscript(messages = messages, paddingValues = it, showSenders = true, onSendMessage = {}) MessageTranscript(
messages = messages,
paddingValues = it,
showSenders = true,
attachmentUris = setOf(),
onAddAttachment = {},
onClearAttachments = {},
onSendMessage = {}
)
} }
} }
@Preview @Preview(showBackground = true)
@Composable @Composable
private fun MessageEntryPreview() { private fun MessageEntryPreview() {
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) { var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
@@ -91,6 +101,18 @@ private fun MessageEntryPreview() {
MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState) MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState)
} }
@Preview(showBackground = true)
@Composable
private fun MessageEntryWithAttachmentsPreview() {
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue("Attachments"))
}
MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState, attachmentItems = listOf(
AttachmentRowItem(painterResource(id = R.drawable.sedona), "id")
))
}
// - No content // - No content
@Preview @Preview

View File

@@ -1,47 +1,132 @@
package net.buzzert.kordophonedroid.ui.messagelist package net.buzzert.kordophonedroid.ui.messagelist
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.ElevatedButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.buzzert.kordophonedroid.R
data class AttachmentRowItem(
val painter: Painter,
val id: String,
)
@Composable
fun AttachmentRow(
attachmentItems: List<AttachmentRowItem>,
onClear: () -> Unit,
) {
Divider()
Row(
modifier = Modifier
.height(120.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.onSurface.copy(0.08f))
.padding(8.dp)
) {
LazyRow {
attachmentItems.forEach { attachmentItem ->
item {
Image(
painter = attachmentItem.painter,
contentDescription = "attachment",
contentScale = ContentScale.Crop,
modifier = Modifier
.aspectRatio(1.0f)
.clip(RoundedCornerShape(4.dp))
)
Spacer(Modifier.width(4.dp))
}
}
}
Spacer(Modifier.weight(1f))
ElevatedButton(
onClick = onClear,
colors = ButtonDefaults.elevatedButtonColors(
containerColor = MaterialTheme.colors.background
),
modifier = Modifier.align(Alignment.CenterVertically)
) {
Text("Remove")
}
}
}
@Composable @Composable
fun MessageEntry( fun MessageEntry(
keyboardType: KeyboardType = KeyboardType.Text,
onTextChanged: (TextFieldValue) -> Unit,
textFieldValue: TextFieldValue, textFieldValue: TextFieldValue,
attachmentItems: List<AttachmentRowItem> = listOf(),
onAddAttachment: () -> Unit = {},
onClearAttachments: () -> Unit = {},
onTextChanged: (TextFieldValue) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
) { ) {
Row( Column {
modifier = Modifier if (attachmentItems.isNotEmpty()) {
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f)) AttachmentRow(attachmentItems, onClear = onClearAttachments)
.fillMaxWidth() }
.padding(vertical = 8.dp, horizontal = 12.dp)
.imePadding() Row(
.navigationBarsPadding() modifier = Modifier
) { .background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f))
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 4.dp)
.imePadding()
.navigationBarsPadding()
) {
IconButton(
onClick = onAddAttachment,
) {
Icon(
painter = painterResource(id = R.drawable.attach_file),
contentDescription = "Attach File"
)
}
Spacer(Modifier.width(8.dp))
Surface( Surface(
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.shadow(4.dp) .shadow(3.dp)
) { ) {
BasicTextField( BasicTextField(
value = textFieldValue, value = textFieldValue,
@@ -68,8 +153,14 @@ fun MessageEntry(
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Button(onClick = onSend) { Button(
onClick = onSend,
enabled = (attachmentItems.isNotEmpty() || textFieldValue.text.isNotEmpty())
) {
Text(text = "Send") Text(text = "Send")
} }
Spacer(Modifier.width(8.dp))
}
} }
} }

View File

@@ -1,6 +1,8 @@
package net.buzzert.kordophonedroid.ui.messagelist package net.buzzert.kordophonedroid.ui.messagelist
import android.util.Log import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -28,8 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -46,6 +47,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.SubcomposeAsyncImage import coil.compose.SubcomposeAsyncImage
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest import coil.request.ImageRequest
import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophonedroid.ui.Destination import net.buzzert.kordophonedroid.ui.Destination
@@ -129,6 +131,13 @@ fun MessageListScreen(
} }
} }
var attachmentUris by remember { mutableStateOf<Set<Uri>>(mutableSetOf()) }
val imagePicker = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
attachmentUris = attachmentUris.plus(it)
}
}
val navController = LocalNavController.current val navController = LocalNavController.current
Scaffold( Scaffold(
topBar = { topBar = {
@@ -138,8 +147,18 @@ fun MessageListScreen(
messages = messageItems, messages = messageItems,
paddingValues = padding, paddingValues = padding,
showSenders = viewModel.isGroupChat, showSenders = viewModel.isGroupChat,
attachmentUris = attachmentUris,
onAddAttachment = {
imagePicker.launch("image/*")
},
onClearAttachments = {
attachmentUris = setOf()
},
onSendMessage = { text -> onSendMessage = { text ->
viewModel.enqueueOutgoingMessage(text) viewModel.enqueueOutgoingMessage(
text = text,
attachmentUris = attachmentUris
)
} }
) )
} }
@@ -150,6 +169,9 @@ fun MessageTranscript(
messages: List<MessageListItem>, messages: List<MessageListItem>,
paddingValues: PaddingValues, paddingValues: PaddingValues,
showSenders: Boolean, showSenders: Boolean,
attachmentUris: Set<Uri>,
onAddAttachment: () -> Unit,
onClearAttachments: () -> Unit,
onSendMessage: (text: String) -> Unit, onSendMessage: (text: String) -> Unit,
) { ) {
val scrollState = rememberLazyListState() val scrollState = rememberLazyListState()
@@ -157,6 +179,13 @@ fun MessageTranscript(
mutableStateOf(TextFieldValue()) mutableStateOf(TextFieldValue())
} }
val attachmentRowItems = attachmentUris.map {
AttachmentRowItem(
painter = rememberAsyncImagePainter(model = it),
id = "attachmentID"
)
}
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@@ -172,6 +201,9 @@ fun MessageTranscript(
MessageEntry( MessageEntry(
onTextChanged = { textState = it }, onTextChanged = { textState = it },
textFieldValue = textState, textFieldValue = textState,
attachmentItems = attachmentRowItems,
onAddAttachment = onAddAttachment,
onClearAttachments = onClearAttachments,
onSend = { onSend = {
onSendMessage(textState.text) onSendMessage(textState.text)
@@ -198,7 +230,6 @@ fun Messages(
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
var lastDate: Date = Date()
val dateFormatter = SimpleDateFormat.getDateTimeInstance() val dateFormatter = SimpleDateFormat.getDateTimeInstance()
for (index in messages.indices) { for (index in messages.indices) {

View File

@@ -1,6 +1,7 @@
package net.buzzert.kordophonedroid.ui.messagelist package net.buzzert.kordophonedroid.ui.messagelist
import android.content.Context import android.content.Context
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -97,7 +98,14 @@ class MessageListViewModel @Inject constructor(
val isGroupChat: Boolean get() = conversation!!.isGroupChat val isGroupChat: Boolean get() = conversation!!.isGroupChat
fun enqueueOutgoingMessage(text: String) { fun enqueueOutgoingMessage(
text: String,
attachmentUris: Set<Uri>
) {
// TODO: Handle attachments!
// Probably make a special OutgoingMessage object for this, since a lot of Message fields are
// meaningless here. We don't have GUIDs yet either.
val outgoingMessage = Message( val outgoingMessage = Message(
guid = UUID.randomUUID().toString(), guid = UUID.randomUUID().toString(),
text = text, text = text,

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M720,630Q720,734 647,807Q574,880 470,880Q366,880 293,807Q220,734 220,630L220,260Q220,185 272.5,132.5Q325,80 400,80Q475,80 527.5,132.5Q580,185 580,260L580,610Q580,656 548,688Q516,720 470,720Q424,720 392,688Q360,656 360,610L360,240L440,240L440,610Q440,623 448.5,631.5Q457,640 470,640Q483,640 491.5,631.5Q500,623 500,610L500,260Q499,218 470.5,189Q442,160 400,160Q358,160 329,189Q300,218 300,260L300,630Q299,701 349,750.5Q399,800 470,800Q540,800 589,750.5Q638,701 640,630L640,240L720,240L720,630Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB