Home and Library screen update (#101)

* Added support for filtering file types in synced folders

* Added support for custom file names and enhanced file information dialog

* added feedback option in the homescreen drawer

* Implemented a limit for number of recent files displayed on the home screen
This commit is contained in:
Aryan 2026-03-20 21:51:03 +05:30 committed by GitHub
parent 9842aea9b1
commit ddcd253c7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 637 additions and 734 deletions

View file

@ -19,20 +19,8 @@
*/
package com.aryan.reader.feedback
import android.net.Uri
import timber.log.Timber
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import android.content.Intent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -41,72 +29,34 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.outlined.Email
import androidx.compose.material.icons.outlined.Feedback
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.aryan.reader.R
import com.aryan.reader.data.FeedbackMessage
import com.aryan.reader.data.FeedbackThread
import java.text.SimpleDateFormat
import java.util.Locale
import android.content.Intent
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.outlined.Email
import androidx.core.net.toUri
import timber.log.Timber
private fun launchEmailFeedback(context: android.content.Context) {
val intent = Intent(Intent.ACTION_SENDTO).apply {
@ -126,583 +76,135 @@ fun FeedbackScreen(
navController: NavHostController,
viewModel: FeedbackViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() }
val activeThreadsCount = remember(uiState.threads) { uiState.threads.count { it.status == "open" } }
val isLimitReached = activeThreadsCount >= 3
// State for Tabs
var selectedTabIndex by remember { mutableIntStateOf(0) }
LaunchedEffect(uiState.errorMessage) {
uiState.errorMessage?.let {
snackbarHostState.showSnackbar(it)
viewModel.clearError()
}
}
BackHandler(enabled = uiState.selectedThreadId != null) {
viewModel.onBackToThreadList()
}
// Helper to determine if current chat is closed
val currentThread = remember(uiState.selectedThreadId, uiState.threads) {
uiState.threads.find { it.id == uiState.selectedThreadId }
}
val isCurrentChatClosed = currentThread?.status == "closed"
val uriHandler = LocalUriHandler.current
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TopAppBar(
title = {
Text(if (uiState.selectedThreadId == null) "Help & Feedback" else "Support Chat")
},
title = { Text("Help & Feedback") },
navigationIcon = {
IconButton(onClick = {
if (uiState.selectedThreadId != null) {
viewModel.onBackToThreadList()
} else {
navController.popBackStack()
}
}) {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
if (uiState.selectedThreadId == null) {
IconButton(onClick = { launchEmailFeedback(context) }) {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = "Send Email Feedback"
)
}
}
}
)
},
floatingActionButton = {
if (uiState.selectedThreadId == null && selectedTabIndex == 0) {
Column(horizontalAlignment = Alignment.End) {
ExtendedFloatingActionButton(
onClick = { if (!isLimitReached) viewModel.onStartCreateTicket() },
icon = { Icon(Icons.Default.Add, "New Ticket") },
text = { Text("New Ticket") },
containerColor = if (isLimitReached) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.primaryContainer,
contentColor = if (isLimitReached) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
if (uiState.selectedThreadId == null) {
// TABS & LIST
Column(modifier = Modifier.fillMaxSize()) {
TabRow(selectedTabIndex = selectedTabIndex) {
Tab(
selected = selectedTabIndex == 0,
onClick = { selectedTabIndex = 0 },
text = { Text("Active") }
)
Tab(
selected = selectedTabIndex == 1,
onClick = { selectedTabIndex = 1 },
text = { Text("Closed") }
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
// Limit Hint Text
if (selectedTabIndex == 0 && isLimitReached) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Limit of 3 active tickets reached.",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Icon(
imageVector = Icons.Outlined.Feedback,
contentDescription = null,
modifier = Modifier.size(72.dp),
tint = MaterialTheme.colorScheme.primary
)
val displayedThreads = if (selectedTabIndex == 0) {
uiState.threads.filter { it.status == "open" }
} else {
uiState.threads.filter { it.status == "closed" }
}
Spacer(modifier = Modifier.height(16.dp))
ThreadList(
threads = displayedThreads,
onThreadClick = { viewModel.onThreadSelected(it.id) },
emptyMessage = if (selectedTabIndex == 0) "No active tickets" else "No closed tickets",
onEmailClick = { launchEmailFeedback(context) }
Text(
text = "Get in Touch",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Found a bug, have a feature request, or just want to say hi? Let us know on GitHub or send us an email.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(48.dp))
FeedbackOptionCard(
title = "GitHub Issues",
description = "Report bugs, request features, and track development progress.",
icon = {
Icon(
painter = painterResource(id = R.drawable.github),
contentDescription = "GitHub",
modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.primary
)
},
onClick = {
uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme/issues")
}
} else {
ChatView(
messages = uiState.currentMessages,
pendingMessages = uiState.pendingMessages,
inputMessage = uiState.chatInputMessage,
inputAttachments = uiState.chatInputAttachments,
onInputChange = { viewModel.onChatInputChange(it) },
onAttachmentsSelected = { viewModel.onChatImagesSelected(it) },
onRemoveAttachment = { viewModel.onRemoveChatImage(it) },
onSend = { viewModel.onSendMessage() },
isClosed = isCurrentChatClosed
)
}
)
if (uiState.isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter))
}
}
}
Spacer(modifier = Modifier.height(16.dp))
if (uiState.isCreatingTicket) {
CreateTicketDialog(
message = uiState.newTicketMessage,
category = uiState.newTicketCategory,
attachments = uiState.newTicketAttachments,
onMessageChange = viewModel::onNewTicketMessageChange,
onCategoryChange = viewModel::onNewTicketCategoryChange,
onAttachmentsSelected = viewModel::onNewTicketImagesSelected,
onRemoveAttachment = viewModel::onRemoveNewTicketImage,
onSubmit = viewModel::onSubmitTicket,
onDismiss = viewModel::onCancelCreateTicket
)
}
}
@Composable
fun ThreadList(
threads: List<FeedbackThread>,
onThreadClick: (FeedbackThread) -> Unit,
emptyMessage: String = "No conversations yet",
onEmailClick: () -> Unit
) {
if (threads.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = emptyMessage,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Prefer email or can't sign in?",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline
FeedbackOptionCard(
title = "Email Support",
description = "Contact us directly via email for any other inquiries.",
icon = {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = "Email",
modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.primary
)
TextButton(onClick = onEmailClick) {
Icon(
Icons.Default.Email,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Contact via Email")
}
},
onClick = {
launchEmailFeedback(context)
}
}
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(threads, key = { it.id }) { thread ->
ThreadItem(thread = thread, onClick = { onThreadClick(thread) })
HorizontalDivider()
}
)
Spacer(modifier = Modifier.weight(1f))
}
}
}
@Composable
fun ThreadItem(
thread: FeedbackThread,
private fun FeedbackOptionCard(
title: String,
description: String,
icon: @Composable () -> Unit,
onClick: () -> Unit
) {
val dateStr = remember(thread.lastUpdated) {
thread.lastUpdated?.let {
SimpleDateFormat("MMM d", Locale.getDefault()).format(it)
} ?: "Just now"
}
val itemAlpha = if (thread.status == "closed") 0.6f else 1f
ListItem(
modifier = Modifier
.clickable(onClick = onClick)
.graphicsLayer { alpha = itemAlpha },
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(thread.category, fontWeight = FontWeight.Bold)
if (thread.hasUnreadAdminReply) {
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.size(8.dp)
.background(MaterialTheme.colorScheme.error, CircleShape)
)
}
}
},
supportingContent = {
val isPreviewEmpty = thread.preview.isBlank()
val previewText = if (isPreviewEmpty) "Attached Image" else thread.preview
Text(
text = previewText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontStyle = if (isPreviewEmpty) FontStyle.Italic else FontStyle.Normal
)
},
trailingContent = {
Text(dateStr, style = MaterialTheme.typography.labelSmall)
}
)
}
@Composable
fun ChatView(
messages: List<FeedbackMessage>,
pendingMessages: List<FeedbackMessage>,
inputMessage: String,
inputAttachments: List<Uri>,
onInputChange: (String) -> Unit,
onAttachmentsSelected: (List<Uri>) -> Unit,
onRemoveAttachment: (Uri) -> Unit,
onSend: () -> Unit,
isClosed: Boolean = false
) {
val imagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(5),
onResult = { uris -> if (uris.isNotEmpty()) onAttachmentsSelected(uris) }
)
val listState = rememberLazyListState()
val displayedMessages = remember(messages, pendingMessages) {
val realIds = messages.map { it.id }.toSet()
val uniquePending = pendingMessages.filter { it.id !in realIds }
Timber.d("ChatView Recomposition: ${messages.size} real, ${uniquePending.size} pending unique")
messages + uniquePending
}
Column(modifier = Modifier.fillMaxSize()) {
LaunchedEffect(displayedMessages.size) {
if (displayedMessages.isNotEmpty()) {
listState.animateScrollToItem(displayedMessages.size - 1)
}
}
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp),
contentPadding = PaddingValues(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(displayedMessages, key = { it.id }) { msg ->
MessageBubble(
message = msg,
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
)
}
}
Surface(
tonalElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.fillMaxWidth()) {
if (isClosed) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "This ticket is closed.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium
)
}
} else {
if (inputAttachments.isNotEmpty()) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, top = 8.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(inputAttachments) { uri ->
Box(modifier = Modifier.size(60.dp)) {
AsyncImage(
model = uri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(8.dp))
)
Icon(
Icons.Default.Close,
contentDescription = "Remove",
modifier = Modifier
.size(20.dp)
.align(Alignment.TopEnd)
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f), CircleShape)
.padding(2.dp)
.clickable { onRemoveAttachment(uri) },
tint = androidx.compose.ui.graphics.Color.White
)
}
}
}
}
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { imagePickerLauncher.launch(PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly)) }) {
Icon(
painter = painterResource(id = R.drawable.image),
contentDescription = "Add Image",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
OutlinedTextField(
value = inputMessage,
onValueChange = onInputChange,
modifier = Modifier.weight(1f),
placeholder = { Text("Type a message...") },
maxLines = 3,
shape = RoundedCornerShape(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = onSend,
enabled = inputMessage.isNotBlank() || inputAttachments.isNotEmpty()
) {
Icon(
Icons.AutoMirrored.Filled.Send,
contentDescription = "Send",
tint = if (inputMessage.isNotBlank() || inputAttachments.isNotEmpty()) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
)
}
}
}
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MessageBubble(
message: FeedbackMessage,
modifier: Modifier = Modifier
) {
val isMe = message.sender == "user"
val alignment = if (isMe) Alignment.End else Alignment.Start
val color = if (isMe) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer
val textColor = if (isMe) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = alignment
OutlinedCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp)
) {
Surface(
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = if (isMe) 16.dp else 4.dp,
bottomEnd = if (isMe) 4.dp else 16.dp
),
color = color,
modifier = Modifier.widthIn(max = 280.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.padding(12.dp)) {
if (message.attachments.isNotEmpty()) {
FlowRow(
modifier = Modifier.padding(bottom = if(message.text.isNotEmpty()) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
message.attachments.forEach { url ->
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = "Attachment",
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentScale = ContentScale.Crop,
error = {
Box(contentAlignment = Alignment.Center) {
Icon(
painter = painterResource(id = R.drawable.broken_image),
contentDescription = "Image unavailable",
modifier = Modifier.padding(24.dp).fillMaxSize(),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
}
}
)
}
}
}
if (message.text.isNotEmpty()) {
Text(
text = message.text,
color = textColor
)
}
icon()
Spacer(modifier = Modifier.width(20.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
message.timestamp?.let {
Text(
text = SimpleDateFormat("h:mm a", Locale.getDefault()).format(it),
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(top = 4.dp),
color = MaterialTheme.colorScheme.outline
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Open",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateTicketDialog(
message: String,
category: String,
attachments: List<Uri>,
onMessageChange: (String) -> Unit,
onCategoryChange: (String) -> Unit,
onAttachmentsSelected: (List<Uri>) -> Unit,
onRemoveAttachment: (Uri) -> Unit,
onSubmit: () -> Unit,
onDismiss: () -> Unit
) {
val imagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(3),
onResult = { uris -> if (uris.isNotEmpty()) onAttachmentsSelected(uris) }
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("New Ticket") },
text = {
Column {
val categories = listOf("Bug Report", "Feature Request", "Feedback", "Other")
Text("Category", style = MaterialTheme.typography.labelLarge)
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
categories.take(2).forEach { cat ->
FilterChip(
selected = category == cat,
onClick = { onCategoryChange(cat) },
label = { Text(cat) },
modifier = Modifier.padding(end = 8.dp)
)
}
}
OutlinedTextField(
value = message,
onValueChange = onMessageChange,
label = { Text("Describe your issue...") },
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
shape = RoundedCornerShape(8.dp)
)
Row(
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextButton(
onClick = { imagePickerLauncher.launch(PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly)) }
) {
Icon(painter = painterResource(id = R.drawable.image), contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Add Image")
}
Spacer(modifier = Modifier.weight(1f))
Text(
"${attachments.size}/3",
style = MaterialTheme.typography.labelSmall
)
}
if (attachments.isNotEmpty()) {
LazyRow(
modifier = Modifier.padding(top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(attachments) { uri ->
Box(modifier = Modifier.size(60.dp)) {
AsyncImage(
model = uri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.matchParentSize()
.clip(RoundedCornerShape(8.dp))
)
Icon(
Icons.Default.Close,
contentDescription = "Remove",
modifier = Modifier
.size(20.dp)
.align(Alignment.TopEnd)
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f), CircleShape)
.padding(2.dp)
.clickable { onRemoveAttachment(uri) },
tint = androidx.compose.ui.graphics.Color.White
)
}
}
}
}
}
},
confirmButton = {
TextButton(
onClick = onSubmit,
enabled = message.isNotBlank()
) {
Text("Submit")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}