app: MessageEntry: Adds UI support for uploading attachments
Still need backend support to finish this.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
Column {
|
||||||
|
if (attachmentItems.isNotEmpty()) {
|
||||||
|
AttachmentRow(attachmentItems, onClear = onClearAttachments)
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f))
|
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f))
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.navigationBarsPadding()
|
.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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
10
app/src/main/res/drawable/attach_file.xml
Normal file
10
app/src/main/res/drawable/attach_file.xml
Normal 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>
|
||||||
BIN
app/src/main/res/drawable/sedona.jpeg
Normal file
BIN
app/src/main/res/drawable/sedona.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 659 KiB |
Reference in New Issue
Block a user