forked from melod1n/fast-messenger
feat(messages): add message selection and actions
This commit is contained in:
@@ -170,7 +170,6 @@ fun MainScreen(
|
|||||||
conversationsScreen(
|
conversationsScreen(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onConversationItemClicked = onConversationItemClicked,
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
onPhotoClicked = onPhotoClicked,
|
|
||||||
onCreateChatClicked = onCreateChatClicked,
|
onCreateChatClicked = onCreateChatClicked,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M10,9V7.41c0,-0.89 -1.08,-1.34 -1.71,-0.71L3.7,11.29c-0.39,0.39 -0.39,1.02 0,1.41l4.59,4.59c0.63,0.63 1.71,0.19 1.71,-0.7V14.9c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M7,7.56c0,-0.94 -1.14,-1.42 -1.81,-0.75L0.71,11.29c-0.39,0.39 -0.39,1.02 0,1.41l4.48,4.48c0.67,0.68 1.81,0.2 1.81,-0.74 0,-0.28 -0.11,-0.55 -0.31,-0.75L3,12l3.69,-3.69c0.2,-0.2 0.31,-0.47 0.31,-0.75zM13,9V7.41c0,-0.89 -1.08,-1.34 -1.71,-0.71L6.7,11.29c-0.39,0.39 -0.39,1.02 0,1.41l4.59,4.59c0.63,0.63 1.71,0.18 1.71,-0.71V14.9c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
-2
@@ -16,7 +16,6 @@ object Conversations
|
|||||||
fun NavGraphBuilder.conversationsScreen(
|
fun NavGraphBuilder.conversationsScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onConversationItemClicked: (id: Int) -> Unit,
|
onConversationItemClicked: (id: Int) -> Unit,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
|
||||||
onCreateChatClicked: () -> Unit,
|
onCreateChatClicked: () -> Unit,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
@@ -27,7 +26,6 @@ fun NavGraphBuilder.conversationsScreen(
|
|||||||
ConversationsRoute(
|
ConversationsRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onConversationItemClicked = onConversationItemClicked,
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
onConversationPhotoClicked = onPhotoClicked,
|
|
||||||
onCreateChatButtonClicked = onCreateChatClicked,
|
onCreateChatButtonClicked = onCreateChatClicked,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-11
@@ -6,7 +6,6 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
@@ -69,10 +67,8 @@ fun ConversationItem(
|
|||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
isUserAccount: Boolean,
|
isUserAccount: Boolean,
|
||||||
conversation: UiConversation,
|
conversation: UiConversation,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier
|
||||||
onPhotoClicked: (url: String) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
|
||||||
val bottomStartCornerRadius by animateDpAsState(
|
val bottomStartCornerRadius by animateDpAsState(
|
||||||
@@ -154,12 +150,7 @@ fun ConversationItem(
|
|||||||
contentDescription = "Avatar",
|
contentDescription = "Avatar",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(CircleShape)
|
.clip(CircleShape),
|
||||||
.clickable {
|
|
||||||
if (avatarImage is String) {
|
|
||||||
onPhotoClicked(avatarImage)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut)
|
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-6
@@ -39,13 +39,10 @@ fun ConversationsList(
|
|||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
|
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
|
||||||
padding: PaddingValues,
|
padding: PaddingValues
|
||||||
onPhotoClicked: (url: String) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val bottomPadding = LocalBottomPadding.current
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = state
|
state = state
|
||||||
@@ -71,8 +68,7 @@ fun ConversationsList(
|
|||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
isUserAccount = isUserAccount,
|
isUserAccount = isUserAccount,
|
||||||
conversation = conversation,
|
conversation = conversation,
|
||||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
onPhotoClicked = onPhotoClicked
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
+1
-5
@@ -85,7 +85,6 @@ import dev.meloda.fast.ui.R as UiR
|
|||||||
fun ConversationsRoute(
|
fun ConversationsRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onConversationItemClicked: (conversationId: Int) -> Unit,
|
onConversationItemClicked: (conversationId: Int) -> Unit,
|
||||||
onConversationPhotoClicked: (url: String) -> Unit,
|
|
||||||
onCreateChatButtonClicked: () -> Unit,
|
onCreateChatButtonClicked: () -> Unit,
|
||||||
viewModel: ConversationsViewModel
|
viewModel: ConversationsViewModel
|
||||||
) {
|
) {
|
||||||
@@ -107,7 +106,6 @@ fun ConversationsRoute(
|
|||||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||||
onRefresh = viewModel::onRefresh,
|
onRefresh = viewModel::onRefresh,
|
||||||
onConversationPhotoClicked = onConversationPhotoClicked,
|
|
||||||
onCreateChatButtonClicked = onCreateChatButtonClicked,
|
onCreateChatButtonClicked = onCreateChatButtonClicked,
|
||||||
setScrollIndex = viewModel::setScrollIndex,
|
setScrollIndex = viewModel::setScrollIndex,
|
||||||
setScrollOffset = viewModel::setScrollOffset
|
setScrollOffset = viewModel::setScrollOffset
|
||||||
@@ -135,7 +133,6 @@ fun ConversationsScreen(
|
|||||||
onPaginationConditionsMet: () -> Unit = {},
|
onPaginationConditionsMet: () -> Unit = {},
|
||||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
onConversationPhotoClicked: (url: String) -> Unit = {},
|
|
||||||
onCreateChatButtonClicked: () -> Unit = {},
|
onCreateChatButtonClicked: () -> Unit = {},
|
||||||
setScrollIndex: (Int) -> Unit = {},
|
setScrollIndex: (Int) -> Unit = {},
|
||||||
setScrollOffset: (Int) -> Unit = {}
|
setScrollOffset: (Int) -> Unit = {}
|
||||||
@@ -356,8 +353,7 @@ fun ConversationsScreen(
|
|||||||
Modifier
|
Modifier
|
||||||
}.fillMaxSize(),
|
}.fillMaxSize(),
|
||||||
onOptionClicked = onOptionClicked,
|
onOptionClicked = onOptionClicked,
|
||||||
padding = padding,
|
padding = padding
|
||||||
onPhotoClicked = onConversationPhotoClicked
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (screenState.conversations.isEmpty()) {
|
if (screenState.conversations.isEmpty()) {
|
||||||
|
|||||||
+75
-1
@@ -1,6 +1,8 @@
|
|||||||
package dev.meloda.fast.messageshistory
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
@@ -45,6 +47,7 @@ import kotlin.random.Random
|
|||||||
interface MessagesHistoryViewModel {
|
interface MessagesHistoryViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<MessagesHistoryScreenState>
|
val screenState: StateFlow<MessagesHistoryScreenState>
|
||||||
|
val selectedMessages: StateFlow<List<Int>>
|
||||||
|
|
||||||
val baseError: StateFlow<BaseError?>
|
val baseError: StateFlow<BaseError?>
|
||||||
val imagesToPreload: StateFlow<List<String>>
|
val imagesToPreload: StateFlow<List<String>>
|
||||||
@@ -53,6 +56,7 @@ interface MessagesHistoryViewModel {
|
|||||||
|
|
||||||
val canPaginate: StateFlow<Boolean>
|
val canPaginate: StateFlow<Boolean>
|
||||||
|
|
||||||
|
fun onCloseButtonClicked()
|
||||||
fun onRefresh()
|
fun onRefresh()
|
||||||
fun onAttachmentButtonClicked()
|
fun onAttachmentButtonClicked()
|
||||||
fun onMessageInputChanged(newText: TextFieldValue)
|
fun onMessageInputChanged(newText: TextFieldValue)
|
||||||
@@ -60,19 +64,23 @@ interface MessagesHistoryViewModel {
|
|||||||
fun onActionButtonClicked()
|
fun onActionButtonClicked()
|
||||||
|
|
||||||
fun onPaginationConditionsMet()
|
fun onPaginationConditionsMet()
|
||||||
|
|
||||||
|
fun onMessageClicked(messageId: Int)
|
||||||
|
fun onMessageLongClicked(messageId: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessagesHistoryViewModelImpl(
|
class MessagesHistoryViewModelImpl(
|
||||||
|
private val applicationContext: Context,
|
||||||
private val messagesUseCase: MessagesUseCase,
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val conversationsUseCase: ConversationsUseCase,
|
private val conversationsUseCase: ConversationsUseCase,
|
||||||
private val resourceProvider: ResourceProvider,
|
private val resourceProvider: ResourceProvider,
|
||||||
private val userSettings: UserSettings,
|
private val userSettings: UserSettings,
|
||||||
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
|
|
||||||
updatesParser: LongPollUpdatesParser,
|
updatesParser: LongPollUpdatesParser,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : MessagesHistoryViewModel, ViewModel() {
|
) : MessagesHistoryViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY)
|
override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY)
|
||||||
|
override val selectedMessages = MutableStateFlow<List<Int>>(emptyList())
|
||||||
|
|
||||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||||
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
override val imagesToPreload = MutableStateFlow<List<String>>(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() {
|
override fun onRefresh() {
|
||||||
loadMessagesHistory(offset = 0)
|
loadMessagesHistory(offset = 0)
|
||||||
}
|
}
|
||||||
@@ -158,6 +178,60 @@ class MessagesHistoryViewModelImpl(
|
|||||||
loadMessagesHistory()
|
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) {
|
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -24,7 +24,8 @@ sealed class UiItem(
|
|||||||
val avatar: UiImage,
|
val avatar: UiImage,
|
||||||
val isEdited: Boolean,
|
val isEdited: Boolean,
|
||||||
val isRead: Boolean,
|
val isRead: Boolean,
|
||||||
val sendingStatus: SendingStatus = SendingStatus.SENT
|
val sendingStatus: SendingStatus = SendingStatus.SENT,
|
||||||
|
val isSelected: Boolean = false
|
||||||
) : UiItem(id, conversationMessageId)
|
) : UiItem(id, conversationMessageId)
|
||||||
|
|
||||||
data class ActionMessage(
|
data class ActionMessage(
|
||||||
|
|||||||
+6
-5
@@ -31,12 +31,11 @@ import dev.meloda.fast.messageshistory.model.UiItem
|
|||||||
fun IncomingMessageBubble(
|
fun IncomingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
animate: Boolean
|
animate: Boolean,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
Row(modifier = modifier.fillMaxWidth()) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.75f)
|
.fillMaxWidth(0.75f)
|
||||||
.padding(start = 16.dp),
|
.padding(start = 16.dp),
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
@@ -48,7 +47,7 @@ fun IncomingMessageBubble(
|
|||||||
message.avatar.extractUrl()?.let { url ->
|
message.avatar.extractUrl()?.let { url ->
|
||||||
rememberAsyncImagePainter(
|
rememberAsyncImagePainter(
|
||||||
model = url,
|
model = url,
|
||||||
imageLoader = context.imageLoader
|
imageLoader = LocalContext.current.imageLoader
|
||||||
)
|
)
|
||||||
} ?: painterResource(id = message.avatar.extractResId()),
|
} ?: painterResource(id = message.avatar.extractResId()),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -87,4 +86,6 @@ fun IncomingMessageBubble(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier=Modifier.fillMaxWidth(0.25f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-8
@@ -1,5 +1,6 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.Animatable
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -107,6 +109,7 @@ fun MessagesHistoryRoute(
|
|||||||
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
val selectedMessages by viewModel.selectedMessages.collectAsStateWithLifecycle()
|
||||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -115,10 +118,12 @@ fun MessagesHistoryRoute(
|
|||||||
|
|
||||||
MessagesHistoryScreen(
|
MessagesHistoryScreen(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
|
selectedMessages = ImmutableList.copyOf(selectedMessages),
|
||||||
baseError = baseError,
|
baseError = baseError,
|
||||||
canPaginate = canPaginate,
|
canPaginate = canPaginate,
|
||||||
showEmojiButton = showEmojiButton,
|
showEmojiButton = showEmojiButton,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
|
onClose = viewModel::onCloseButtonClicked,
|
||||||
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
||||||
onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked,
|
onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked,
|
||||||
onRefresh = viewModel::onRefresh,
|
onRefresh = viewModel::onRefresh,
|
||||||
@@ -126,7 +131,9 @@ fun MessagesHistoryRoute(
|
|||||||
onMessageInputChanged = viewModel::onMessageInputChanged,
|
onMessageInputChanged = viewModel::onMessageInputChanged,
|
||||||
onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked,
|
onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked,
|
||||||
onActionButtonClicked = viewModel::onActionButtonClicked,
|
onActionButtonClicked = viewModel::onActionButtonClicked,
|
||||||
onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked
|
onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked,
|
||||||
|
onMessageClicked = viewModel::onMessageClicked,
|
||||||
|
onMessageLongClicked = viewModel::onMessageLongClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +145,12 @@ fun MessagesHistoryRoute(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MessagesHistoryScreen(
|
fun MessagesHistoryScreen(
|
||||||
screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY,
|
screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY,
|
||||||
|
selectedMessages: ImmutableList<Int> = ImmutableList.empty(),
|
||||||
baseError: BaseError? = null,
|
baseError: BaseError? = null,
|
||||||
canPaginate: Boolean = false,
|
canPaginate: Boolean = false,
|
||||||
showEmojiButton: Boolean = false,
|
showEmojiButton: Boolean = false,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
|
onClose: () -> Unit = {},
|
||||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||||
onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> },
|
onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> },
|
||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
@@ -149,7 +158,9 @@ fun MessagesHistoryScreen(
|
|||||||
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||||
onAttachmentButtonClicked: () -> Unit = {},
|
onAttachmentButtonClicked: () -> Unit = {},
|
||||||
onActionButtonClicked: () -> Unit = {},
|
onActionButtonClicked: () -> Unit = {},
|
||||||
onEmojiButtonLongClicked: () -> Unit = {}
|
onEmojiButtonLongClicked: () -> Unit = {},
|
||||||
|
onMessageClicked: (Int) -> Unit = {},
|
||||||
|
onMessageLongClicked: (Int) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
@@ -157,6 +168,11 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
|
|
||||||
|
BackHandler(
|
||||||
|
enabled = selectedMessages.isNotEmpty(),
|
||||||
|
onBack = onClose
|
||||||
|
)
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
val paginationConditionMet by remember(canPaginate, listState) {
|
val paginationConditionMet by remember(canPaginate, listState) {
|
||||||
@@ -191,6 +207,10 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val showReplyAction by remember(screenState) {
|
||||||
|
mutableStateOf(selectedMessages.size == 1)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
contentWindowInsets = WindowInsets.statusBars,
|
||||||
@@ -213,6 +233,7 @@ fun MessagesHistoryScreen(
|
|||||||
.weight(1f),
|
.weight(1f),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
if (selectedMessages.isEmpty()) {
|
||||||
val avatar = screenState.avatar.getImage()
|
val avatar = screenState.avatar.getImage()
|
||||||
if (avatar is Painter) {
|
if (avatar is Painter) {
|
||||||
Image(
|
Image(
|
||||||
@@ -234,11 +255,14 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text =
|
text = when {
|
||||||
if (screenState.isLoading) stringResource(id = UiR.string.title_loading)
|
screenState.isLoading -> stringResource(id = UiR.string.title_loading)
|
||||||
else screenState.title,
|
selectedMessages.size > 0 -> "(${selectedMessages.size})"
|
||||||
|
else -> screenState.title
|
||||||
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.headlineSmall
|
style = MaterialTheme.typography.headlineSmall
|
||||||
@@ -246,9 +270,18 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (selectedMessages.isEmpty()) onBack()
|
||||||
|
else onClose()
|
||||||
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
imageVector = if (selectedMessages.isEmpty()) {
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowBack
|
||||||
|
} else {
|
||||||
|
Icons.Rounded.Close
|
||||||
|
},
|
||||||
contentDescription = "Back button"
|
contentDescription = "Back button"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -259,6 +292,46 @@ fun MessagesHistoryScreen(
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
actions = {
|
actions = {
|
||||||
|
if (selectedMessages.isNotEmpty()) {
|
||||||
|
AnimatedVisibility(showReplyAction) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
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(
|
IconButton(
|
||||||
onClick = { dropDownMenuExpanded = true }
|
onClick = { dropDownMenuExpanded = true }
|
||||||
) {
|
) {
|
||||||
@@ -309,6 +382,7 @@ fun MessagesHistoryScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val showHorizontalProgressBar by remember(screenState) {
|
val showHorizontalProgressBar by remember(screenState) {
|
||||||
@@ -323,7 +397,6 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -343,7 +416,16 @@ fun MessagesHistoryScreen(
|
|||||||
index = screenState.messages.indexOfMessageByCmId(cmId)
|
index = screenState.messages.indexOfMessageByCmId(cmId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onMessageClicked = { id ->
|
||||||
|
if (selectedMessages.isNotEmpty()) {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
onMessageClicked(id)
|
||||||
|
},
|
||||||
|
onMessageLongClicked = onMessageLongClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
+45
-7
@@ -1,30 +1,41 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.chrisbanes.haze.HazeState
|
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.datastore.AppSettings
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesList(
|
fun MessagesList(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -33,7 +44,9 @@ fun MessagesList(
|
|||||||
immutableMessages: ImmutableList<UiItem>,
|
immutableMessages: ImmutableList<UiItem>,
|
||||||
isPaginating: Boolean,
|
isPaginating: Boolean,
|
||||||
messageBarHeight: Dp,
|
messageBarHeight: Dp,
|
||||||
onRequestScrollToCmId: (cmId: Int) -> Unit = {}
|
onRequestScrollToCmId: (cmId: Int) -> Unit = {},
|
||||||
|
onMessageClicked: (Int) -> Unit = {},
|
||||||
|
onMessageLongClicked: (Int) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val enableAnimations = remember {
|
val enableAnimations = remember {
|
||||||
AppSettings.Experimental.moreAnimations
|
AppSettings.Experimental.moreAnimations
|
||||||
@@ -42,13 +55,14 @@ fun MessagesList(
|
|||||||
immutableMessages.toList()
|
immutableMessages.toList()
|
||||||
}
|
}
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
|
val view = LocalView.current
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.haze(state = hazeState)
|
Modifier.hazeSource(state = hazeState)
|
||||||
} else Modifier
|
} else Modifier
|
||||||
),
|
),
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -87,10 +101,33 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is UiItem.Message -> {
|
is UiItem.Message -> {
|
||||||
|
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) {
|
if (item.isOut) {
|
||||||
OutgoingMessageBubble(
|
OutgoingMessageBubble(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.then(
|
Modifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.then(
|
||||||
if (enableAnimations) Modifier.animateItem(
|
if (enableAnimations) Modifier.animateItem(
|
||||||
fadeInSpec = null,
|
fadeInSpec = null,
|
||||||
fadeOutSpec = null
|
fadeOutSpec = null
|
||||||
@@ -103,7 +140,9 @@ fun MessagesList(
|
|||||||
} else {
|
} else {
|
||||||
IncomingMessageBubble(
|
IncomingMessageBubble(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.then(
|
Modifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.then(
|
||||||
if (enableAnimations) Modifier.animateItem(
|
if (enableAnimations) Modifier.animateItem(
|
||||||
fadeInSpec = null,
|
fadeInSpec = null,
|
||||||
fadeOutSpec = null
|
fadeOutSpec = null
|
||||||
@@ -116,8 +155,7 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
|
|||||||
Reference in New Issue
Block a user