diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt
index 27286c74..2d375f6d 100644
--- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt
+++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt
@@ -170,7 +170,6 @@ fun MainScreen(
conversationsScreen(
onError = onError,
onConversationItemClicked = onConversationItemClicked,
- onPhotoClicked = onPhotoClicked,
onCreateChatClicked = onCreateChatClicked,
navController = navController,
)
diff --git a/core/ui/src/main/res/drawable/round_reply_24.xml b/core/ui/src/main/res/drawable/round_reply_24.xml
new file mode 100644
index 00000000..8f412d6f
--- /dev/null
+++ b/core/ui/src/main/res/drawable/round_reply_24.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/round_reply_all_24.xml b/core/ui/src/main/res/drawable/round_reply_all_24.xml
new file mode 100644
index 00000000..921da1f8
--- /dev/null
+++ b/core/ui/src/main/res/drawable/round_reply_all_24.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt
index 0cf05c68..63876d0e 100644
--- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt
+++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt
@@ -16,7 +16,6 @@ object Conversations
fun NavGraphBuilder.conversationsScreen(
onError: (BaseError) -> Unit,
onConversationItemClicked: (id: Int) -> Unit,
- onPhotoClicked: (url: String) -> Unit,
onCreateChatClicked: () -> Unit,
navController: NavController,
) {
@@ -27,7 +26,6 @@ fun NavGraphBuilder.conversationsScreen(
ConversationsRoute(
onError = onError,
onConversationItemClicked = onConversationItemClicked,
- onConversationPhotoClicked = onPhotoClicked,
onCreateChatButtonClicked = onCreateChatClicked,
viewModel = viewModel
)
diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt
index ed0751f0..81ee7f02 100644
--- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt
+++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt
@@ -6,7 +6,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
@@ -40,7 +39,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
@@ -69,10 +67,8 @@ fun ConversationItem(
maxLines: Int,
isUserAccount: Boolean,
conversation: UiConversation,
- modifier: Modifier = Modifier,
- onPhotoClicked: (url: String) -> Unit
+ modifier: Modifier = Modifier
) {
- val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
val bottomStartCornerRadius by animateDpAsState(
@@ -154,12 +150,7 @@ fun ConversationItem(
contentDescription = "Avatar",
modifier = Modifier
.fillMaxSize()
- .clip(CircleShape)
- .clickable {
- if (avatarImage is String) {
- onPhotoClicked(avatarImage)
- }
- },
+ .clip(CircleShape),
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut)
)
}
diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt
index 666827c6..f889911c 100644
--- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt
+++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt
@@ -39,13 +39,10 @@ fun ConversationsList(
maxLines: Int,
modifier: Modifier,
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
- padding: PaddingValues,
- onPhotoClicked: (url: String) -> Unit
+ padding: PaddingValues
) {
val coroutineScope = rememberCoroutineScope()
- val bottomPadding = LocalBottomPadding.current
-
LazyColumn(
modifier = modifier,
state = state
@@ -71,8 +68,7 @@ fun ConversationsList(
maxLines = maxLines,
isUserAccount = isUserAccount,
conversation = conversation,
- modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
- onPhotoClicked = onPhotoClicked
+ modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
)
Spacer(modifier = Modifier.height(8.dp))
diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt
index 863fafef..b7e1a75f 100644
--- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt
+++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt
@@ -85,7 +85,6 @@ import dev.meloda.fast.ui.R as UiR
fun ConversationsRoute(
onError: (BaseError) -> Unit,
onConversationItemClicked: (conversationId: Int) -> Unit,
- onConversationPhotoClicked: (url: String) -> Unit,
onCreateChatButtonClicked: () -> Unit,
viewModel: ConversationsViewModel
) {
@@ -107,7 +106,6 @@ fun ConversationsRoute(
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefreshDropdownItemClicked = viewModel::onRefresh,
onRefresh = viewModel::onRefresh,
- onConversationPhotoClicked = onConversationPhotoClicked,
onCreateChatButtonClicked = onCreateChatButtonClicked,
setScrollIndex = viewModel::setScrollIndex,
setScrollOffset = viewModel::setScrollOffset
@@ -135,7 +133,6 @@ fun ConversationsScreen(
onPaginationConditionsMet: () -> Unit = {},
onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {},
- onConversationPhotoClicked: (url: String) -> Unit = {},
onCreateChatButtonClicked: () -> Unit = {},
setScrollIndex: (Int) -> Unit = {},
setScrollOffset: (Int) -> Unit = {}
@@ -356,8 +353,7 @@ fun ConversationsScreen(
Modifier
}.fillMaxSize(),
onOptionClicked = onOptionClicked,
- padding = padding,
- onPhotoClicked = onConversationPhotoClicked
+ padding = padding
)
if (screenState.conversations.isEmpty()) {
diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt
index 345830e5..2c6af89c 100644
--- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt
+++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt
@@ -1,6 +1,8 @@
package dev.meloda.fast.messageshistory
+import android.content.Context
import android.util.Log
+import android.widget.Toast
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle
@@ -45,6 +47,7 @@ import kotlin.random.Random
interface MessagesHistoryViewModel {
val screenState: StateFlow
+ val selectedMessages: StateFlow>
val baseError: StateFlow
val imagesToPreload: StateFlow>
@@ -53,6 +56,7 @@ interface MessagesHistoryViewModel {
val canPaginate: StateFlow
+ fun onCloseButtonClicked()
fun onRefresh()
fun onAttachmentButtonClicked()
fun onMessageInputChanged(newText: TextFieldValue)
@@ -60,19 +64,23 @@ interface MessagesHistoryViewModel {
fun onActionButtonClicked()
fun onPaginationConditionsMet()
+
+ fun onMessageClicked(messageId: Int)
+ fun onMessageLongClicked(messageId: Int)
}
class MessagesHistoryViewModelImpl(
+ private val applicationContext: Context,
private val messagesUseCase: MessagesUseCase,
private val conversationsUseCase: ConversationsUseCase,
private val resourceProvider: ResourceProvider,
private val userSettings: UserSettings,
- private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
updatesParser: LongPollUpdatesParser,
savedStateHandle: SavedStateHandle
) : MessagesHistoryViewModel, ViewModel() {
override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY)
+ override val selectedMessages = MutableStateFlow>(emptyList())
override val baseError = MutableStateFlow(null)
override val imagesToPreload = MutableStateFlow>(emptyList())
@@ -104,6 +112,18 @@ class MessagesHistoryViewModelImpl(
)
}
+ override fun onCloseButtonClicked() {
+ screenState.setValue { old ->
+ old.copy(
+ messages = old.messages.map {
+ if (it is UiItem.Message) it.copy(isSelected = false)
+ else it
+ }
+ )
+ }
+ selectedMessages.setValue { emptyList() }
+ }
+
override fun onRefresh() {
loadMessagesHistory(offset = 0)
}
@@ -158,6 +178,60 @@ class MessagesHistoryViewModelImpl(
loadMessagesHistory()
}
+ override fun onMessageClicked(messageId: Int) {
+ val messageIndex = screenState.value.messages.indexOfFirstOrNull {
+ it is UiItem.Message && it.id == messageId
+ } ?: return
+
+ val newMessages = screenState.value.messages.toMutableList()
+ val currentMessage: UiItem.Message = newMessages[messageIndex] as UiItem.Message
+
+ if (selectedMessages.value.isNotEmpty()) {
+ val isSelected = selectedMessages.value.contains(currentMessage.id)
+
+ newMessages[messageIndex] = currentMessage.copy(
+ isSelected = !isSelected
+ )
+ screenState.setValue { old -> old.copy(messages = newMessages) }
+ selectedMessages.setValue { old ->
+ old.toMutableList().also {
+ if (isSelected) {
+ it.remove(currentMessage.id)
+ } else {
+ it.add(currentMessage.id)
+ }
+ }
+ }
+ } else {
+ Toast.makeText(applicationContext, "Click", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onMessageLongClicked(messageId: Int) {
+ val messageIndex = screenState.value.messages.indexOfFirstOrNull {
+ it is UiItem.Message && it.id == messageId
+ } ?: return
+
+ val newMessages = screenState.value.messages.toMutableList()
+ val currentMessage: UiItem.Message = newMessages[messageIndex] as UiItem.Message
+
+ val isSelected = selectedMessages.value.contains(currentMessage.id)
+
+ newMessages[messageIndex] = currentMessage.copy(
+ isSelected = !isSelected
+ )
+ screenState.setValue { old -> old.copy(messages = newMessages) }
+ selectedMessages.setValue { old ->
+ old.toMutableList().also {
+ if (isSelected) {
+ it.remove(currentMessage.id)
+ } else {
+ it.add(currentMessage.id)
+ }
+ }
+ }
+ }
+
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
val message = event.message
diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt
index 1eadb2d0..a7013fc1 100644
--- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt
+++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt
@@ -24,7 +24,8 @@ sealed class UiItem(
val avatar: UiImage,
val isEdited: Boolean,
val isRead: Boolean,
- val sendingStatus: SendingStatus = SendingStatus.SENT
+ val sendingStatus: SendingStatus = SendingStatus.SENT,
+ val isSelected: Boolean = false
) : UiItem(id, conversationMessageId)
data class ActionMessage(
diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt
index d01bdef3..dd1d960a 100644
--- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt
+++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt
@@ -31,60 +31,61 @@ import dev.meloda.fast.messageshistory.model.UiItem
fun IncomingMessageBubble(
modifier: Modifier = Modifier,
message: UiItem.Message,
- animate: Boolean
+ animate: Boolean,
) {
- val context = LocalContext.current
-
- Row(
- modifier = modifier
- .fillMaxWidth(0.75f)
- .padding(start = 16.dp),
- verticalAlignment = Alignment.Bottom,
- horizontalArrangement = Arrangement.Start
- ) {
- if (message.isInChat) {
- Image(
- painter =
- message.avatar.extractUrl()?.let { url ->
- rememberAsyncImagePainter(
- model = url,
- imageLoader = context.imageLoader
- )
- } ?: painterResource(id = message.avatar.extractResId()),
- contentDescription = null,
- modifier = Modifier
- .padding(bottom = 6.dp)
- .size(28.dp)
- .alpha(if (message.showAvatar) 1f else 0f)
- .clip(CircleShape),
- )
- Spacer(modifier = Modifier.width(8.dp))
- }
-
- Column {
- AnimatedVisibility(visible = message.showName) {
- Text(
+ Row(modifier = modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(0.75f)
+ .padding(start = 16.dp),
+ verticalAlignment = Alignment.Bottom,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ if (message.isInChat) {
+ Image(
+ painter =
+ message.avatar.extractUrl()?.let { url ->
+ rememberAsyncImagePainter(
+ model = url,
+ imageLoader = LocalContext.current.imageLoader
+ )
+ } ?: painterResource(id = message.avatar.extractResId()),
+ contentDescription = null,
modifier = Modifier
- .padding(start = 12.dp)
- .widthIn(max = 140.dp),
- text = message.name,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.primary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
+ .padding(bottom = 6.dp)
+ .size(28.dp)
+ .alpha(if (message.showAvatar) 1f else 0f)
+ .clip(CircleShape),
)
+ Spacer(modifier = Modifier.width(8.dp))
}
- MessageBubble(
- modifier = Modifier,
- text = message.text,
- isOut = false,
- date = message.date,
- edited = message.isEdited,
- animate = animate,
- isRead = message.isRead,
- sendingStatus = message.sendingStatus
- )
+ Column {
+ AnimatedVisibility(visible = message.showName) {
+ Text(
+ modifier = Modifier
+ .padding(start = 12.dp)
+ .widthIn(max = 140.dp),
+ text = message.name,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ MessageBubble(
+ modifier = Modifier,
+ text = message.text,
+ isOut = false,
+ date = message.date,
+ edited = message.isEdited,
+ animate = animate,
+ isRead = message.isRead,
+ sendingStatus = message.sendingStatus
+ )
+ }
}
+ Spacer(modifier=Modifier.fillMaxWidth(0.25f))
}
}
diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt
index b599ce3c..c7c67c84 100644
--- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt
+++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt
@@ -1,5 +1,6 @@
package dev.meloda.fast.messageshistory.presentation
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
@@ -33,6 +34,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
@@ -107,6 +109,7 @@ fun MessagesHistoryRoute(
viewModel: MessagesHistoryViewModel = koinViewModel()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+ val selectedMessages by viewModel.selectedMessages.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
@@ -115,10 +118,12 @@ fun MessagesHistoryRoute(
MessagesHistoryScreen(
screenState = screenState,
+ selectedMessages = ImmutableList.copyOf(selectedMessages),
baseError = baseError,
canPaginate = canPaginate,
showEmojiButton = showEmojiButton,
onBack = onBack,
+ onClose = viewModel::onCloseButtonClicked,
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked,
onRefresh = viewModel::onRefresh,
@@ -126,7 +131,9 @@ fun MessagesHistoryRoute(
onMessageInputChanged = viewModel::onMessageInputChanged,
onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked,
onActionButtonClicked = viewModel::onActionButtonClicked,
- onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked
+ onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked,
+ onMessageClicked = viewModel::onMessageClicked,
+ onMessageLongClicked = viewModel::onMessageLongClicked
)
}
@@ -138,10 +145,12 @@ fun MessagesHistoryRoute(
@Composable
fun MessagesHistoryScreen(
screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY,
+ selectedMessages: ImmutableList = ImmutableList.empty(),
baseError: BaseError? = null,
canPaginate: Boolean = false,
showEmojiButton: Boolean = false,
onBack: () -> Unit = {},
+ onClose: () -> Unit = {},
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> },
onRefresh: () -> Unit = {},
@@ -149,7 +158,9 @@ fun MessagesHistoryScreen(
onMessageInputChanged: (TextFieldValue) -> Unit = {},
onAttachmentButtonClicked: () -> Unit = {},
onActionButtonClicked: () -> Unit = {},
- onEmojiButtonLongClicked: () -> Unit = {}
+ onEmojiButtonLongClicked: () -> Unit = {},
+ onMessageClicked: (Int) -> Unit = {},
+ onMessageLongClicked: (Int) -> Unit = {}
) {
val view = LocalView.current
@@ -157,6 +168,11 @@ fun MessagesHistoryScreen(
val currentTheme = LocalThemeConfig.current
+ BackHandler(
+ enabled = selectedMessages.isNotEmpty(),
+ onBack = onClose
+ )
+
val listState = rememberLazyListState()
val paginationConditionMet by remember(canPaginate, listState) {
@@ -191,6 +207,10 @@ fun MessagesHistoryScreen(
val density = LocalDensity.current
+ val showReplyAction by remember(screenState) {
+ mutableStateOf(selectedMessages.size == 1)
+ }
+
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
@@ -213,32 +233,36 @@ fun MessagesHistoryScreen(
.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
- val avatar = screenState.avatar.getImage()
- if (avatar is Painter) {
- Image(
- painter = avatar,
- contentDescription = null,
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape)
- )
- } else {
- AsyncImage(
- model = screenState.avatar.getImage(),
- contentDescription = "Profile Image",
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape),
- placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut),
- )
+ if (selectedMessages.isEmpty()) {
+ val avatar = screenState.avatar.getImage()
+ if (avatar is Painter) {
+ Image(
+ painter = avatar,
+ contentDescription = null,
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape)
+ )
+ } else {
+ AsyncImage(
+ model = screenState.avatar.getImage(),
+ contentDescription = "Profile Image",
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape),
+ placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut),
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
}
- Spacer(modifier = Modifier.width(12.dp))
-
Text(
- text =
- if (screenState.isLoading) stringResource(id = UiR.string.title_loading)
- else screenState.title,
+ text = when {
+ screenState.isLoading -> stringResource(id = UiR.string.title_loading)
+ selectedMessages.size > 0 -> "(${selectedMessages.size})"
+ else -> screenState.title
+ },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall
@@ -246,9 +270,18 @@ fun MessagesHistoryScreen(
}
},
navigationIcon = {
- IconButton(onClick = onBack) {
+ IconButton(
+ onClick = {
+ if (selectedMessages.isEmpty()) onBack()
+ else onClose()
+ }
+ ) {
Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ imageVector = if (selectedMessages.isEmpty()) {
+ Icons.AutoMirrored.Rounded.ArrowBack
+ } else {
+ Icons.Rounded.Close
+ },
contentDescription = "Back button"
)
}
@@ -259,54 +292,95 @@ fun MessagesHistoryScreen(
)
),
actions = {
- IconButton(
- onClick = { dropDownMenuExpanded = true }
- ) {
- Icon(
- imageVector = Icons.Outlined.MoreVert,
- contentDescription = "Options"
- )
- }
-
- DropdownMenu(
- modifier = Modifier.defaultMinSize(minWidth = 140.dp),
- expanded = dropDownMenuExpanded,
- onDismissRequest = {
- dropDownMenuExpanded = false
- },
- offset = DpOffset(x = (-4).dp, y = (-60).dp)
- ) {
- DropdownMenuItem(
- onClick = {
- dropDownMenuExpanded = false
-
- // TODO: 11/07/2024, Danil Nikolaev: to VM
-
- // TODO: 23-Mar-25, Danil Nikolaev: crash if no messages (ex. new chat)
- onChatMaterialsDropdownItemClicked(
- screenState.conversationId,
- screenState.messages.firstMessage().conversationMessageId
- )
- },
- text = {
- Text(text = "Materials")
- }
- )
- DropdownMenuItem(
- onClick = {
- onRefresh()
- dropDownMenuExpanded = false
- },
- text = {
- Text(text = "Refresh")
- },
- leadingIcon = {
+ if (selectedMessages.isNotEmpty()) {
+ AnimatedVisibility(showReplyAction) {
+ IconButton(
+ onClick = {
+ if (AppSettings.General.enableHaptic) {
+ view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
+ }
+ }
+ ) {
Icon(
- imageVector = Icons.Rounded.Refresh,
+ painter = painterResource(UiR.drawable.round_reply_24),
contentDescription = null
)
}
- )
+ }
+ IconButton(
+ onClick = {
+ if (AppSettings.General.enableHaptic) {
+ view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(UiR.drawable.round_reply_all_24),
+ contentDescription = null
+ )
+ }
+ IconButton(
+ onClick = {
+ if (AppSettings.General.enableHaptic) {
+ view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(UiR.drawable.round_delete_outline_24),
+ contentDescription = null
+ )
+ }
+ } else {
+ IconButton(
+ onClick = { dropDownMenuExpanded = true }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = "Options"
+ )
+ }
+
+ DropdownMenu(
+ modifier = Modifier.defaultMinSize(minWidth = 140.dp),
+ expanded = dropDownMenuExpanded,
+ onDismissRequest = {
+ dropDownMenuExpanded = false
+ },
+ offset = DpOffset(x = (-4).dp, y = (-60).dp)
+ ) {
+ DropdownMenuItem(
+ onClick = {
+ dropDownMenuExpanded = false
+
+ // TODO: 11/07/2024, Danil Nikolaev: to VM
+
+ // TODO: 23-Mar-25, Danil Nikolaev: crash if no messages (ex. new chat)
+ onChatMaterialsDropdownItemClicked(
+ screenState.conversationId,
+ screenState.messages.firstMessage().conversationMessageId
+ )
+ },
+ text = {
+ Text(text = "Materials")
+ }
+ )
+ DropdownMenuItem(
+ onClick = {
+ onRefresh()
+ dropDownMenuExpanded = false
+ },
+ text = {
+ Text(text = "Refresh")
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = null
+ )
+ }
+ )
+ }
}
}
)
@@ -323,7 +397,6 @@ fun MessagesHistoryScreen(
}
}
) { padding ->
-
Box(
modifier = Modifier
.fillMaxSize()
@@ -343,7 +416,16 @@ fun MessagesHistoryScreen(
index = screenState.messages.indexOfMessageByCmId(cmId)
)
}
- }
+ },
+ onMessageClicked = { id ->
+ if (selectedMessages.isNotEmpty()) {
+ if (AppSettings.General.enableHaptic) {
+ view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK)
+ }
+ }
+ onMessageClicked(id)
+ },
+ onMessageLongClicked = onMessageLongClicked
)
Column(
diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt
index ad9b7c0e..8da688dd 100644
--- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt
+++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt
@@ -1,30 +1,41 @@
package dev.meloda.fast.messageshistory.presentation
+import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState
-import dev.chrisbanes.haze.haze
+import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MessagesList(
modifier: Modifier = Modifier,
@@ -33,7 +44,9 @@ fun MessagesList(
immutableMessages: ImmutableList,
isPaginating: Boolean,
messageBarHeight: Dp,
- onRequestScrollToCmId: (cmId: Int) -> Unit = {}
+ onRequestScrollToCmId: (cmId: Int) -> Unit = {},
+ onMessageClicked: (Int) -> Unit = {},
+ onMessageLongClicked: (Int) -> Unit = {}
) {
val enableAnimations = remember {
AppSettings.Experimental.moreAnimations
@@ -42,13 +55,14 @@ fun MessagesList(
immutableMessages.toList()
}
val currentTheme = LocalThemeConfig.current
+ val view = LocalView.current
LazyColumn(
modifier = modifier
.fillMaxWidth()
.then(
if (currentTheme.enableBlur) {
- Modifier.haze(state = hazeState)
+ Modifier.hazeSource(state = hazeState)
} else Modifier
),
state = listState,
@@ -87,37 +101,61 @@ fun MessagesList(
}
is UiItem.Message -> {
- if (item.isOut) {
- OutgoingMessageBubble(
- modifier =
- Modifier.then(
- if (enableAnimations) Modifier.animateItem(
- fadeInSpec = null,
- fadeOutSpec = null
- )
- else Modifier
- ),
- message = item,
- animate = enableAnimations
- )
- } else {
- IncomingMessageBubble(
- modifier =
- Modifier.then(
- if (enableAnimations) Modifier.animateItem(
- fadeInSpec = null,
- fadeOutSpec = null
- )
- else Modifier
- ),
- message = item,
- animate = enableAnimations
- )
+ val backgroundColor by animateColorAsState(
+ targetValue = if (item.isSelected) {
+ MaterialTheme.colorScheme.primary.copy(alpha = 0.35f)
+ } else {
+ Color.Transparent
+ }
+ )
+
+ Surface(
+ modifier = Modifier
+ .combinedClickable(
+ onLongClick = {
+ if (AppSettings.General.enableHaptic) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ }
+ onMessageLongClicked(item.id)
+ },
+ onClick = { onMessageClicked(item.id) }
+ ),
+ color = backgroundColor
+ ) {
+ if (item.isOut) {
+ OutgoingMessageBubble(
+ modifier =
+ Modifier
+ .padding(vertical = 4.dp)
+ .then(
+ if (enableAnimations) Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null
+ )
+ else Modifier
+ ),
+ message = item,
+ animate = enableAnimations
+ )
+ } else {
+ IncomingMessageBubble(
+ modifier =
+ Modifier
+ .padding(vertical = 4.dp)
+ .then(
+ if (enableAnimations) Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null
+ )
+ else Modifier
+ ),
+ message = item,
+ animate = enableAnimations
+ )
+ }
}
}
}
-
- Spacer(modifier = Modifier.height(8.dp))
}
item {