Compare commits

...

2 Commits

Author SHA1 Message Date
2169f28632 version bump 2026-03-02 18:21:36 +00:00
21aed4f682 sync fix and reactions 2026-03-02 18:20:07 +00:00
5 changed files with 407 additions and 21 deletions

View File

@@ -13,7 +13,9 @@
"WebFetch(domain:raw.githubusercontent.com)", "WebFetch(domain:raw.githubusercontent.com)",
"Bash(export TERM=dumb:*)", "Bash(export TERM=dumb:*)",
"Bash(grep:*)", "Bash(grep:*)",
"Bash(jar tf:*)" "Bash(jar tf:*)",
"Bash(javap:*)",
"Bash(jar xf:*)"
] ]
} }
} }

View File

@@ -15,7 +15,7 @@ android {
minSdk = 34 minSdk = 34
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -50,6 +50,9 @@ fun MainScreen(
val expandedThreadRooms by viewModel.expandedThreadRooms.collectAsStateWithLifecycle() val expandedThreadRooms by viewModel.expandedThreadRooms.collectAsStateWithLifecycle()
val selectedThread by viewModel.selectedThread.collectAsStateWithLifecycle() val selectedThread by viewModel.selectedThread.collectAsStateWithLifecycle()
val threadMessages by viewModel.threadMessages.collectAsStateWithLifecycle() val threadMessages by viewModel.threadMessages.collectAsStateWithLifecycle()
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
val replyingTo by viewModel.replyingTo.collectAsStateWithLifecycle()
val editingMessage by viewModel.editingMessage.collectAsStateWithLifecycle()
val listState = viewModel.channelListState val listState = viewModel.channelListState
val preferencesManager: PreferencesManager = koinInject() val preferencesManager: PreferencesManager = koinInject()
val hideSpacesWhenClosed by preferencesManager.hideSpacesWhenClosed.collectAsStateWithLifecycle(initialValue = false) val hideSpacesWhenClosed by preferencesManager.hideSpacesWhenClosed.collectAsStateWithLifecycle(initialValue = false)
@@ -128,6 +131,17 @@ fun MainScreen(
} }
} }
}, },
currentUserId = currentUserId,
replyingTo = replyingTo,
editingMessage = editingMessage,
onSetReplyingTo = { viewModel.setReplyingTo(it) },
onSetEditingMessage = { viewModel.setEditingMessage(it) },
onSendReply = { body, eventId -> viewModel.sendReply(body, eventId) },
onSendThreadReply = { body, eventId -> viewModel.sendThreadReply(body, eventId) },
onEditMessage = { eventId, body -> viewModel.editMessage(eventId, body) },
onEditThreadMessage = { eventId, body -> viewModel.editThreadMessage(eventId, body) },
onSendReaction = { eventId, emoji -> viewModel.sendReaction(eventId, emoji) },
onSendThreadReaction = { eventId, emoji -> viewModel.sendThreadReaction(eventId, emoji) },
) )
AnimatedVisibility(visible = showMemberList) { AnimatedVisibility(visible = showMemberList) {

View File

@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EditedContent
import org.matrix.rustcomponents.sdk.EventOrTransactionId import org.matrix.rustcomponents.sdk.EventOrTransactionId
import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.MembershipState import org.matrix.rustcomponents.sdk.MembershipState
@@ -185,6 +186,15 @@ class MainViewModel(
private val _homeUnreadStatus = MutableStateFlow(UnreadStatus.NONE) private val _homeUnreadStatus = MutableStateFlow(UnreadStatus.NONE)
val homeUnreadStatus: StateFlow<UnreadStatus> = _homeUnreadStatus val homeUnreadStatus: StateFlow<UnreadStatus> = _homeUnreadStatus
private val _currentUserId = MutableStateFlow<String?>(null)
val currentUserId: StateFlow<String?> = _currentUserId
private val _replyingTo = MutableStateFlow<MessageItem?>(null)
val replyingTo: StateFlow<MessageItem?> = _replyingTo
private val _editingMessage = MutableStateFlow<MessageItem?>(null)
val editingMessage: StateFlow<MessageItem?> = _editingMessage
private val _spaceChildrenMap = MutableStateFlow<Map<String, Set<String>>>(emptyMap()) private val _spaceChildrenMap = MutableStateFlow<Map<String, Set<String>>>(emptyMap())
private val _channelSections = MutableStateFlow<List<ChannelSection>>(emptyList()) private val _channelSections = MutableStateFlow<List<ChannelSection>>(emptyList())
@@ -271,6 +281,7 @@ class MainViewModel(
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
syncService = authRepository.getOrStartSync() syncService = authRepository.getOrStartSync()
try { _currentUserId.value = authRepository.getClient()?.userId() } catch (_: Exception) {}
loadRooms() loadRooms()
} }
observeSelectedChannel() observeSelectedChannel()
@@ -545,11 +556,46 @@ class MainViewModel(
} }
is TimelineDiff.Set -> { is TimelineDiff.Set -> {
val idx = d.index.toInt() val idx = d.index.toInt()
if (idx in sdkItems.indices) sdkItems[idx] = d.value if (idx in sdkItems.indices) {
// Remove the old item from cache (e.g. TransactionId → EventId transition on send)
val old = sdkItems[idx]
sdkItems[idx] = d.value
val oldEvent = old.asEvent()
if (oldEvent != null) {
val oldId = when (val eot = oldEvent.eventOrTransactionId) {
is EventOrTransactionId.EventId -> eot.eventId
is EventOrTransactionId.TransactionId -> eot.transactionId
}
if (ids.remove(oldId)) {
val ri = cached.indexOfFirst { it.eventId == oldId }
if (ri >= 0) cached.removeAt(ri)
// Also remove from thread cache if it was a thread reply
threadMessageCache[roomId]?.values?.forEach { list ->
list.removeAll { it.eventId == oldId }
}
}
}
}
} }
is TimelineDiff.Remove -> { is TimelineDiff.Remove -> {
val idx = d.index.toInt() val idx = d.index.toInt()
if (idx in sdkItems.indices) sdkItems.removeAt(idx) if (idx in sdkItems.indices) {
val removed = sdkItems.removeAt(idx)
val removedEvent = removed.asEvent()
if (removedEvent != null) {
val removedId = when (val eot = removedEvent.eventOrTransactionId) {
is EventOrTransactionId.EventId -> eot.eventId
is EventOrTransactionId.TransactionId -> eot.transactionId
}
if (ids.remove(removedId)) {
val ri = cached.indexOfFirst { it.eventId == removedId }
if (ri >= 0) cached.removeAt(ri)
threadMessageCache[roomId]?.values?.forEach { list ->
list.removeAll { it.eventId == removedId }
}
}
}
}
} }
is TimelineDiff.Truncate -> { is TimelineDiff.Truncate -> {
val len = d.length.toInt() val len = d.length.toInt()
@@ -1442,6 +1488,71 @@ class MainViewModel(
} }
} }
fun setReplyingTo(message: MessageItem?) { _replyingTo.value = message }
fun setEditingMessage(message: MessageItem?) { _editingMessage.value = message }
fun editMessage(eventId: String, newBody: String) {
val timeline = activeTimeline ?: return
viewModelScope.launch(Dispatchers.IO) {
try {
timeline.edit(
EventOrTransactionId.EventId(eventId),
EditedContent.RoomMessage(messageEventContentFromMarkdown(newBody)),
)
} catch (_: Exception) {}
_editingMessage.value = null
}
}
fun editThreadMessage(eventId: String, newBody: String) {
viewModelScope.launch(Dispatchers.IO) {
threadTimelineMutex.withLock {
val t = activeThreadTimeline ?: return@withLock
try {
t.edit(
EventOrTransactionId.EventId(eventId),
EditedContent.RoomMessage(messageEventContentFromMarkdown(newBody)),
)
} catch (_: Exception) {}
_editingMessage.value = null
}
}
}
fun sendReaction(eventId: String, emoji: String) {
val timeline = activeTimeline ?: return
viewModelScope.launch(Dispatchers.IO) {
try { timeline.toggleReaction(EventOrTransactionId.EventId(eventId), emoji) } catch (_: Exception) {}
}
}
fun sendThreadReaction(eventId: String, emoji: String) {
viewModelScope.launch(Dispatchers.IO) {
threadTimelineMutex.withLock {
val t = activeThreadTimeline ?: return@withLock
try { t.toggleReaction(EventOrTransactionId.EventId(eventId), emoji) } catch (_: Exception) {}
}
}
}
fun sendReply(body: String, replyToEventId: String) {
val timeline = activeTimeline ?: return
viewModelScope.launch(Dispatchers.IO) {
try { timeline.sendReply(messageEventContentFromMarkdown(body), replyToEventId) } catch (_: Exception) {}
_replyingTo.value = null
}
}
fun sendThreadReply(body: String, replyToEventId: String) {
viewModelScope.launch(Dispatchers.IO) {
threadTimelineMutex.withLock {
val t = activeThreadTimeline ?: return@withLock
try { t.sendReply(messageEventContentFromMarkdown(body), replyToEventId) } catch (_: Exception) {}
_replyingTo.value = null
}
}
}
fun logout() { fun logout() {
ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver) ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
viewModelScope.launch { viewModelScope.launch {

View File

@@ -55,15 +55,23 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Surface
import androidx.compose.material3.TextButton
import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownColor import com.mikepenz.markdown.m3.markdownColor
import com.mikepenz.markdown.m3.markdownTypography import com.mikepenz.markdown.m3.markdownTypography
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.filled.PlayCircleFilled import androidx.compose.material.icons.filled.PlayCircleFilled
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -140,9 +148,21 @@ fun MessageTimeline(
onOpenThread: (String) -> Unit = {}, onOpenThread: (String) -> Unit = {},
threadReplyCounts: Map<String, Int> = emptyMap(), threadReplyCounts: Map<String, Int> = emptyMap(),
selectedThreadName: String? = null, selectedThreadName: String? = null,
currentUserId: String? = null,
replyingTo: MessageItem? = null,
editingMessage: MessageItem? = null,
onSetReplyingTo: (MessageItem?) -> Unit = {},
onSetEditingMessage: (MessageItem?) -> Unit = {},
onSendReply: (String, String) -> Unit = { _, _ -> },
onSendThreadReply: (String, String) -> Unit = { _, _ -> },
onEditMessage: (String, String) -> Unit = { _, _ -> },
onEditThreadMessage: (String, String) -> Unit = { _, _ -> },
onSendReaction: (String, String) -> Unit = { _, _ -> },
onSendThreadReaction: (String, String) -> Unit = { _, _ -> },
) { ) {
var fullscreenImageUrl by remember { mutableStateOf<String?>(null) } var fullscreenImageUrl by remember { mutableStateOf<String?>(null) }
var fullscreenVideoUrl by remember { mutableStateOf<String?>(null) } var fullscreenVideoUrl by remember { mutableStateOf<String?>(null) }
var contextMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
if (fullscreenImageUrl != null) { if (fullscreenImageUrl != null) {
FullscreenImageViewer( FullscreenImageViewer(
@@ -158,6 +178,32 @@ fun MessageTimeline(
) )
} }
contextMenuMessage?.let { msg ->
MessageContextMenu(
message = msg,
isOwnMessage = msg.senderId == currentUserId,
isInThread = selectedThread != null,
onDismiss = { contextMenuMessage = null },
onReact = { emoji ->
contextMenuMessage = null
if (selectedThread != null) onSendThreadReaction(msg.eventId, emoji)
else onSendReaction(msg.eventId, emoji)
},
onReply = {
onSetReplyingTo(msg)
contextMenuMessage = null
},
onEdit = {
onSetEditingMessage(msg)
contextMenuMessage = null
},
onStartThread = {
onOpenThread(msg.eventId)
contextMenuMessage = null
},
)
}
CompositionLocalProvider( CompositionLocalProvider(
LocalImageViewer provides { url -> fullscreenImageUrl = url }, LocalImageViewer provides { url -> fullscreenImageUrl = url },
LocalVideoPlayer provides { url -> fullscreenVideoUrl = url }, LocalVideoPlayer provides { url -> fullscreenVideoUrl = url },
@@ -281,17 +327,17 @@ fun MessageTimeline(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
if (isFirstInGroup) { if (isFirstInGroup) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
FullMessage(message, onOpenThread = onOpenThread, threadReplyCount = threadReplyCounts[message.eventId] ?: 0) FullMessage(message, onOpenThread = onOpenThread, threadReplyCount = threadReplyCounts[message.eventId] ?: 0, onLongPress = { contextMenuMessage = it })
} else { } else {
CompactMessage(message) CompactMessage(message, onLongPress = { contextMenuMessage = it })
} }
} }
} else { } else {
if (isFirstInGroup) { if (isFirstInGroup) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
FullMessage(message, onOpenThread = onOpenThread) FullMessage(message, onOpenThread = onOpenThread, onLongPress = { contextMenuMessage = it })
} else { } else {
CompactMessage(message) CompactMessage(message, onLongPress = { contextMenuMessage = it })
} }
} }
} }
@@ -319,10 +365,41 @@ fun MessageTimeline(
} }
HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant) HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant)
// Reply preview bar
replyingTo?.let { replyMsg ->
ReplyPreviewBar(
senderName = replyMsg.senderName,
body = when (val c = replyMsg.content) {
is com.example.fluffytrix.ui.screens.main.MessageContent.Text -> c.body
else -> "Attachment"
},
onDismiss = { onSetReplyingTo(null) },
)
}
// Edit mode bar
editingMessage?.let { editMsg ->
EditModeBar(
body = when (val c = editMsg.content) {
is com.example.fluffytrix.ui.screens.main.MessageContent.Text -> c.body
else -> ""
},
onDismiss = { onSetEditingMessage(null) },
)
}
MessageInput( MessageInput(
channelName = if (selectedThread != null) "thread" else (channelName ?: "message"), channelName = if (selectedThread != null) "thread" else (channelName ?: "message"),
replyingTo = replyingTo,
editingMessage = editingMessage,
onSendMessage = activeSend, onSendMessage = activeSend,
onSendFiles = onSendFiles, onSendFiles = onSendFiles,
onSendReply = { body, eventId ->
if (selectedThread != null) onSendThreadReply(body, eventId)
else onSendReply(body, eventId)
},
onEditMessage = { eventId, body ->
if (selectedThread != null) onEditThreadMessage(eventId, body)
else onEditMessage(eventId, body)
},
) )
} }
} }
@@ -371,12 +448,12 @@ private fun ThreadTopBar(title: String, onClose: () -> Unit) {
} }
@Composable @Composable
private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {}, threadReplyCount: Int = 0) { private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {}, threadReplyCount: Int = 0, onLongPress: (MessageItem) -> Unit = {}) {
val senderColor = remember(message.senderName) { colorForSender(message.senderName) } val senderColor = remember(message.senderName) { colorForSender(message.senderName) }
val time = remember(message.timestamp) { formatTimestamp(message.timestamp) } val time = remember(message.timestamp) { formatTimestamp(message.timestamp) }
val reply = message.replyTo val reply = message.replyTo
Column { Column(modifier = Modifier.combinedClickable(onClick = {}, onLongClick = { onLongPress(message) })) {
if (reply != null) { if (reply != null) {
ReplyConnector(reply, hasAvatar = true) ReplyConnector(reply, hasAvatar = true)
} }
@@ -427,8 +504,8 @@ private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {
} }
@Composable @Composable
private fun CompactMessage(message: MessageItem) { private fun CompactMessage(message: MessageItem, onLongPress: (MessageItem) -> Unit = {}) {
Column { Column(modifier = Modifier.combinedClickable(onClick = {}, onLongClick = { onLongPress(message) })) {
if (message.replyTo != null) { if (message.replyTo != null) {
ReplyConnector(message.replyTo, hasAvatar = false) ReplyConnector(message.replyTo, hasAvatar = false)
} }
@@ -807,9 +884,27 @@ private fun formatFileSize(bytes: Long): String {
} }
@Composable @Composable
private fun MessageInput(channelName: String, onSendMessage: (String) -> Unit, onSendFiles: (List<Uri>, String?) -> Unit) { private fun MessageInput(
channelName: String,
onSendMessage: (String) -> Unit,
onSendFiles: (List<Uri>, String?) -> Unit,
replyingTo: MessageItem? = null,
editingMessage: MessageItem? = null,
onSendReply: (String, String) -> Unit = { _, _ -> },
onEditMessage: (String, String) -> Unit = { _, _ -> },
) {
var text by remember { mutableStateOf("") } var text by remember { mutableStateOf("") }
var attachedUris by remember { mutableStateOf(listOf<Uri>()) } var attachedUris by remember { mutableStateOf(listOf<Uri>()) }
// Pre-fill text when entering edit mode
androidx.compose.runtime.LaunchedEffect(editingMessage) {
if (editingMessage != null) {
text = when (val c = editingMessage.content) {
is com.example.fluffytrix.ui.screens.main.MessageContent.Text -> c.body
else -> ""
}
}
}
val filePickerLauncher = rememberLauncherForActivityResult( val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents() contract = ActivityResultContracts.GetMultipleContents()
) { uris: List<Uri> -> ) { uris: List<Uri> ->
@@ -887,22 +982,186 @@ private fun MessageInput(channelName: String, onSendMessage: (String) -> Unit, o
) )
IconButton( IconButton(
onClick = { onClick = {
if (attachedUris.isNotEmpty()) { val trimmed = text.trim()
onSendFiles(attachedUris, text.trim().ifBlank { null }) when {
attachedUris = emptyList() editingMessage != null && trimmed.isNotBlank() -> {
text = "" onEditMessage(editingMessage.eventId, trimmed)
} else if (text.isNotBlank()) { text = ""
onSendMessage(text.trim()) }
text = "" replyingTo != null && trimmed.isNotBlank() -> {
onSendReply(trimmed, replyingTo.eventId)
text = ""
}
attachedUris.isNotEmpty() -> {
onSendFiles(attachedUris, trimmed.ifBlank { null })
attachedUris = emptyList()
text = ""
}
trimmed.isNotBlank() -> {
onSendMessage(trimmed)
text = ""
}
} }
}, },
enabled = canSend, enabled = canSend,
) { ) {
Icon( Icon(
Icons.AutoMirrored.Filled.Send, "Send", if (editingMessage != null) Icons.Default.Check else Icons.AutoMirrored.Filled.Send,
if (editingMessage != null) "Confirm edit" else "Send",
tint = if (canSend) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, tint = if (canSend) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
} }
} }
} }
private val QUICK_REACTIONS = listOf("👍", "👎", "❤️", "😂", "😮", "😢", "🎉", "🔥")
@Composable
private fun MessageContextMenu(
message: MessageItem,
isOwnMessage: Boolean,
isInThread: Boolean,
onDismiss: () -> Unit,
onReact: (String) -> Unit,
onReply: () -> Unit,
onEdit: () -> Unit,
onStartThread: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = null,
text = {
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
// Quick emoji reactions
LazyRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(vertical = 8.dp),
) {
items(QUICK_REACTIONS) { emoji ->
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { onReact(emoji) },
contentAlignment = Alignment.Center,
) {
Text(emoji, fontSize = 20.sp)
}
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
TextButton(
onClick = onReply,
modifier = Modifier.fillMaxWidth(),
) {
Icon(Icons.AutoMirrored.Filled.Reply, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Reply", modifier = Modifier.weight(1f))
}
if (isOwnMessage) {
TextButton(
onClick = onEdit,
modifier = Modifier.fillMaxWidth(),
) {
Icon(Icons.Default.Check, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Edit", modifier = Modifier.weight(1f))
}
}
if (!isInThread) {
TextButton(
onClick = onStartThread,
modifier = Modifier.fillMaxWidth(),
) {
Icon(Icons.Default.Tag, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Start Thread", modifier = Modifier.weight(1f))
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
},
)
}
@Composable
private fun ReplyPreviewBar(senderName: String, body: String, onDismiss: () -> Unit) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.width(3.dp)
.height(36.dp)
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(2.dp)),
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
"Replying to $senderName",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary,
)
Text(
body,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
IconButton(onClick = onDismiss, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Close, "Cancel reply", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
@Composable
private fun EditModeBar(body: String, onDismiss: () -> Unit) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.width(3.dp)
.height(36.dp)
.background(MaterialTheme.colorScheme.tertiary, RoundedCornerShape(2.dp)),
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
"Editing message",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.tertiary,
)
Text(
body,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
IconButton(onClick = onDismiss, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Close, "Cancel edit", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}