forked from melod1n/fast-messenger
feat(auth): add web captcha handling
- replace manual captcha screen with WebView-based VK captcha flow - handle captcha error 14 by showing the captcha overlay and retrying with success_token - pass captcha redirect/result state through AppSettings - remove old captcha ViewModel, navigation, validation, and DI - add ACRA crash reporting - add WIP message edit mode UI/state - update Gradle wrapper, SDK config, and dependencies
This commit is contained in:
+4
-1
@@ -19,7 +19,7 @@ interface MessagesHistoryViewModel {
|
||||
val dialog: StateFlow<MessageDialog?>
|
||||
val selectedMessages: StateFlow<List<VkMessage>>
|
||||
|
||||
val inputFieldFocusRequester: StateFlow<Boolean>
|
||||
val showKeyboard: StateFlow<Boolean>
|
||||
|
||||
val isNeedToScrollToIndex: StateFlow<Int?>
|
||||
|
||||
@@ -54,6 +54,7 @@ interface MessagesHistoryViewModel {
|
||||
fun onPinnedMessageClicked(messageId: Long)
|
||||
fun onUnpinMessageClicked()
|
||||
|
||||
fun onEditSelectedMessageClicked()
|
||||
fun onDeleteSelectedMessagesClicked()
|
||||
|
||||
fun onBoldClicked()
|
||||
@@ -66,5 +67,7 @@ interface MessagesHistoryViewModel {
|
||||
|
||||
fun onRequestReplyToMessage(cmId: Long)
|
||||
|
||||
fun onKeyboardShown()
|
||||
|
||||
suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int
|
||||
}
|
||||
|
||||
+165
-55
@@ -45,6 +45,7 @@ import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.domain.util.asPresentation
|
||||
import dev.meloda.fast.domain.util.extractAvatar
|
||||
import dev.meloda.fast.domain.util.extractReplySummary
|
||||
import dev.meloda.fast.domain.util.extractReplyTitle
|
||||
import dev.meloda.fast.domain.util.extractTitle
|
||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||
import dev.meloda.fast.messageshistory.model.MessageDialog
|
||||
@@ -55,7 +56,6 @@ import dev.meloda.fast.messageshistory.navigation.MessagesHistory
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.LongPollParsedEvent
|
||||
import dev.meloda.fast.model.api.domain.FormatDataType
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
@@ -65,6 +65,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
@@ -73,7 +74,6 @@ import kotlinx.serialization.json.put
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
@@ -94,7 +94,7 @@ class MessagesHistoryViewModelImpl(
|
||||
override val dialog = MutableStateFlow<MessageDialog?>(null)
|
||||
override val selectedMessages = MutableStateFlow<List<VkMessage>>(emptyList())
|
||||
|
||||
override val inputFieldFocusRequester = MutableStateFlow(false)
|
||||
override val showKeyboard = MutableStateFlow(false)
|
||||
|
||||
override val isNeedToScrollToIndex = MutableStateFlow<Int?>(null)
|
||||
|
||||
@@ -115,6 +115,8 @@ class MessagesHistoryViewModelImpl(
|
||||
|
||||
private var replyToCmId: Long? = null
|
||||
|
||||
private var editMessage: VkMessage? = null
|
||||
|
||||
init {
|
||||
val arguments = MessagesHistory.from(savedStateHandle).arguments
|
||||
|
||||
@@ -229,7 +231,7 @@ class MessagesHistoryViewModelImpl(
|
||||
override fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) {
|
||||
when (dialog) {
|
||||
is MessageDialog.MessageOptions -> {
|
||||
val messageId = bundle.getLong("messageId")
|
||||
// val messageId = bundle.getLong("messageId")
|
||||
val cmId = bundle.getLong("cmId")
|
||||
|
||||
when (val option = bundle.getParcelableCompat("option", MessageOption::class)) {
|
||||
@@ -289,7 +291,10 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
MessageOption.Edit -> {}
|
||||
MessageOption.Edit -> {
|
||||
editMessage(cmId)
|
||||
syncUiMessages()
|
||||
}
|
||||
|
||||
MessageOption.Delete -> {
|
||||
this.dialog.setValue {
|
||||
@@ -313,7 +318,14 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onCloseButtonClicked() {
|
||||
selectedMessages.setValue { emptyList() }
|
||||
if (selectedMessages.value.isNotEmpty()) {
|
||||
selectedMessages.setValue { emptyList() }
|
||||
}
|
||||
|
||||
if (screenState.value.editCmId != null) {
|
||||
stopEditMessage()
|
||||
}
|
||||
|
||||
syncUiMessages()
|
||||
}
|
||||
|
||||
@@ -329,8 +341,20 @@ class MessagesHistoryViewModelImpl(
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
message = newText,
|
||||
actionMode = if (newText.text.isBlank()) ActionMode.RECORD_AUDIO
|
||||
else ActionMode.SEND
|
||||
actionMode =
|
||||
when {
|
||||
screenState.value.editCmId != null -> {
|
||||
// TODO: 13/03/2026, Danil Nikolaev: also check if attachments is empty
|
||||
if (newText.text.trim().isEmpty()) {
|
||||
ActionMode.DELETE
|
||||
} else {
|
||||
ActionMode.EDIT
|
||||
}
|
||||
}
|
||||
|
||||
newText.text.trim().isEmpty() -> ActionMode.RECORD_AUDIO
|
||||
else -> ActionMode.SEND
|
||||
}
|
||||
)
|
||||
}
|
||||
updateStyles()
|
||||
@@ -347,13 +371,9 @@ class MessagesHistoryViewModelImpl(
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
when (screenState.value.actionMode) {
|
||||
ActionMode.DELETE -> {
|
||||
ActionMode.DELETE -> confirmDeleteCurrentEditMessage()
|
||||
|
||||
}
|
||||
|
||||
ActionMode.EDIT -> {
|
||||
|
||||
}
|
||||
ActionMode.EDIT -> editCurrentEditMessage()
|
||||
|
||||
ActionMode.RECORD_AUDIO -> {
|
||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_VIDEO) }
|
||||
@@ -429,6 +449,16 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditSelectedMessageClicked() {
|
||||
val cmId = selectedMessages.value.firstOrNull()?.cmId ?: return
|
||||
|
||||
selectedMessages.setValue { emptyList() }
|
||||
|
||||
editMessage(cmId)
|
||||
|
||||
syncUiMessages()
|
||||
}
|
||||
|
||||
override fun onDeleteSelectedMessagesClicked() {
|
||||
dialog.setValue {
|
||||
MessageDialog.MessagesDelete(selectedMessages.value)
|
||||
@@ -438,7 +468,7 @@ class MessagesHistoryViewModelImpl(
|
||||
private fun replyToMessage(cmId: Long) {
|
||||
val messageToReply = messages.value.find { it.cmId == cmId } ?: return
|
||||
|
||||
inputFieldFocusRequester.setValue { true }
|
||||
showKeyboard.setValue { true }
|
||||
replyToCmId = cmId
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
@@ -448,6 +478,56 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun editMessage(cmId: Long) {
|
||||
this.screenState.setValue { old ->
|
||||
old.copy(editCmId = cmId)
|
||||
}
|
||||
|
||||
val messageToEdit = messages.value.firstOrNull { it.cmId == cmId } ?: return
|
||||
editMessage = messageToEdit
|
||||
|
||||
lastMessageText = screenState.value.message.text
|
||||
|
||||
var newState = screenState.value.copy(
|
||||
message = TextFieldValue(
|
||||
text = messageToEdit.text.orEmpty(),
|
||||
selection = TextRange(messageToEdit.text.orEmpty().length)
|
||||
),
|
||||
actionMode = ActionMode.EDIT
|
||||
)
|
||||
|
||||
messageToEdit.replyMessage?.let { reply ->
|
||||
replyToCmId = reply.cmId
|
||||
newState = newState.copy(
|
||||
replyTitle = reply.extractReplyTitle(),
|
||||
replyText = reply.extractReplySummary(resourceProvider.resources)
|
||||
)
|
||||
}
|
||||
|
||||
showKeyboard.setValue { true }
|
||||
screenState.setValue { newState }
|
||||
}
|
||||
|
||||
private fun stopEditMessage() {
|
||||
val lastText = lastMessageText.orEmpty().trim()
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
editCmId = null,
|
||||
message = TextFieldValue(
|
||||
text = lastText,
|
||||
selection = TextRange(lastText.length)
|
||||
),
|
||||
actionMode = if (lastText.isBlank()) ActionMode.RECORD_AUDIO
|
||||
else ActionMode.SEND,
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: use last reply
|
||||
replyTitle = null,
|
||||
replyText = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var formatData = VkMessage.FormatData("1", emptyList())
|
||||
|
||||
private fun updateStyles() {
|
||||
@@ -580,23 +660,28 @@ class MessagesHistoryViewModelImpl(
|
||||
replyToMessage(cmId)
|
||||
}
|
||||
|
||||
override suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int = suspendCoroutine {
|
||||
viewModelScope.launch {
|
||||
getMessageReadPeersUseCase
|
||||
.invoke(peerId = peerId, cmId = cmId)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
it.resume(-1)
|
||||
},
|
||||
success = { count ->
|
||||
it.resume(count)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun onKeyboardShown() {
|
||||
showKeyboard.setValue { false }
|
||||
}
|
||||
|
||||
override suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int =
|
||||
suspendCancellableCoroutine {
|
||||
viewModelScope.launch {
|
||||
getMessageReadPeersUseCase
|
||||
.invoke(peerId = peerId, cmId = cmId)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
it.resume(-1)
|
||||
},
|
||||
success = { count ->
|
||||
it.resume(count)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||
val message = event.message
|
||||
|
||||
@@ -988,11 +1073,13 @@ class MessagesHistoryViewModelImpl(
|
||||
message = newMessage.text,
|
||||
forward = forward,
|
||||
attachments = null,
|
||||
formatData = newMessage.formatData
|
||||
formatData = newMessage.formatData,
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
any = { sendingMessages.remove(newMessage) },
|
||||
error = { error ->
|
||||
Log.d("MessagesHistoryViewModelImpl", "sendMessage: ERROR: $error")
|
||||
|
||||
val failedId = -500_000L - failedMessages.size
|
||||
val newFailedMessage = newMessage.copy(id = failedId)
|
||||
failedMessages += newFailedMessage
|
||||
@@ -1015,6 +1102,51 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmDeleteCurrentEditMessage() {
|
||||
val currentMessage = editMessage ?: return
|
||||
|
||||
this.dialog.setValue {
|
||||
MessageDialog.MessageDelete(currentMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun editCurrentEditMessage() {
|
||||
replyToCmId = null
|
||||
|
||||
val newText = screenState.value.message.text
|
||||
|
||||
val lastText = lastMessageText.orEmpty().trim()
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
editCmId = null,
|
||||
message = TextFieldValue(
|
||||
text = lastText,
|
||||
selection = TextRange(lastText.length)
|
||||
),
|
||||
actionMode = if (lastText.isBlank()) ActionMode.RECORD_AUDIO
|
||||
else ActionMode.SEND,
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: save last reply
|
||||
replyTitle = null,
|
||||
replyText = null
|
||||
)
|
||||
}
|
||||
|
||||
syncUiMessages()
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: actually edit message
|
||||
|
||||
val newMessage = editMessage?.copy(
|
||||
replyMessage = if (replyToCmId == null) null else editMessage?.replyMessage,
|
||||
text = newText
|
||||
) ?: return
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: check if message is exact same, then do not edit
|
||||
|
||||
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
|
||||
}
|
||||
|
||||
private fun markAsImportant(
|
||||
messageIds: List<Long>,
|
||||
important: Boolean,
|
||||
@@ -1118,29 +1250,6 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
fun editMessage(
|
||||
originalMessage: VkMessage,
|
||||
peerid: Long,
|
||||
messageid: Long,
|
||||
newText: String? = null,
|
||||
attachments: List<VkAttachment>? = null,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// sendRequest {
|
||||
// messagesRepository.edit(
|
||||
// MessagesEditRequest(
|
||||
// peerId = peerId,
|
||||
// messageId = messageId,
|
||||
// message = newText,
|
||||
// attachments = attachments
|
||||
// )
|
||||
// )
|
||||
// } ?: return@launch
|
||||
|
||||
// TODO: 25.08.2023, Danil Nikolaev: update message
|
||||
}
|
||||
}
|
||||
|
||||
private fun readMessage(message: VkMessage) {
|
||||
messagesUseCase.markAsRead(
|
||||
peerId = screenState.value.convoId,
|
||||
@@ -1237,7 +1346,8 @@ class MessagesHistoryViewModelImpl(
|
||||
nextMessage = messages.getOrNull(index - 1),
|
||||
showTimeInActionMessages = AppSettings.Experimental.showTimeInActionMessages,
|
||||
convo = screenState.value.convo,
|
||||
isSelected = selectedMessages.indexOfFirstOrNull { it.id == message.id } != null
|
||||
isSelected = screenState.value.editCmId == message.cmId ||
|
||||
selectedMessages.indexOfFirstOrNull { it.id == message.id } != null
|
||||
)
|
||||
}
|
||||
uiMessages.setValue { newUiMessages }
|
||||
|
||||
+3
-1
@@ -26,7 +26,8 @@ data class MessagesHistoryScreenState(
|
||||
val pinnedTitle: String?,
|
||||
val pinnedSummary: AnnotatedString?,
|
||||
val replyTitle: String?,
|
||||
val replyText: AnnotatedString?
|
||||
val replyText: AnnotatedString?,
|
||||
val editCmId: Long?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -48,6 +49,7 @@ data class MessagesHistoryScreenState(
|
||||
pinnedSummary = null,
|
||||
replyTitle = null,
|
||||
replyText = null,
|
||||
editCmId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -81,7 +81,7 @@ fun InputBar(
|
||||
actionMode: ActionMode,
|
||||
replyTitle: String?,
|
||||
replyText: AnnotatedString?,
|
||||
inputFieldFocusRequester: Boolean,
|
||||
showKeyboard: Boolean,
|
||||
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||
onBoldRequested: () -> Unit = {},
|
||||
onItalicRequested: () -> Unit = {},
|
||||
@@ -92,7 +92,8 @@ fun InputBar(
|
||||
onEmojiButtonLongClicked: () -> Unit = {},
|
||||
onAttachmentButtonClicked: () -> Unit = {},
|
||||
onActionButtonClicked: () -> Unit = {},
|
||||
onReplyCloseClicked: () -> Unit = {}
|
||||
onReplyCloseClicked: () -> Unit = {},
|
||||
onKeyboardShown: () -> Unit
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
@@ -106,8 +107,9 @@ fun InputBar(
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(inputFieldFocusRequester) {
|
||||
if (inputFieldFocusRequester) {
|
||||
LaunchedEffect(showKeyboard) {
|
||||
if (showKeyboard) {
|
||||
onKeyboardShown()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
@@ -360,6 +362,7 @@ private fun InputBarPreview() {
|
||||
actionMode = ActionMode.SEND,
|
||||
replyTitle = "Иннокентий Панфилович",
|
||||
replyText = "Ого, ром!".annotated(),
|
||||
inputFieldFocusRequester = false
|
||||
showKeyboard = false,
|
||||
onKeyboardShown = {}
|
||||
)
|
||||
}
|
||||
|
||||
+4
-2
@@ -29,7 +29,7 @@ fun MessagesHistoryRoute(
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
|
||||
val inputFieldFocusRequester by viewModel.inputFieldFocusRequester.collectAsStateWithLifecycle()
|
||||
val showKeyboard by viewModel.showKeyboard.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(navigationEvent) {
|
||||
val needToConsume = when (val navigation = navigationEvent) {
|
||||
@@ -55,7 +55,7 @@ fun MessagesHistoryRoute(
|
||||
canPaginate = canPaginate,
|
||||
showEmojiButton = AppSettings.General.showEmojiButton,
|
||||
showAttachmentButton = AppSettings.General.showAttachmentButton,
|
||||
inputFieldFocusRequester = inputFieldFocusRequester,
|
||||
showKeyboard = showKeyboard,
|
||||
onBack = onBack,
|
||||
onClose = viewModel::onCloseButtonClicked,
|
||||
onScrolledToIndex = viewModel::onScrolledToIndex,
|
||||
@@ -72,6 +72,7 @@ fun MessagesHistoryRoute(
|
||||
onPhotoClicked = onNavigateToPhotoViewer,
|
||||
onPinnedMessageClicked = viewModel::onPinnedMessageClicked,
|
||||
onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked,
|
||||
onEditSelectedMessageClicked = viewModel::onEditSelectedMessageClicked,
|
||||
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked,
|
||||
onBoldRequested = viewModel::onBoldClicked,
|
||||
onItalicRequested = viewModel::onItalicClicked,
|
||||
@@ -80,6 +81,7 @@ fun MessagesHistoryRoute(
|
||||
onRegularRequested = viewModel::onRegularClicked,
|
||||
onReplyCloseClicked = viewModel::onReplyCloseClicked,
|
||||
onRequestReplyToMessage = viewModel::onRequestReplyToMessage,
|
||||
onKeyboardShown = viewModel::onKeyboardShown
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
|
||||
+21
-7
@@ -31,12 +31,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.domain.util.indexOfMessageByCmId
|
||||
@@ -69,13 +71,14 @@ fun MessagesHistoryScreen(
|
||||
canPaginate: Boolean = false,
|
||||
showEmojiButton: Boolean = false,
|
||||
showAttachmentButton: Boolean = false,
|
||||
inputFieldFocusRequester: Boolean,
|
||||
showKeyboard: Boolean,
|
||||
onBack: () -> Unit = {},
|
||||
onClose: () -> Unit = {},
|
||||
onScrolledToIndex: () -> Unit = {},
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onTopBarClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onEditSelectedMessageClicked: () -> Unit = {},
|
||||
onPaginationConditionsMet: () -> Unit = {},
|
||||
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||
onAttachmentButtonClicked: () -> Unit = {},
|
||||
@@ -93,7 +96,8 @@ fun MessagesHistoryScreen(
|
||||
onUnderlineRequested: () -> Unit = {},
|
||||
onRegularRequested: () -> Unit = {},
|
||||
onReplyCloseClicked: () -> Unit = {},
|
||||
onRequestReplyToMessage: (cmId: Long) -> Unit = {}
|
||||
onRequestReplyToMessage: (cmId: Long) -> Unit = {},
|
||||
onKeyboardShown: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
@@ -114,7 +118,7 @@ fun MessagesHistoryScreen(
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = selectedMessages.isNotEmpty(),
|
||||
enabled = selectedMessages.isNotEmpty() || screenState.editCmId != null,
|
||||
onBack = onClose
|
||||
)
|
||||
|
||||
@@ -162,6 +166,9 @@ fun MessagesHistoryScreen(
|
||||
derivedStateOf { selectedMessages.size == 1 }
|
||||
}
|
||||
|
||||
val isLoadingText = stringResource(R.string.title_loading)
|
||||
val editMessageText = stringResource(R.string.title_edit_message)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
@@ -169,7 +176,8 @@ fun MessagesHistoryScreen(
|
||||
val topBarTitle by remember(screenState, selectedMessages) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
screenState.isLoading -> context.getString(R.string.title_loading)
|
||||
screenState.isLoading -> isLoadingText
|
||||
screenState.editCmId != null -> editMessageText
|
||||
selectedMessages.isNotEmpty() -> "(${selectedMessages.size})"
|
||||
else -> screenState.title
|
||||
}
|
||||
@@ -179,13 +187,16 @@ fun MessagesHistoryScreen(
|
||||
MessagesHistoryTopBarContainer(
|
||||
hazeState = hazeState,
|
||||
showReplyAction = showReplyAction,
|
||||
showEditAction = selectedMessages.size == 1,
|
||||
topBarContainerColor = topBarContainerColor,
|
||||
topBarContainerColorAlpha = topBarContainerColorAlpha,
|
||||
isClickable = !(screenState.isLoading && messages.isEmpty()),
|
||||
isMessagesSelecting = selectedMessages.isNotEmpty(),
|
||||
isPeerAccount = screenState.convoId == UserConfig.userId,
|
||||
avatar = screenState.avatar,
|
||||
avatarUrl = screenState.avatar.takeIf { it is UiImage.Url }?.extractUrl(),
|
||||
avatarResourceId = screenState.avatar.takeIf { it is UiImage.Resource }?.extractResId(),
|
||||
title = topBarTitle,
|
||||
isEditing = screenState.editCmId != null,
|
||||
showHorizontalProgressBar = screenState.isLoading && messages.isNotEmpty(),
|
||||
showPinnedContainer = !screenState.isLoading && pinnedMessage != null,
|
||||
pinnedMessage = pinnedMessage,
|
||||
@@ -196,6 +207,7 @@ fun MessagesHistoryScreen(
|
||||
onBack = onBack,
|
||||
onClose = onClose,
|
||||
onDeleteSelectedButtonClicked = onDeleteSelectedButtonClicked,
|
||||
onEditSelectedMessageClicked = onEditSelectedMessageClicked,
|
||||
onRefresh = onRefresh,
|
||||
onPinnedMessageClicked = onPinnedMessageClicked,
|
||||
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
||||
@@ -211,6 +223,7 @@ fun MessagesHistoryScreen(
|
||||
) {
|
||||
MessagesList(
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
screenState = screenState,
|
||||
hazeState = hazeState,
|
||||
listState = listState,
|
||||
hasPinnedMessage = pinnedMessage != null,
|
||||
@@ -259,12 +272,13 @@ fun MessagesHistoryScreen(
|
||||
actionMode = screenState.actionMode,
|
||||
replyTitle = screenState.replyTitle,
|
||||
replyText = screenState.replyText,
|
||||
inputFieldFocusRequester = inputFieldFocusRequester,
|
||||
showKeyboard = showKeyboard,
|
||||
onSetMessageBarHeight = { messageBarHeight = it },
|
||||
onEmojiButtonLongClicked = onEmojiButtonLongClicked,
|
||||
onAttachmentButtonClicked = onAttachmentButtonClicked,
|
||||
onActionButtonClicked = onActionButtonClicked,
|
||||
onReplyCloseClicked = onReplyCloseClicked
|
||||
onReplyCloseClicked = onReplyCloseClicked,
|
||||
onKeyboardShown = onKeyboardShown
|
||||
)
|
||||
|
||||
when {
|
||||
|
||||
+60
-43
@@ -2,6 +2,7 @@ package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -31,7 +32,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -44,11 +44,9 @@ import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.getImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
@@ -56,16 +54,20 @@ fun MessagesHistoryTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
hazeState: HazeState,
|
||||
showReplyAction: Boolean,
|
||||
showEditAction: Boolean,
|
||||
isClickable: Boolean,
|
||||
isMessagesSelecting: Boolean,
|
||||
isPeerAccount: Boolean,
|
||||
avatar: UiImage,
|
||||
avatarUrl: String?,
|
||||
avatarResourceId: Int?,
|
||||
title: String,
|
||||
isEditing: Boolean,
|
||||
onTopBarClicked: () -> Unit = {},
|
||||
onBack: () -> Unit = {},
|
||||
onClose: () -> Unit = {},
|
||||
onDeleteSelectedButtonClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {}
|
||||
onRefresh: () -> Unit = {},
|
||||
onEditSelectedMessageClicked: () -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val theme = LocalThemeConfig.current
|
||||
@@ -96,50 +98,55 @@ fun MessagesHistoryTopBar(
|
||||
// modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (!isMessagesSelecting) {
|
||||
if (isPeerAccount) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_bookmark_round_24),
|
||||
contentDescription = "Favorites icon",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val actualAvatar = avatar.getImage()
|
||||
|
||||
if (actualAvatar is Painter) {
|
||||
Image(
|
||||
painter = actualAvatar,
|
||||
contentDescription = null,
|
||||
AnimatedVisibility(!isMessagesSelecting && !isEditing) {
|
||||
Row {
|
||||
if (isPeerAccount) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_bookmark_round_24),
|
||||
contentDescription = "Favorites icon",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = actualAvatar,
|
||||
contentDescription = "Profile Image",
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = R.drawable.ic_account_circle_fill_round_24),
|
||||
)
|
||||
}
|
||||
}
|
||||
when {
|
||||
avatarUrl != null -> {
|
||||
AsyncImage(
|
||||
model = avatarUrl,
|
||||
contentDescription = "Profile Image",
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = R.drawable.ic_account_circle_fill_round_24),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
avatarResourceId != null -> {
|
||||
Image(
|
||||
painter = painterResource(avatarResourceId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -150,11 +157,11 @@ fun MessagesHistoryTopBar(
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!isMessagesSelecting) onBack()
|
||||
if (!isMessagesSelecting && !isEditing) onBack()
|
||||
else onClose()
|
||||
}
|
||||
) {
|
||||
Crossfade(targetState = !isMessagesSelecting) { state ->
|
||||
Crossfade(targetState = !isMessagesSelecting && !isEditing) { state ->
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
if (state) {
|
||||
@@ -210,6 +217,16 @@ fun MessagesHistoryTopBar(
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(showEditAction) {
|
||||
IconButton(onClick = onEditSelectedMessageClicked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_edit_round_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onDeleteSelectedButtonClicked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete_round_24),
|
||||
|
||||
+11
-4
@@ -16,7 +16,6 @@ import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.common.extensions.orDots
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
|
||||
@@ -26,13 +25,16 @@ fun MessagesHistoryTopBarContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
hazeState: HazeState,
|
||||
showReplyAction: Boolean,
|
||||
showEditAction: Boolean,
|
||||
topBarContainerColor: Color,
|
||||
topBarContainerColorAlpha: Float,
|
||||
isClickable: Boolean,
|
||||
isMessagesSelecting: Boolean,
|
||||
isPeerAccount: Boolean,
|
||||
avatar: UiImage,
|
||||
avatarUrl: String?,
|
||||
avatarResourceId: Int?,
|
||||
title: String,
|
||||
isEditing: Boolean,
|
||||
showHorizontalProgressBar: Boolean,
|
||||
showPinnedContainer: Boolean,
|
||||
pinnedMessage: VkMessage?,
|
||||
@@ -44,6 +46,7 @@ fun MessagesHistoryTopBarContainer(
|
||||
onClose: () -> Unit = {},
|
||||
onDeleteSelectedButtonClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onEditSelectedMessageClicked: () -> Unit = {},
|
||||
onPinnedMessageClicked: (Long) -> Unit = {},
|
||||
onUnpinMessageButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
@@ -66,16 +69,20 @@ fun MessagesHistoryTopBarContainer(
|
||||
modifier = modifier,
|
||||
hazeState = hazeState,
|
||||
showReplyAction = showReplyAction,
|
||||
showEditAction = showEditAction,
|
||||
isClickable = isClickable,
|
||||
isMessagesSelecting = isMessagesSelecting,
|
||||
isPeerAccount = isPeerAccount,
|
||||
avatar = avatar,
|
||||
avatarUrl = avatarUrl,
|
||||
avatarResourceId = avatarResourceId,
|
||||
title = title,
|
||||
isEditing = isEditing,
|
||||
onTopBarClicked = onTopBarClicked,
|
||||
onBack = onBack,
|
||||
onClose = onClose,
|
||||
onDeleteSelectedButtonClicked = onDeleteSelectedButtonClicked,
|
||||
onRefresh = onRefresh
|
||||
onRefresh = onRefresh,
|
||||
onEditSelectedMessageClicked = onEditSelectedMessageClicked
|
||||
)
|
||||
|
||||
if (showHorizontalProgressBar) {
|
||||
|
||||
+48
-38
@@ -46,6 +46,7 @@ import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkFileDomain
|
||||
import dev.meloda.fast.model.api.domain.VkLinkDomain
|
||||
@@ -60,6 +61,7 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun MessagesList(
|
||||
modifier: Modifier = Modifier,
|
||||
screenState: MessagesHistoryScreenState,
|
||||
hasPinnedMessage: Boolean,
|
||||
hazeState: HazeState,
|
||||
listState: LazyListState,
|
||||
@@ -226,47 +228,55 @@ fun MessagesList(
|
||||
fadeOutSpec = null
|
||||
) else Modifier
|
||||
)
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
onMessageLongClicked(item.id)
|
||||
},
|
||||
onClick = { onMessageClicked(item.id) }
|
||||
)
|
||||
.pointerInput(item.cmId) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragCancel = {
|
||||
if (offsetX == -100f) {
|
||||
onRequestMessageReply(item.cmId)
|
||||
}
|
||||
.then(
|
||||
if (screenState.editCmId == null) {
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
)
|
||||
}
|
||||
onMessageLongClicked(item.id)
|
||||
},
|
||||
onClick = { onMessageClicked(item.id) }
|
||||
)
|
||||
.pointerInput(item.cmId) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragCancel = {
|
||||
if (offsetX == -100f) {
|
||||
onRequestMessageReply(item.cmId)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
animate = true
|
||||
offsetX = 0f
|
||||
offsetAnimatable.animateTo(0f)
|
||||
animate = false
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
if (offsetX == -100f) {
|
||||
onRequestMessageReply(item.cmId)
|
||||
}
|
||||
scope.launch {
|
||||
animate = true
|
||||
offsetX = 0f
|
||||
offsetAnimatable.animateTo(0f)
|
||||
animate = false
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
if (offsetX == -100f) {
|
||||
onRequestMessageReply(item.cmId)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
animate = true
|
||||
offsetX = 0f
|
||||
offsetAnimatable.animateTo(0f)
|
||||
animate = false
|
||||
scope.launch {
|
||||
animate = true
|
||||
offsetX = 0f
|
||||
offsetAnimatable.animateTo(0f)
|
||||
animate = false
|
||||
}
|
||||
},
|
||||
onHorizontalDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX =
|
||||
(offsetX + dragAmount).coerceIn(-100f, 0f)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onHorizontalDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX = (offsetX + dragAmount).coerceIn(-100f, 0f)
|
||||
}
|
||||
)
|
||||
},
|
||||
} else Modifier
|
||||
),
|
||||
color = backgroundColor
|
||||
) {
|
||||
if (item.isOut) {
|
||||
|
||||
Reference in New Issue
Block a user