diff --git a/app/src/debug/kotlin/previews/Previews.kt b/app/src/debug/kotlin/previews/Previews.kt index 2cc9ec9..e437353 100644 --- a/app/src/debug/kotlin/previews/Previews.kt +++ b/app/src/debug/kotlin/previews/Previews.kt @@ -11,12 +11,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import net.buzzert.kordophonedroid.R import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListItem import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen 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.MessageListItem import net.buzzert.kordophonedroid.ui.messagelist.MessageMetadata @@ -77,11 +79,19 @@ private fun MessageListScreenPreview() { ).reversed() 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 private fun MessageEntryPreview() { var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) { @@ -91,6 +101,18 @@ private fun MessageEntryPreview() { 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 @Preview diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageEntryView.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageEntryView.kt index f80d380..6af3b9e 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageEntryView.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageEntryView.kt @@ -1,47 +1,132 @@ package net.buzzert.kordophonedroid.ui.messagelist +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding 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.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.Surface import androidx.compose.material.Text +import androidx.compose.material3.ElevatedButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow 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.unit.dp +import net.buzzert.kordophonedroid.R + +data class AttachmentRowItem( + val painter: Painter, + val id: String, +) + +@Composable +fun AttachmentRow( + attachmentItems: List, + 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 fun MessageEntry( - keyboardType: KeyboardType = KeyboardType.Text, - onTextChanged: (TextFieldValue) -> Unit, textFieldValue: TextFieldValue, + attachmentItems: List = listOf(), + + onAddAttachment: () -> Unit = {}, + onClearAttachments: () -> Unit = {}, + onTextChanged: (TextFieldValue) -> Unit, onSend: () -> Unit, ) { - Row( - modifier = Modifier - .background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f)) - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 12.dp) - .imePadding() - .navigationBarsPadding() - ) { + Column { + if (attachmentItems.isNotEmpty()) { + AttachmentRow(attachmentItems, onClear = onClearAttachments) + } + + Row( + 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( shape = MaterialTheme.shapes.medium, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) .align(Alignment.CenterVertically) - .shadow(4.dp) + .shadow(3.dp) ) { BasicTextField( value = textFieldValue, @@ -68,8 +153,14 @@ fun MessageEntry( Spacer(Modifier.width(8.dp)) - Button(onClick = onSend) { + Button( + onClick = onSend, + enabled = (attachmentItems.isNotEmpty() || textFieldValue.text.isNotEmpty()) + ) { Text(text = "Send") } + + Spacer(Modifier.width(8.dp)) + } } } diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt index 9ab435c..57c6abb 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt @@ -1,6 +1,8 @@ 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.clickable import androidx.compose.foundation.layout.Arrangement @@ -28,8 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCompositionContext -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -46,6 +47,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.SubcomposeAsyncImage +import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophonedroid.ui.Destination @@ -129,6 +131,13 @@ fun MessageListScreen( } } + var attachmentUris by remember { mutableStateOf>(mutableSetOf()) } + val imagePicker = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri? -> + uri?.let { + attachmentUris = attachmentUris.plus(it) + } + } + val navController = LocalNavController.current Scaffold( topBar = { @@ -138,8 +147,18 @@ fun MessageListScreen( messages = messageItems, paddingValues = padding, showSenders = viewModel.isGroupChat, + attachmentUris = attachmentUris, + onAddAttachment = { + imagePicker.launch("image/*") + }, + onClearAttachments = { + attachmentUris = setOf() + }, onSendMessage = { text -> - viewModel.enqueueOutgoingMessage(text) + viewModel.enqueueOutgoingMessage( + text = text, + attachmentUris = attachmentUris + ) } ) } @@ -150,6 +169,9 @@ fun MessageTranscript( messages: List, paddingValues: PaddingValues, showSenders: Boolean, + attachmentUris: Set, + onAddAttachment: () -> Unit, + onClearAttachments: () -> Unit, onSendMessage: (text: String) -> Unit, ) { val scrollState = rememberLazyListState() @@ -157,6 +179,13 @@ fun MessageTranscript( mutableStateOf(TextFieldValue()) } + val attachmentRowItems = attachmentUris.map { + AttachmentRowItem( + painter = rememberAsyncImagePainter(model = it), + id = "attachmentID" + ) + } + Column( Modifier .fillMaxSize() @@ -172,6 +201,9 @@ fun MessageTranscript( MessageEntry( onTextChanged = { textState = it }, textFieldValue = textState, + attachmentItems = attachmentRowItems, + onAddAttachment = onAddAttachment, + onClearAttachments = onClearAttachments, onSend = { onSendMessage(textState.text) @@ -198,7 +230,6 @@ fun Messages( .fillMaxSize() .padding(horizontal = 16.dp) ) { - var lastDate: Date = Date() val dateFormatter = SimpleDateFormat.getDateTimeInstance() for (index in messages.indices) { diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt index 936df13..8dc1cbd 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt @@ -1,6 +1,7 @@ package net.buzzert.kordophonedroid.ui.messagelist import android.content.Context +import android.net.Uri import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel @@ -97,7 +98,14 @@ class MessageListViewModel @Inject constructor( val isGroupChat: Boolean get() = conversation!!.isGroupChat - fun enqueueOutgoingMessage(text: String) { + fun enqueueOutgoingMessage( + text: String, + attachmentUris: Set + ) { + // 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( guid = UUID.randomUUID().toString(), text = text, diff --git a/app/src/main/res/drawable/attach_file.xml b/app/src/main/res/drawable/attach_file.xml new file mode 100644 index 0000000..6d9ac73 --- /dev/null +++ b/app/src/main/res/drawable/attach_file.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/sedona.jpeg b/app/src/main/res/drawable/sedona.jpeg new file mode 100644 index 0000000..4d62506 Binary files /dev/null and b/app/src/main/res/drawable/sedona.jpeg differ