Clean public chat, prepare for new implementation

This commit is contained in:
2025-12-05 21:13:14 +03:00
Unverified
parent 8064a69a04
commit 84e07e338d
6 changed files with 14 additions and 895 deletions
-8
View File
@@ -27,8 +27,6 @@ android {
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
@@ -89,11 +87,5 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.haze)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
+1 -1
View File
@@ -14,7 +14,7 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
+1 -1
View File
@@ -75,7 +75,7 @@ fun App() {
composable("chat") { MainScreen() }
composable("chats/publicChat") { PublicChatScreen() }
composable("chats/publicChat") { /* PublicChatScreen() */ }
}
}
}
+1 -44
View File
@@ -1,21 +1,13 @@
package ru.fromchat.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
@Composable
fun RowHeader(
@@ -42,39 +34,4 @@ fun RowHeader(
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
@Composable
fun GlassSurface(
modifier: Modifier = Modifier,
hazeState: HazeState,
shape: RoundedCornerShape = RoundedCornerShape(16.dp),
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.7f),
borderColor: Color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
borderWidth: androidx.compose.ui.unit.Dp = 1.dp,
content: @Composable () -> Unit
) {
androidx.compose.foundation.layout.Box(
modifier = modifier
.clip(shape)
.hazeChild(state = hazeState)
.background(backgroundColor)
.border(borderWidth, borderColor, shape)
.padding(16.dp)
) {
content()
}
}
@Composable
fun GlassContainer(
modifier: Modifier = Modifier,
hazeState: HazeState,
content: @Composable () -> Unit
) {
androidx.compose.foundation.layout.Box(
modifier = modifier
) {
content()
}
}
}
@@ -1,821 +0,0 @@
package ru.fromchat.ui
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.SolidColor
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.format
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
import ru.fromchat.DATETIME_FORMAT
import ru.fromchat.R
import ru.fromchat.api.ApiClient
import ru.fromchat.api.Message
import ru.fromchat.api.SendMessageRequest
import ru.fromchat.api.WebSocketCredentials
import ru.fromchat.api.WebSocketManager
import ru.fromchat.api.WebSocketMessage
import ru.fromchat.api.apiRequest
import ru.fromchat.utils.exclude
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun PublicChatScreen() {
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val hazeState = remember { HazeState() }
val messages = remember { mutableStateListOf<Message>() }
val typingUsers = remember { mutableStateMapOf<Int, String>() }
var loading by remember { mutableStateOf(true) }
var selectedMessageId by remember { mutableStateOf<Int?>(null) }
var showEditDialog by remember { mutableStateOf(false) }
var editingMessage by remember { mutableStateOf<Message?>(null) }
var showDeleteDialog by remember { mutableStateOf(false) }
var deletingMessageId by remember { mutableStateOf<Int?>(null) }
var replyingToMessage by remember { mutableStateOf<Message?>(null) }
var menuMessageId by remember { mutableStateOf<Int?>(null) }
var menuOffset by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) }
var typingJob: Job? = null
suspend fun scrollDown(animated: Boolean = true) {
if (messages.isNotEmpty()) {
if (animated) {
listState.animateScrollToItem(messages.lastIndex)
} else {
listState.scrollToItem(messages.lastIndex)
}
}
}
fun sendTyping() {
typingJob?.cancel()
typingJob = scope.launch {
try {
WebSocketManager.send(
WebSocketMessage(
type = "typing",
credentials = WebSocketCredentials(
scheme = "Bearer",
credentials = ApiClient.token!!
),
data = null
)
)
delay(3000)
WebSocketManager.send(
WebSocketMessage(
type = "stopTyping",
credentials = WebSocketCredentials(
scheme = "Bearer",
credentials = ApiClient.token!!
),
data = null
)
)
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to send typing", e)
}
}
}
LaunchedEffect(Unit) {
apiRequest {
ApiClient.getMessages()
}.onSuccess {
messages.clear()
messages += it.messages
}
WebSocketManager.connect()
WebSocketManager.addGlobalMessageHandler { msg ->
scope.launch {
when (msg.type) {
"newMessage" -> {
try {
val newMessage = ApiClient.json.decodeFromJsonElement<Message>(msg.data!!)
if (messages.none { it.id == newMessage.id }) {
messages += newMessage
scrollDown()
}
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to process incoming message:", e)
}
}
"messageEdited" -> {
try {
val editedMessage = ApiClient.json.decodeFromJsonElement<Message>(msg.data!!)
val index = messages.indexOfFirst { it.id == editedMessage.id }
if (index >= 0) {
messages[index] = editedMessage
}
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to process edited message:", e)
}
}
"messageDeleted" -> {
try {
val data = msg.data!!.jsonObject
val messageId = data["message_id"]?.toString()?.toIntOrNull()
if (messageId != null) {
messages.removeAll { it.id == messageId }
}
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to process deleted message:", e)
}
}
"typing" -> {
try {
val data = msg.data!!.jsonObject
val userId = data["userId"]?.toString()?.toIntOrNull()
val username = data["username"]?.toString()
if (userId != null && username != null && userId != ApiClient.user?.id) {
typingUsers[userId] = username
}
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to process typing:", e)
}
}
"stopTyping" -> {
try {
val data = msg.data!!.jsonObject
val userId = data["userId"]?.toString()?.toIntOrNull()
if (userId != null) {
typingUsers.remove(userId)
}
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to process stop typing:", e)
}
}
}
}
}
delay(500)
scrollDown(false)
loading = false
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.public_chat),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(
onClick = {
navController.navigateUp()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
},
bottomBar = {
Column(
modifier = Modifier
.hazeChild(state = hazeState)
.clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.7f))
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top))
.padding(vertical = 8.dp, horizontal = 12.dp)
) {
if (replyingToMessage != null) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.7f)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Replying to ${replyingToMessage!!.username}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)
Text(
text = replyingToMessage!!.content.take(50) + if (replyingToMessage!!.content.length > 50) "..." else "",
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(
onClick = { replyingToMessage = null },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Cancel reply",
modifier = Modifier.size(16.dp)
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
val message = rememberTextFieldState()
LaunchedEffect(message.text) {
if (message.text.isNotEmpty()) {
sendTyping()
}
}
BasicTextField(
state = message,
modifier = Modifier.weight(1f),
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 4),
decorator = { innerTextField ->
Box {
if (message.text.isEmpty()) {
Text(
text = stringResource(R.string.message_placeholder),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
innerTextField()
}
}
)
IconButton(
onClick = {
scope.launch {
val messageText = message.text.trim()
if (messageText.isEmpty()) return@launch
try {
val response = WebSocketManager.request(
WebSocketMessage(
type = "sendMessage",
credentials = WebSocketCredentials(
scheme = "Bearer",
credentials = ApiClient.token!!
),
data = ApiClient.json.encodeToJsonElement(
SendMessageRequest(
content = messageText.toString(),
reply_to_id = replyingToMessage?.id
)
)
)
)
if (response != null && response.error == null) {
message.setTextAndPlaceCursorAtEnd("")
replyingToMessage = null
typingJob?.cancel()
WebSocketManager.send(
WebSocketMessage(
type = "stopTyping",
credentials = WebSocketCredentials(
scheme = "Bearer",
credentials = ApiClient.token!!
),
data = null
)
)
} else {
Log.e("PublicChatScreen", "WebSocket error: ${response?.error}")
}
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to send message", e)
}
}
}
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Send,
contentDescription = "Send"
)
}
}
}
}
) { innerPadding ->
Box(Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
modifier = Modifier.padding(innerPadding)
) {
items(messages, { it.id }) { message ->
MessageBubble(
message = message,
isAuthor = message.user_id == ApiClient.user!!.id,
hazeState = hazeState,
onTap = { offset ->
menuMessageId = message.id
menuOffset = offset
},
onEdit = {
editingMessage = message
showEditDialog = true
menuMessageId = null
},
onDelete = {
deletingMessageId = message.id
showDeleteDialog = true
menuMessageId = null
},
onReply = {
replyingToMessage = message
menuMessageId = null
}
)
}
if (typingUsers.isNotEmpty()) {
item {
TypingIndicator(
users = typingUsers.values.toList(),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
}
}
if (menuMessageId != null) {
val message = messages.find { it.id == menuMessageId }
if (message != null) {
MessageMenu(
message = message,
offset = menuOffset,
onDismiss = { menuMessageId = null },
onEdit = {
editingMessage = message
showEditDialog = true
menuMessageId = null
},
onDelete = {
deletingMessageId = message.id
showDeleteDialog = true
menuMessageId = null
},
onReply = {
replyingToMessage = message
menuMessageId = null
},
hazeState = hazeState
)
}
}
}
AnimatedVisibility(
visible = loading,
enter = EnterTransition.None,
exit = fadeOut()
) {
Box(
Modifier
.background(MaterialTheme.colorScheme.surface)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LoadingIndicator(Modifier.size(70.dp))
}
}
}
if (showEditDialog && editingMessage != null) {
EditMessageDialog(
message = editingMessage!!,
onDismiss = {
showEditDialog = false
editingMessage = null
},
onConfirm = { newContent ->
scope.launch {
try {
apiRequest {
ApiClient.editMessage(editingMessage!!.id, newContent)
}
showEditDialog = false
editingMessage = null
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to edit message", e)
}
}
}
)
}
if (showDeleteDialog && deletingMessageId != null) {
DeleteMessageDialog(
onDismiss = {
showDeleteDialog = false
deletingMessageId = null
},
onConfirm = {
scope.launch {
try {
apiRequest {
ApiClient.deleteMessage(deletingMessageId!!)
}
showDeleteDialog = false
deletingMessageId = null
} catch (e: Exception) {
Log.e("PublicChatScreen", "Failed to delete message", e)
}
}
}
)
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun MessageBubble(
message: Message,
isAuthor: Boolean,
hazeState: HazeState,
onTap: (androidx.compose.ui.geometry.Offset) -> Unit,
onEdit: () -> Unit,
onDelete: () -> Unit,
onReply: () -> Unit
) {
var rowWidth by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp)
.fillMaxWidth()
.onSizeChanged {
rowWidth = with(density) {
it.width.toDp()
}
},
horizontalArrangement = if (isAuthor) Arrangement.End else Arrangement.Start
) {
val background = if (isAuthor)
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.7f)
else
MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.7f)
Box(
modifier = Modifier
.clip(
RoundedCornerShape(
topStart = if (isAuthor) 16.dp else 5.dp,
topEnd = 16.dp,
bottomEnd = if (isAuthor) 5.dp else 16.dp,
bottomStart = 16.dp
)
)
.hazeChild(state = hazeState)
.background(background)
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), RoundedCornerShape(
topStart = if (isAuthor) 16.dp else 5.dp,
topEnd = 16.dp,
bottomEnd = if (isAuthor) 5.dp else 16.dp,
bottomStart = 16.dp
))
.clickable {
onTap(androidx.compose.ui.geometry.Offset(0f, 0f))
}
.width(IntrinsicSize.Max)
.widthIn(max = rowWidth * 0.80f)
) {
Column(
modifier = Modifier.padding(10.dp)
) {
if (message.reply_to != null) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
)
) {
Column(
modifier = Modifier.padding(8.dp)
) {
Text(
text = message.reply_to.username,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.W600,
color = MaterialTheme.colorScheme.primary
)
Text(
text = message.reply_to.content,
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
if (!isAuthor) {
Text(
text = message.username,
fontWeight = FontWeight.W600,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(message.content)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth()
) {
Spacer(Modifier.weight(1f))
if (message.is_edited) {
Text(
text = "(edited)",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = Instant
.parse(message.utcTimestamp)
.toLocalDateTime(TimeZone.currentSystemDefault())
.format(DATETIME_FORMAT),
fontSize = 12.sp
)
if (isAuthor && message.is_read) {
Icon(
imageVector = Icons.Filled.DoneAll,
contentDescription = "Read",
modifier = Modifier.size(14.dp)
)
}
}
}
}
}
}
@Composable
fun TypingIndicator(
users: List<String>,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.7f)
)
) {
Text(
text = if (users.size == 1) {
"${users[0]} is typing..."
} else {
"${users.size} people are typing..."
},
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun MessageMenu(
message: Message,
offset: androidx.compose.ui.geometry.Offset,
onDismiss: () -> Unit,
onEdit: () -> Unit,
onDelete: () -> Unit,
onReply: () -> Unit,
hazeState: HazeState
) {
val isAuthor = message.user_id == ApiClient.user!!.id
Popup(
onDismissRequest = onDismiss,
offset = androidx.compose.ui.unit.IntOffset(offset.x.toInt(), offset.y.toInt()),
properties = PopupProperties(focusable = true)
) {
GlassSurface(
hazeState = hazeState,
shape = RoundedCornerShape(12.dp)
) {
Column {
MenuItem(
icon = Icons.AutoMirrored.Filled.Reply,
text = "Reply",
onClick = {
onReply()
onDismiss()
}
)
if (isAuthor) {
MenuItem(
icon = Icons.Default.Edit,
text = "Edit",
onClick = {
onEdit()
onDismiss()
}
)
MenuItem(
icon = Icons.Default.Delete,
text = "Delete",
onClick = {
onDelete()
onDismiss()
}
)
}
}
}
}
}
@Composable
fun MenuItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = icon,
contentDescription = text,
modifier = Modifier.size(20.dp)
)
Text(text)
}
}
@Composable
fun EditMessageDialog(
message: Message,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
var text by remember { mutableStateOf(message.content) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Edit Message") },
text = {
BasicTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.fillMaxWidth(),
textStyle = LocalTextStyle.current,
singleLine = false
)
},
confirmButton = {
TextButton(
onClick = {
if (text.trim().isNotEmpty()) {
onConfirm(text.trim())
}
}
) {
Text("Save")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
@Composable
fun DeleteMessageDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Delete Message") },
text = { Text("Are you sure you want to delete this message?") },
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
+11 -20
View File
@@ -1,28 +1,22 @@
[versions]
agp = "8.13.0"
kotlin = "2.2.20"
agp = "8.13.1"
kotlin = "2.2.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxDatetime = "0.7.1"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2025.09.01"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.0"
composeBom = "2025.11.01"
materialIconsExtended = "1.7.8"
navigationCompose = "2.9.5"
ktor = "3.3.0"
navigationCompose = "2.9.6"
ktor = "3.3.3"
kotlinxSerializationJson = "1.9.0"
compose = "1.9.2"
material3 = "1.5.0-alpha04"
haze = "0.7.0"
compose = "1.9.5"
material3 = "1.5.0-alpha09"
haze = "1.7.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
@@ -30,8 +24,6 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compo
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
@@ -48,5 +40,4 @@ haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }