From 9df35bf6bfcf558fdb76ecd2d01b7ba0742c7859 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Tue, 16 Jul 2024 04:52:47 +0300 Subject: [PATCH] Implement action messages in messages history --- .../fast/common/provider/ResourceProvider.kt | 4 +- .../meloda/app/fast/datastore/AppSettings.kt | 7 + .../meloda/app/fast/datastore/SettingsKeys.kt | 3 + .../meloda/app/fast/datastore/UserSettings.kt | 8 + .../MessagesHistoryViewModel.kt | 57 ++- .../model/MessagesHistoryScreenState.kt | 2 +- .../app/fast/messageshistory/model/UiItem.kt | 35 ++ .../fast/messageshistory/model/UiMessage.kt | 20 - .../presentation/ActionMessageItem.kt | 67 +++ .../presentation/IncomingMessageBubble.kt | 4 +- .../presentation/MessagesHistoryScreen.kt | 15 +- .../presentation/MessagesList.kt | 76 +-- .../presentation/OutgoingMessageBubble.kt | 4 +- .../app/fast/messageshistory/util/Ext.kt | 17 + .../messageshistory/util/MessageMapper.kt | 459 +++++++++++++++++- .../app/fast/settings/SettingsViewModel.kt | 26 +- 16 files changed, 709 insertions(+), 95 deletions(-) create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiItem.kt delete mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiMessage.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/ActionMessageItem.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/Ext.kt diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/provider/ResourceProvider.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/provider/ResourceProvider.kt index 0586a6fc..a9872d60 100644 --- a/core/common/src/main/kotlin/com/meloda/app/fast/common/provider/ResourceProvider.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/provider/ResourceProvider.kt @@ -4,10 +4,12 @@ import android.content.res.Resources interface ResourceProvider { + val resources: Resources + fun getString(resId: Int): String } -class ResourceProviderImpl(private val resources: Resources) : ResourceProvider { +class ResourceProviderImpl(override val resources: Resources) : ResourceProvider { override fun getString(resId: Int): String { return resources.getString(resId) diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/AppSettings.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/AppSettings.kt index 9da167d4..f0f96510 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/AppSettings.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/AppSettings.kt @@ -180,6 +180,13 @@ object AppSettings { ) set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value) + var showTimeInActionMessages: Boolean + get() = get( + SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES + ) + set(value) = put(SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, value) + var showDebugCategory: Boolean get() = get( SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt index db2ec922..e4ec8b0a 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt @@ -29,6 +29,9 @@ object SettingsKeys { const val DEFAULT_APPEARANCE_LANGUAGE = "" const val KEY_APPEARANCE_USE_BLUR = "appearance_use_blur" const val DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR = false + const val KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = + "appearance_show_time_in_action_messages" + const val DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = false const val KEY_FEATURES_FAST_TEXT = "features_fast_text" const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt index 9fd41add..473de2d0 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt @@ -23,6 +23,7 @@ interface UserSettings { val longPollInBackground: StateFlow val useBlur: StateFlow val showEmojiButton: StateFlow + val showTimeInActionMessages: StateFlow val showDebugCategory: StateFlow fun onUseContactNamesChanged(use: Boolean) @@ -42,6 +43,7 @@ interface UserSettings { fun onLongPollInBackgroundChanged(inBackground: Boolean) fun onUseBlurChanged(use: Boolean) fun onShowEmojiButtonChanged(show: Boolean) + fun onShowTimeInActionMessagesChanged(show: Boolean) fun onShowDebugCategoryChanged(show: Boolean) } @@ -64,6 +66,8 @@ class UserSettingsImpl : UserSettings { override val longPollInBackground = MutableStateFlow(AppSettings.Debug.longPollInBackground) override val useBlur = MutableStateFlow(AppSettings.Debug.useBlur) override val showEmojiButton = MutableStateFlow(AppSettings.Debug.showEmojiButton) + override val showTimeInActionMessages = + MutableStateFlow(AppSettings.Debug.showTimeInActionMessages) override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory) override fun onUseContactNamesChanged(use: Boolean) { @@ -118,6 +122,10 @@ class UserSettingsImpl : UserSettings { showEmojiButton.value = show } + override fun onShowTimeInActionMessagesChanged(show: Boolean) { + showTimeInActionMessages.value = show + } + override fun onShowDebugCategoryChanged(show: Boolean) { showDebugCategory.value = show } diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt index 91b15199..7efb6cfc 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt @@ -1,7 +1,6 @@ package com.meloda.app.fast.messageshistory import android.content.SharedPreferences -import android.content.res.Resources import android.util.Log import androidx.core.content.edit import androidx.lifecycle.SavedStateHandle @@ -14,14 +13,18 @@ import com.meloda.app.fast.common.UserConfig import com.meloda.app.fast.common.extensions.listenValue import com.meloda.app.fast.common.extensions.setValue import com.meloda.app.fast.common.extensions.updateValue +import com.meloda.app.fast.common.provider.ResourceProvider import com.meloda.app.fast.data.LongPollUpdatesParser import com.meloda.app.fast.data.VkMemoryCache import com.meloda.app.fast.data.api.conversations.ConversationsUseCase import com.meloda.app.fast.data.api.messages.MessagesUseCase import com.meloda.app.fast.data.processState +import com.meloda.app.fast.datastore.AppSettings import com.meloda.app.fast.datastore.SettingsKeys +import com.meloda.app.fast.datastore.UserSettings import com.meloda.app.fast.messageshistory.model.ActionMode import com.meloda.app.fast.messageshistory.model.MessagesHistoryScreenState +import com.meloda.app.fast.messageshistory.model.UiItem import com.meloda.app.fast.messageshistory.navigation.MessagesHistory import com.meloda.app.fast.messageshistory.util.asPresentation import com.meloda.app.fast.messageshistory.util.extractAvatar @@ -62,7 +65,8 @@ class MessagesHistoryViewModelImpl( private val messagesUseCase: MessagesUseCase, private val conversationsUseCase: ConversationsUseCase, private val preferences: SharedPreferences, - private val resources: Resources, + private val resourceProvider: ResourceProvider, + private val userSettings: UserSettings, updatesParser: LongPollUpdatesParser, savedStateHandle: SavedStateHandle ) : MessagesHistoryViewModel, ViewModel() { @@ -92,6 +96,8 @@ class MessagesHistoryViewModelImpl( updatesParser.onMessageEdited(::handleEditedMessage) updatesParser.onMessageIncomingRead(::handleReadIncomingEvent) updatesParser.onMessageOutgoingRead(::handleReadOutgoingEvent) + + userSettings.showTimeInActionMessages.listenValue(::toggleShowTimeInActionMessages) } override fun onRefresh() { @@ -169,19 +175,23 @@ class MessagesHistoryViewModelImpl( } val newMessage = message.asPresentation( + resourceProvider = resourceProvider, showDate = false, showName = false, prevMessage = prevMessage, - nextMessage = null + nextMessage = null, + showTimeInActionMessages = userSettings.showTimeInActionMessages.value ) newMessages.add(0, newMessage) prevMessage?.let { prev -> newMessages[1] = prev.asPresentation( + resourceProvider = resourceProvider, showDate = false, showName = false, prevMessage = prevMessage, - nextMessage = messages.value.first() + nextMessage = messages.value.first(), + showTimeInActionMessages = userSettings.showTimeInActionMessages.value ) } @@ -196,10 +206,12 @@ class MessagesHistoryViewModelImpl( .indexOfFirstOrNull { it.id == message.id } ?.let { index -> val newMessage = message.asPresentation( + resourceProvider = resourceProvider, showDate = false, showName = false, prevMessage = messages.value.getOrNull(index + 1), - nextMessage = messages.value.getOrNull(index - 1) + nextMessage = messages.value.getOrNull(index - 1), + showTimeInActionMessages = userSettings.showTimeInActionMessages.value ) val newMessages = screenState.value.messages.toMutableList() @@ -248,10 +260,12 @@ class MessagesHistoryViewModelImpl( val loadedMessages = fullMessages.mapIndexed { index, message -> message.asPresentation( + resourceProvider = resourceProvider, showDate = false, showName = false, prevMessage = messages.getOrNull(index + 1), nextMessage = messages.getOrNull(index - 1), + showTimeInActionMessages = userSettings.showTimeInActionMessages.value ) } @@ -268,11 +282,8 @@ class MessagesHistoryViewModelImpl( ?.let { conversation -> newState = newState.copy( title = conversation.extractTitle( - useContactName = preferences.getBoolean( - SettingsKeys.KEY_USE_CONTACT_NAMES, - SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES - ), - resources = resources + useContactName = AppSettings.General.useContactNames, + resources = resourceProvider.resources ), avatar = conversation.extractAvatar() ) @@ -337,10 +348,12 @@ class MessagesHistoryViewModelImpl( val newMessages = screenState.value.messages.toMutableList() val newUiMessage = newMessage.asPresentation( + resourceProvider = resourceProvider, showDate = false, showName = false, prevMessage = messages.value.firstOrNull(), - nextMessage = null + nextMessage = null, + showTimeInActionMessages = userSettings.showTimeInActionMessages.value ) newMessages.add(0, newUiMessage) @@ -373,7 +386,9 @@ class MessagesHistoryViewModelImpl( val messages = screenState.value.messages.toMutableList() messages.indexOfOrNull(newUiMessage)?.let { index -> - messages[index] = messages[index].copy(id = messageId) + (messages[index] as? UiItem.Message)?.let { message -> + messages[index] = message.copy(id = messageId) + } } screenState.setValue { old -> old.copy(messages = messages) } @@ -490,6 +505,24 @@ class MessagesHistoryViewModelImpl( } } + private fun toggleShowTimeInActionMessages(show: Boolean) { + val messages = messages.value + val uiMessages = messages.mapIndexed { index, item -> + item.asPresentation( + resourceProvider = resourceProvider, + showDate = false, + showName = false, + prevMessage = messages.getOrNull(index + 1), + nextMessage = messages.getOrNull(index - 1), + showTimeInActionMessages = show + ) + } + + screenState.setValue { old -> + old.copy(messages = uiMessages) + } + } + companion object { const val MESSAGES_LOAD_COUNT = 30 } diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryScreenState.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryScreenState.kt index e8534503..630bdce4 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryScreenState.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryScreenState.kt @@ -10,7 +10,7 @@ data class MessagesHistoryScreenState( val title: String, val status: String?, val avatar: UiImage, - val messages: List, + val messages: List, val message: String, val attachments: List, val isLoading: Boolean, diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiItem.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiItem.kt new file mode 100644 index 00000000..cefdcf74 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiItem.kt @@ -0,0 +1,35 @@ +package com.meloda.app.fast.messageshistory.model + +import androidx.compose.ui.text.AnnotatedString +import com.meloda.app.fast.common.model.UiImage + +sealed class UiItem( + open val id: Int, + val cmId: Int +) { + + data class Message( + override val id: Int, + val conversationMessageId: Int, + val text: String?, + val isOut: Boolean, + val fromId: Int, + val date: String, + val randomId: Int, + val isInChat: Boolean, + val name: String, + val showDate: Boolean, + val showAvatar: Boolean, + val showName: Boolean, + val avatar: UiImage, + val isEdited: Boolean + ) : UiItem(id, conversationMessageId) + + data class ActionMessage( + override val id: Int, + val conversationMessageId: Int, + val text: AnnotatedString, + val actionCmId: Int? + ) : UiItem(id, conversationMessageId) +} + diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiMessage.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiMessage.kt deleted file mode 100644 index 6a3f52ab..00000000 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiMessage.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.app.fast.messageshistory.model - -import com.meloda.app.fast.common.model.UiImage - -data class UiMessage( - val id: Int, - val conversationMessageId: Int, - val text: String?, - val isOut: Boolean, - val fromId: Int, - val date: String, - val randomId: Int, - val isInChat: Boolean, - val name: String, - val showDate: Boolean, - val showAvatar: Boolean, - val showName: Boolean, - val avatar: UiImage, - val isEdited: Boolean -) diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/ActionMessageItem.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/ActionMessageItem.kt new file mode 100644 index 00000000..a6d5ee23 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/ActionMessageItem.kt @@ -0,0 +1,67 @@ +package com.meloda.app.fast.messageshistory.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.messageshistory.model.UiItem + +@Composable +fun ActionMessageItem( + item: UiItem.ActionMessage, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Text( + text = item.text, + modifier = modifier + .padding(horizontal = 32.dp) + .clip(RoundedCornerShape(12.dp)) + .then( + if (item.actionCmId != null) { + Modifier.clickable(onClick = onClick) + } + else Modifier + ) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)) + .fillMaxWidth() + .padding( + horizontal = 32.dp, + vertical = 4.dp + ), + textAlign = TextAlign.Center + ) +} + +@Preview +@Composable +fun ActionMessageItemPreview() { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) { + ActionMessageItem( + item = UiItem.ActionMessage( + id = 0, + text = buildAnnotatedString { + append("You pinned message \"wow hello there\"") + }, + actionCmId = null, + conversationMessageId = 2135 + ) + ) + } +} diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/IncomingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/IncomingMessageBubble.kt index fc764ea6..5c325fda 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/IncomingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/IncomingMessageBubble.kt @@ -25,12 +25,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import coil.imageLoader -import com.meloda.app.fast.messageshistory.model.UiMessage +import com.meloda.app.fast.messageshistory.model.UiItem @Composable fun IncomingMessageBubble( modifier: Modifier = Modifier, - message: UiMessage, + message: UiItem.Message, ) { val context = LocalContext.current diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt index d7aa929a..f6090fda 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -78,6 +78,8 @@ import com.meloda.app.fast.messageshistory.MessagesHistoryViewModel import com.meloda.app.fast.messageshistory.MessagesHistoryViewModelImpl import com.meloda.app.fast.messageshistory.model.ActionMode import com.meloda.app.fast.messageshistory.model.MessagesHistoryScreenState +import com.meloda.app.fast.messageshistory.util.firstMessage +import com.meloda.app.fast.messageshistory.util.indexOfMessageByCmId import com.meloda.app.fast.model.BaseError import com.meloda.app.fast.ui.theme.LocalThemeConfig import com.meloda.app.fast.ui.util.ImmutableList @@ -142,6 +144,8 @@ fun MessagesHistoryScreen( ) { val view = LocalView.current + val coroutineScope = rememberCoroutineScope() + val preferences: SharedPreferences = koinInject() val currentTheme = LocalThemeConfig.current @@ -250,7 +254,7 @@ fun MessagesHistoryScreen( onChatMaterialsDropdownItemClicked( screenState.conversationId, - screenState.messages.first().conversationMessageId + screenState.messages.firstMessage().conversationMessageId ) }, text = { @@ -313,7 +317,14 @@ fun MessagesHistoryScreen( immutableMessages = ImmutableList.copyOf(screenState.messages), isPaginating = screenState.isPaginating, enableAnimations = animationsEnabled, - messageBarHeight = messageBarHeight + messageBarHeight = messageBarHeight, + onRequestScrollToCmId = { cmId -> + coroutineScope.launch { + listState.animateScrollToItem( + index = screenState.messages.indexOfMessageByCmId(cmId) + ) + } + } ) Column( diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesList.kt index 5b9e099a..85ba2a44 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesList.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.meloda.app.fast.messageshistory.model.UiMessage +import com.meloda.app.fast.messageshistory.model.UiItem import com.meloda.app.fast.ui.theme.LocalThemeConfig import com.meloda.app.fast.ui.util.ImmutableList import dev.chrisbanes.haze.HazeState @@ -31,10 +31,11 @@ fun MessagesList( modifier: Modifier = Modifier, hazeState: HazeState, listState: LazyListState, - immutableMessages: ImmutableList, + immutableMessages: ImmutableList, isPaginating: Boolean, enableAnimations: Boolean, - messageBarHeight: Dp + messageBarHeight: Dp, + onRequestScrollToCmId: (cmId: Int) -> Unit = {} ) { val messages = immutableMessages.toList() val currentTheme = LocalThemeConfig.current @@ -65,32 +66,53 @@ fun MessagesList( items( items = messages, - key = UiMessage::id, - ) { message -> - if (message.isOut) { - OutgoingMessageBubble( - modifier = - Modifier.then( - if (enableAnimations) Modifier.animateItem( - fadeInSpec = null, - fadeOutSpec = null + key = UiItem::id, + contentType = { item -> + when (item) { + is UiItem.ActionMessage -> "action_message" + is UiItem.Message -> "message" + } + } + ) { item -> + when (item) { + is UiItem.ActionMessage -> { + ActionMessageItem( + item = item, + onClick = { + if (item.actionCmId != null) { + onRequestScrollToCmId(item.actionCmId) + } + } + ) + } + + is UiItem.Message -> { + if (item.isOut) { + OutgoingMessageBubble( + modifier = + Modifier.then( + if (enableAnimations) Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ) + else Modifier + ), + message = item, ) - else Modifier - ), - message = message, - ) - } else { - IncomingMessageBubble( - modifier = - Modifier.then( - if (enableAnimations) Modifier.animateItem( - fadeInSpec = null, - fadeOutSpec = null + } else { + IncomingMessageBubble( + modifier = + Modifier.then( + if (enableAnimations) Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ) + else Modifier + ), + message = item, ) - else Modifier - ), - message = message, - ) + } + } } Spacer(modifier = Modifier.height(8.dp)) diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/OutgoingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/OutgoingMessageBubble.kt index b31c80ab..4a703814 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/OutgoingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/OutgoingMessageBubble.kt @@ -14,12 +14,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.meloda.app.fast.common.extensions.orDots -import com.meloda.app.fast.messageshistory.model.UiMessage +import com.meloda.app.fast.messageshistory.model.UiItem @Composable fun OutgoingMessageBubble( modifier: Modifier = Modifier, - message: UiMessage, + message: UiItem.Message, ) { Row( modifier = modifier.fillMaxWidth(), diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/Ext.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/Ext.kt new file mode 100644 index 00000000..bf326d38 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/Ext.kt @@ -0,0 +1,17 @@ +package com.meloda.app.fast.messageshistory.util + +import com.meloda.app.fast.messageshistory.model.UiItem + +fun List.firstMessage(): UiItem.Message = first() as UiItem.Message + +fun List.indexOfMessageById(messageId: Int): Int = + indexOfFirst { it.id == messageId } + +fun List.findMessageById(messageId: Int): UiItem.Message = + first { it.id == messageId } as UiItem.Message + +fun List.indexOfMessageByCmId(cmId: Int): Int = + indexOfFirst { it.cmId == cmId } + +fun List.findMessageByCmId(cmId: Int): UiItem.Message = + first { it.cmId == cmId } as UiItem.Message diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/MessageMapper.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/MessageMapper.kt index 083f3a34..2fee8b82 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/MessageMapper.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/MessageMapper.kt @@ -1,17 +1,22 @@ package com.meloda.app.fast.messageshistory.util import android.content.res.Resources -import com.meloda.app.fast.common.model.UiImage -import com.meloda.app.fast.common.model.UiText +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight import com.meloda.app.fast.common.UserConfig import com.meloda.app.fast.common.extensions.orDots +import com.meloda.app.fast.common.model.UiImage +import com.meloda.app.fast.common.model.UiText import com.meloda.app.fast.common.model.parseString +import com.meloda.app.fast.common.provider.ResourceProvider import com.meloda.app.fast.data.VkMemoryCache -import com.meloda.app.fast.ui.R -import com.meloda.app.fast.messageshistory.model.UiMessage +import com.meloda.app.fast.messageshistory.model.UiItem import com.meloda.app.fast.model.api.PeerType import com.meloda.app.fast.model.api.domain.VkConversation import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.ui.R import java.text.SimpleDateFormat import java.util.Locale import com.meloda.app.fast.ui.R as UiR @@ -85,26 +90,42 @@ fun VkConversation.extractTitle( }.parseString(resources).orDots() fun VkMessage.asPresentation( + resourceProvider: ResourceProvider, showDate: Boolean, showName: Boolean, prevMessage: VkMessage?, - nextMessage: VkMessage? -): UiMessage = UiMessage( - id = id, - conversationMessageId = conversationMessageId, - text = text, - isOut = isOut, - fromId = fromId, - date = extractDate(), - randomId = randomId, - isInChat = isPeerChat(), - name = extractTitle(), - showDate = showDate, - showAvatar = extractShowAvatar(nextMessage), - showName = showName && extractShowName(prevMessage), - avatar = extractAvatar(), - isEdited = updateTime != null -) + nextMessage: VkMessage?, + showTimeInActionMessages: Boolean +): UiItem = when { + action != null -> UiItem.ActionMessage( + id = id, + conversationMessageId = conversationMessageId, + text = extractActionText( + resources = resourceProvider.resources, + youPrefix = resourceProvider.getString(R.string.you_message_prefix), + showTime = showTimeInActionMessages + ) ?: buildAnnotatedString { }, + actionCmId = actionConversationMessageId + ) + + else -> UiItem.Message( + id = id, + conversationMessageId = conversationMessageId, + text = text, + isOut = isOut, + fromId = fromId, + date = extractDate(), + randomId = randomId, + isInChat = isPeerChat(), + name = extractTitle(), + showDate = showDate, + showAvatar = extractShowAvatar(nextMessage), + showName = showName && extractShowName(prevMessage), + avatar = extractAvatar(), + isEdited = updateTime != null + ) +} + fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean { if (isOut) return false @@ -115,3 +136,399 @@ fun VkMessage.extractShowName(prevMessage: VkMessage?): Boolean { if (isOut || !isPeerChat()) return false return prevMessage == null || prevMessage.fromId != fromId } + +fun VkMessage.extractActionText( + resources: Resources, + youPrefix: String, + showTime: Boolean +): AnnotatedString? { + val lastMessage = this + + val action = lastMessage.action ?: return null + + val formattedMessageDate = + SimpleDateFormat("HH:mm", Locale.getDefault()).format(lastMessage.date * 1000L) + + val fromId = lastMessage.fromId + val text = lastMessage.actionText.orDots() + val groupName = lastMessage.group?.name.orDots() + val userName = lastMessage.user?.fullName.orDots() + val actionGroupName = lastMessage.actionGroup?.name.orDots() + val actionUserName = lastMessage.actionUser?.fullName.orDots() + val memberId = lastMessage.actionMemberId + val isMemberUser = (memberId ?: 0) > 0 + val isMemberGroup = (memberId ?: 0) < 0 + + val prefix = when { + lastMessage.fromId == UserConfig.userId -> youPrefix + lastMessage.isGroup() -> groupName + lastMessage.isUser() -> userName + else -> null + }.orDots() + + val memberPrefix = when { + memberId == UserConfig.userId -> youPrefix + isMemberUser -> actionUserName + isMemberGroup -> actionGroupName + else -> null + }.orDots() + + return buildAnnotatedString { + when (action) { + VkMessage.Action.CHAT_CREATE -> { + val string = UiText.ResourceParams( + UiR.string.message_action_chat_created, + listOf(prefix, text) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + + val textStartIndex = string.indexOf(text) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = textStartIndex, + end = textStartIndex + text.length + ) + } + + VkMessage.Action.CHAT_TITLE_UPDATE -> { + val string = UiText.ResourceParams( + UiR.string.message_action_chat_renamed, + listOf(prefix, text) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + + val textStartIndex = string.indexOf(text) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = textStartIndex, + end = textStartIndex + text.length + ) + } + + VkMessage.Action.CHAT_PHOTO_UPDATE -> { + UiText.ResourceParams( + UiR.string.message_action_chat_photo_update, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_PHOTO_REMOVE -> { + UiText.ResourceParams( + UiR.string.message_action_chat_photo_remove, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_KICK_USER -> { + if (memberId == fromId) { + UiText.ResourceParams( + UiR.string.message_action_chat_user_left, + listOf(memberPrefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = memberPrefix.length + ) + } else { + val postfix = + if (memberId == UserConfig.userId) youPrefix.lowercase() + else lastMessage.actionUser.toString() + + val string = UiText.ResourceParams( + UiR.string.message_action_chat_user_kicked, + listOf(prefix, postfix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + + val postfixStartIndex = string.indexOf(postfix) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = postfixStartIndex, + end = postfixStartIndex + postfix.length + ) + } + } + + VkMessage.Action.CHAT_INVITE_USER -> { + if (memberId == lastMessage.fromId) { + UiText.ResourceParams( + UiR.string.message_action_chat_user_returned, + listOf(memberPrefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = memberPrefix.length + ) + } else { + val postfix = + if (memberId == UserConfig.userId) youPrefix.lowercase() + else lastMessage.actionUser.toString() + + val string = UiText.ResourceParams( + UiR.string.message_action_chat_user_invited, + listOf(memberPrefix, postfix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + val postfixStartIndex = string.indexOf(postfix) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = postfixStartIndex, + end = postfixStartIndex + postfix.length + ) + } + } + + VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { + UiText.ResourceParams( + UiR.string.message_action_chat_user_joined_by_link, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { + UiText.ResourceParams( + UiR.string.message_action_chat_user_joined_by_call, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { + UiText.ResourceParams( + UiR.string.message_action_chat_user_joined_by_call_link, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_PIN_MESSAGE -> { + // TODO: 16/07/2024, Danil Nikolaev: get pinned message by cmid +// val messageText = lastMessage.text.orEmpty().trim() +// val croppedMessage = messageText.take(40) +// val hasMessageText = messageText.isNotEmpty() + + UiText.ResourceParams( + UiR.string.message_action_chat_pin_message, + listOf(prefix) + ).parseString(resources) + .orEmpty() +// .let { text -> +// if (hasMessageText) { +// text.plus("«%s»".format(croppedMessage)) +// .plus(if (messageText.length > 40) "..." else "") +// } else text +// } + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + +// if (hasMessageText) { +// val croppedIndex = fullText.indexOf(croppedMessage) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.Medium), +// start = croppedIndex - 1, +// end = croppedIndex - 1 + croppedMessage.length + 1 +// ) +// } + } + + VkMessage.Action.CHAT_UNPIN_MESSAGE -> { + UiText.ResourceParams( + UiR.string.message_action_chat_unpin_message, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_SCREENSHOT -> { + UiText.ResourceParams( + UiR.string.message_action_chat_screenshot, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + + VkMessage.Action.CHAT_STYLE_UPDATE -> { + UiText.ResourceParams( + UiR.string.message_action_chat_style_update, + listOf(prefix) + ).parseString(resources) + .orEmpty() + .let { text -> + if (showTime) { + text.plus("\n") + .plus(formattedMessageDate) + } else text + }.also(::append) + + addStyle( + style = SpanStyle(fontWeight = FontWeight.Medium), + start = 0, + end = prefix.length + ) + } + } + } +} diff --git a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt index 8157e097..c96b0c1b 100644 --- a/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt @@ -230,6 +230,12 @@ class SettingsViewModelImpl( userSettings.onShowEmojiButtonChanged(show) } + SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES -> { + val show = newValue as? Boolean + ?: SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES + userSettings.onShowTimeInActionMessagesChanged(show) + } + SettingsKeys.KEY_SHOW_DEBUG_CATEGORY -> { val show = newValue as? Boolean ?: false userSettings.onShowDebugCategoryChanged(show) @@ -349,12 +355,6 @@ class SettingsViewModelImpl( ) } } - val debugLongPollBackground = SettingsItem.Switch( - key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, - title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title), - text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary) - ) val activityTitle = SettingsItem.Title( key = "activity", @@ -382,6 +382,12 @@ class SettingsViewModelImpl( title = UiText.Simple("Show alert after crash"), text = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)") ) + val debugLongPollBackground = SettingsItem.Switch( + key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, + title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title), + text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary) + ) val debugUseBlur = SettingsItem.Switch( key = SettingsKeys.KEY_APPEARANCE_USE_BLUR, defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR, @@ -394,6 +400,11 @@ class SettingsViewModelImpl( text = UiText.Simple("Show emoji button in chat panel"), defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON ) + val debugShowTimeInActionMessages = SettingsItem.Switch( + key = SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, + defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, + title = UiText.Simple("Show time in action messages") + ) val debugHideDebugList = SettingsItem.TitleText( key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, @@ -432,7 +443,8 @@ class SettingsViewModelImpl( debugShowCrashAlert, debugLongPollBackground, debugUseBlur, - debugShowEmojiButton + debugShowEmojiButton, + debugShowTimeInActionMessages ).forEach(debugList::add) debugList += debugHideDebugList