diff --git a/README.md b/README.md index c4973f70..427c4da9 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Unofficial messenger for russian social network VKontakte - [ ] Link - [ ] TODO - [x] Send messages - - [ ] Pinned message + - [x] Pinned message - [ ] Pin & unpin messages - [ ] Reply to message - [ ] Delete message diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt index a9713a38..c02ad58c 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt @@ -46,20 +46,22 @@ interface MessagesRepository { title: String? ): ApiResult + suspend fun pin( + peerId: Int, + messageId: Int?, + conversationMessageId: Int? + ): ApiResult + + suspend fun unpin( + peerId: Int + ): ApiResult + suspend fun storeMessages(messages: List) // suspend fun markAsImportant( // params: MessagesMarkAsImportantRequest // ): ApiResult, RestApiErrorDomain> // -// suspend fun pin( -// params: MessagesPinMessageRequest -// ): ApiResult -// -// suspend fun unpin( -// params: MessagesUnPinMessageRequest -// ): ApiResult -// // suspend fun delete( // params: MessagesDeleteRequest // ): ApiResult diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt index 0dafa7fe..d7ab97b4 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -20,7 +20,9 @@ import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsReadRequest +import dev.meloda.fast.model.api.requests.MessagesPinMessageRequest import dev.meloda.fast.model.api.requests.MessagesSendRequest +import dev.meloda.fast.model.api.requests.MessagesUnPinMessageRequest import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiResult @@ -216,6 +218,32 @@ class MessagesRepositoryImpl( ) } + override suspend fun pin( + peerId: Int, + messageId: Int?, + conversationMessageId: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesPinMessageRequest( + peerId = peerId, + messageId = messageId, + conversationMessageId = conversationMessageId + ) + + messagesService.pin(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + apiResponse.requireResponse().asDomain() + }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun unpin( + peerId: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesUnPinMessageRequest(peerId = peerId) + messagesService.unpin(requestModel.map).mapApiDefault() + } + override suspend fun storeMessages(messages: List) { messageDao.insertAll(messages.map(VkMessage::asEntity)) } @@ -229,24 +257,6 @@ class MessagesRepositoryImpl( // ) // } // -// override suspend fun pin( -// params: MessagesPinMessageRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.pin(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun unpin( -// params: MessagesUnPinMessageRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.unpin(params.map).mapResult( -// successMapper = {}, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// // override suspend fun delete( // params: MessagesDeleteRequest // ): ApiResult = withContext(Dispatchers.IO) { @@ -293,4 +303,3 @@ class MessagesRepositoryImpl( // ) // } } - diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt index 1e91bded..4bb5af38 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt @@ -21,7 +21,7 @@ import dev.meloda.fast.model.database.VkUserEntity VkConversationEntity::class ], - version = 7 + version = 8 ) @TypeConverters(Converters::class) abstract class CacheDatabase : RoomDatabase() { diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt index ff7adfb8..4405d9ed 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.database.di import androidx.room.Room import dev.meloda.fast.database.AccountsDatabase +import dev.meloda.fast.database.CacheDatabase import org.koin.core.scope.Scope import org.koin.dsl.module @@ -12,7 +13,7 @@ val databaseModule = module { single { get().accountDao() } single { - Room.databaseBuilder(get(), dev.meloda.fast.database.CacheDatabase::class.java, "cache") + Room.databaseBuilder(get(), CacheDatabase::class.java, "cache") .fallbackToDestructiveMigration() .build() } @@ -22,4 +23,4 @@ val databaseModule = module { single { cacheDB().conversationDao() } } -private fun Scope.cacheDB(): dev.meloda.fast.database.CacheDatabase = get() +private fun Scope.cacheDB(): CacheDatabase = get() diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt index 7275c87d..65b602a8 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt @@ -47,6 +47,16 @@ interface MessagesUseCase { title: String? ): Flow> + fun pin( + peerId: Int, + messageId: Int?, + conversationMessageId: Int? + ): Flow> + + fun unpin( + peerId: Int + ): Flow> + suspend fun storeMessage(message: VkMessage) suspend fun storeMessages(messages: List) } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt index 3c6c9f52..b6a8da52 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt @@ -108,6 +108,30 @@ class MessagesUseCaseImpl( emit(newState) } + override fun pin( + peerId: Int, + messageId: Int?, + conversationMessageId: Int? + ): Flow> = flow { + emit(State.Loading) + + val newState = repository.pin( + peerId = peerId, + messageId = messageId, + conversationMessageId = conversationMessageId + ).mapToState() + + emit(newState) + } + + override fun unpin(peerId: Int): Flow> = flow { + emit(State.Loading) + + val newState = repository.unpin(peerId = peerId).mapToState() + + emit(newState) + } + override suspend fun storeMessage(message: VkMessage) { repository.storeMessages(listOf(message)) } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt index c4ee04ab..fdd76b7d 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt @@ -1,8 +1,8 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkMessage import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkMessage @JsonClass(generateAdapter = true) data class VkMessageData( @@ -23,7 +23,9 @@ data class VkMessageData( @Json(name = "action") val action: Action?, @Json(name = "ttl") val ttl: Int?, @Json(name = "reply_message") val replyMessage: VkMessageData?, - @Json(name = "update_time") val updateTime: Int? + @Json(name = "update_time") val updateTime: Int?, + @Json(name = "is_pinned") val isPinned: Boolean?, + @Json(name = "pinned_at") val pinnedAt: Int? ) { @JsonClass(generateAdapter = true) @@ -72,7 +74,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage( actionConversationMessageId = action?.conversationMessageId, actionMessage = action?.message, geoType = geo?.type, - important = important, + isImportant = important, updateTime = updateTime, forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain), attachments = attachments.map(VkAttachmentItemData::toDomain), @@ -81,4 +83,6 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage( group = null, actionUser = null, actionGroup = null, + pinnedAt = pinnedAt, + isPinned = isPinned == true ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt index 2b4b523a..2bf15c8d 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt @@ -41,7 +41,7 @@ data class VkPinnedMessageData( actionConversationMessageId = action?.conversationMessageId, actionMessage = action?.message, geoType = geo?.type, - important = important, + isImportant = important, updateTime = updateTime, forwards = forwards.orEmpty().map(VkMessageData::asDomain), @@ -52,6 +52,7 @@ data class VkPinnedMessageData( group = null, actionUser = null, actionGroup = null, + pinnedAt = null, + isPinned = true, ) } - diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt index 6829b0ce..29643ff1 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt @@ -18,8 +18,9 @@ data class VkMessage( val actionMessage: String?, val updateTime: Int?, - - val important: Boolean = false, + val pinnedAt: Int?, + val isPinned: Boolean, + val isImportant: Boolean = false, val forwards: List?, val attachments: List?, @@ -91,10 +92,12 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity( actionConversationMessageId = actionConversationMessageId, actionMessage = actionMessage, updateTime = updateTime, - important = important, + important = isImportant, forwardIds = forwards.orEmpty().map(VkMessage::id), // TODO: 05/05/2024, Danil Nikolaev: save attachments attachments = emptyList(), replyMessageId = replyMessage?.id, - geoType = geoType + geoType = geoType, + pinnedAt = pinnedAt, + isPinned = isPinned, ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt index 1eb9e8b5..652b7336 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt @@ -25,7 +25,9 @@ data class VkMessageEntity( val forwardIds: List?, val attachments: List?, // TODO: 01/05/2024, Danil Nikolaev: how to store??? val replyMessageId: Int?, - val geoType: String? + val geoType: String?, + val pinnedAt: Int?, + val isPinned: Boolean ) fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( @@ -43,7 +45,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( actionConversationMessageId = actionConversationMessageId, actionMessage = actionMessage, updateTime = updateTime, - important = important, + isImportant = important, forwards = emptyList(),//forwards.orEmpty().map(VkMessageEntity::asExternalModel), // TODO: 05/05/2024, Danil Nikolaev: restore attachments attachments = attachments.orEmpty().map { VkUnknownAttachment }, @@ -53,4 +55,6 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( group = null, actionUser = null, actionGroup = null, + pinnedAt = pinnedAt, + isPinned = isPinned ) diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt index e5a16e09..4ed126af 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.network.service.messages import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.data.VkLongPollData +import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse @@ -56,6 +57,18 @@ interface MessagesService { @FieldMap params: Map ): ApiResult, RestApiError> + @FormUrlEncoded + @POST(MessagesUrls.PIN) + suspend fun pin( + @FieldMap params: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(MessagesUrls.UNPIN) + suspend fun unpin( + @FieldMap params: Map + ): ApiResult, RestApiError> + // @FormUrlEncoded // @POST(MessagesUrls.MarkAsImportant) // suspend fun markAsImportant( @@ -63,18 +76,6 @@ interface MessagesService { // ): ApiResult>, RestApiError> // // @FormUrlEncoded -// @POST(MessagesUrls.Pin) -// suspend fun pin( -// @FieldMap params: Map -// ): ApiResult, RestApiError> -// -// @FormUrlEncoded -// @POST(MessagesUrls.Unpin) -// suspend fun unpin( -// @FieldMap params: Map -// ): ApiResult, RestApiError> -// -// @FormUrlEncoded // @POST(MessagesUrls.Delete) // suspend fun delete( // @FieldMap params: Map diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index bfd5122e..2c4b90a7 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -6,13 +6,16 @@ Да Нет Ответить + Переслать Пометить как важное - Помететить как не важное + Пометить как не важное Время: %1$s + Помеьиьб как не спам Закрепить Открепить Изменить Удалить + Скопировать Удалить сообщение? Для всех Пометить как спам @@ -229,4 +232,5 @@ Музыка Файлы Ссылки + Пометить как спам diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 65d4bcd3..38f44095 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -124,14 +124,19 @@ Signing out will delete all data related to this account from this device. Continue? Yes No + Time: %1$s + Reply + Forward Mark as important Unmark as important - Time: %1$s + Mark as spam + Unmark as spam Pin Unpin Edit Delete + Copy Delete the message? diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt index 4758deee..cf42aeb3 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt @@ -170,6 +170,6 @@ class ChatMaterialsViewModelImpl( } companion object { - const val LOAD_COUNT = 200 + const val LOAD_COUNT = 100 } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index f5fa9342..5fc8e4d1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -1,8 +1,6 @@ package dev.meloda.fast.messageshistory -import android.content.Context import android.util.Log -import android.widget.Toast import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue @@ -54,13 +52,18 @@ interface MessagesHistoryViewModel { val screenState: StateFlow val selectedMessages: StateFlow> + val isNeedToScrollToIndex: StateFlow + val baseError: StateFlow val imagesToPreload: StateFlow> - val currentOffset: StateFlow + val showMessageOptions: StateFlow + val currentOffset: StateFlow val canPaginate: StateFlow + fun onScrolledToIndex() + fun onCloseButtonClicked() fun onRefresh() fun onAttachmentButtonClicked() @@ -72,10 +75,12 @@ interface MessagesHistoryViewModel { fun onMessageClicked(messageId: Int) fun onMessageLongClicked(messageId: Int) + fun onMessageOptionsDialogDismissed() + fun onPinnedMessageClicked(messageId: Int) + fun onUnpinMessageClicked() } class MessagesHistoryViewModelImpl( - private val applicationContext: Context, private val messagesUseCase: MessagesUseCase, private val conversationsUseCase: ConversationsUseCase, private val resourceProvider: ResourceProvider, @@ -88,9 +93,13 @@ class MessagesHistoryViewModelImpl( override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY) override val selectedMessages = MutableStateFlow>(emptyList()) + override val isNeedToScrollToIndex = MutableStateFlow(null) + override val baseError = MutableStateFlow(null) override val imagesToPreload = MutableStateFlow>(emptyList()) + override val showMessageOptions = MutableStateFlow(null) + override val currentOffset = MutableStateFlow(0) override val canPaginate = MutableStateFlow(false) @@ -120,6 +129,10 @@ class MessagesHistoryViewModelImpl( ) } + override fun onScrolledToIndex() { + isNeedToScrollToIndex.setValue { null } + } + override fun onCloseButtonClicked() { screenState.setValue { old -> old.copy( @@ -211,7 +224,9 @@ class MessagesHistoryViewModelImpl( } } } else { - Toast.makeText(applicationContext, "Click", Toast.LENGTH_SHORT).show() + messages.value.firstOrNull { it.id == currentMessage.id }?.let { message -> + showMessageOptions.setValue { message } + } } } @@ -240,6 +255,62 @@ class MessagesHistoryViewModelImpl( } } + override fun onMessageOptionsDialogDismissed() { + showMessageOptions.setValue { null } + } + + override fun onPinnedMessageClicked(messageId: Int) { + val messageIndex = screenState.value.messages.indexOfFirstOrNull { + it is UiItem.Message && it.id == messageId + } + + if (messageIndex == null) { // сообщения нет в списке + // pizdets + } else { + isNeedToScrollToIndex.setValue { messageIndex } + } + } + + override fun onUnpinMessageClicked() { + // TODO: 27.03.2025, Danil Nikolaev: confirmation alert + val pinnedMessageId = screenState.value.pinnedMessage?.id ?: return + unpinMessage(pinnedMessageId) + } + + private fun unpinMessage(messageId: Int) { + val messageIndex = screenState.value.messages.indexOfFirstOrNull { + it is UiItem.Message && it.id == messageId + } + + messagesUseCase.unpin(screenState.value.conversationId) + .listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { + var newState = screenState.value.copy( + pinnedMessage = null, + conversation = screenState.value.conversation.copy( + pinnedMessage = null, + pinnedMessageId = null + ), + pinnedSummary = null, + pinnedTitle = null + ) + + if (messageIndex != null) { + val newMessages = screenState.value.messages.toMutableList() + val currentMessage: UiItem.Message = + newMessages[messageIndex] as UiItem.Message + newMessages[messageIndex] = currentMessage.copy(isPinned = false) + newState = newState.copy(messages = newMessages) + } + + screenState.setValue { old -> newState } + } + ) + } + } + private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message @@ -543,7 +614,7 @@ class MessagesHistoryViewModelImpl( actionConversationMessageId = null, actionMessage = null, updateTime = null, - important = false, + isImportant = false, forwards = null, attachments = null, replyMessage = null, @@ -551,7 +622,9 @@ class MessagesHistoryViewModelImpl( user = VkMemoryCache.getUser(UserConfig.userId), group = null, actionUser = null, - actionGroup = null + actionGroup = null, + isPinned = false, + pinnedAt = null ) sendingMessages += newMessage diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt index a7013fc1..ae764c40 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt @@ -24,8 +24,9 @@ sealed class UiItem( val avatar: UiImage, val isEdited: Boolean, val isRead: Boolean, - val sendingStatus: SendingStatus = SendingStatus.SENT, - val isSelected: Boolean = false + val sendingStatus: SendingStatus, + val isSelected: Boolean, + val isPinned: Boolean ) : UiItem(id, conversationMessageId) data class ActionMessage( diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ActionMessageItem.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ActionMessageItem.kt index faa0608e..8a095842 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ActionMessageItem.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/ActionMessageItem.kt @@ -27,13 +27,15 @@ fun ActionMessageItem( Text( text = item.text, modifier = modifier - .padding(horizontal = 32.dp) + .padding( + horizontal = 32.dp, + vertical = 4.dp + ) .clip(RoundedCornerShape(12.dp)) .then( if (item.actionCmId != null) { Modifier.clickable(onClick = onClick) - } - else Modifier + } else Modifier ) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)) .fillMaxWidth() diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt index dd1d960a..72f631cd 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.messageshistory.presentation import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,10 +34,10 @@ fun IncomingMessageBubble( message: UiItem.Message, animate: Boolean, ) { - Row(modifier = modifier.fillMaxWidth()) { + Row(modifier = modifier.fillMaxWidth().then(if (animate) Modifier.animateContentSize() else Modifier),) { Row( modifier = Modifier - .fillMaxWidth(0.75f) + .fillMaxWidth(0.85f) .padding(start = 16.dp), verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.Start @@ -82,7 +83,8 @@ fun IncomingMessageBubble( edited = message.isEdited, animate = animate, isRead = message.isRead, - sendingStatus = message.sendingStatus + sendingStatus = message.sendingStatus, + pinned = message.isPinned ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt index f4c1459c..2f07847e 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -40,7 +41,8 @@ fun MessageBubble( edited: Boolean, animate: Boolean, isRead: Boolean, - sendingStatus: SendingStatus + sendingStatus: SendingStatus, + pinned: Boolean ) { val backgroundColor = if (!isOut) { MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) @@ -63,12 +65,14 @@ fun MessageBubble( horizontal = 8.dp, vertical = 6.dp ) + .then(if (animate) Modifier.animateContentSize() else Modifier), ) { val minDateContainerWidth = remember(edited, isOut) { val mainPart = if (edited) 50.dp else 30.dp val readIndicatorPart = if (isOut) 14.dp else 0.dp + val pinnedIndicatorPart = if (pinned) 14.dp else 0.dp - mainPart + readIndicatorPart + mainPart + readIndicatorPart + pinnedIndicatorPart } val dateContainerWidth by animateDpAsState( @@ -94,7 +98,18 @@ fun MessageBubble( modifier = Modifier .align(Alignment.BottomEnd) .defaultMinSize(minWidth = dateContainerWidth) + .then(if (animate) Modifier.animateContentSize() else Modifier), ) { + if (pinned) { + Icon( + painter = painterResource(UiR.drawable.ic_round_push_pin_24), + contentDescription = null, + modifier = Modifier + .size(14.dp) + .rotate(45f) + ) + Spacer(modifier = Modifier.width(4.dp)) + } if (edited) { Icon( imageVector = Icons.Rounded.Create, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 5ab6f569..dfdbf582 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -64,6 +64,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color @@ -100,6 +101,8 @@ import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.IconButton +import dev.meloda.fast.ui.components.MaterialDialog +import dev.meloda.fast.ui.components.SelectionType import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.getImage @@ -119,18 +122,22 @@ fun MessagesHistoryRoute( val selectedMessages by viewModel.selectedMessages.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + val showMessageOptions by viewModel.showMessageOptions.collectAsStateWithLifecycle() + val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle() val userSettings: UserSettings = koinInject() val showEmojiButton by userSettings.showEmojiButton.collectAsStateWithLifecycle() MessagesHistoryScreen( screenState = screenState, + scrollIndex = scrollIndex, selectedMessages = ImmutableList.copyOf(selectedMessages), baseError = baseError, canPaginate = canPaginate, showEmojiButton = showEmojiButton, onBack = onBack, onClose = viewModel::onCloseButtonClicked, + onScrolledToIndex = viewModel::onScrolledToIndex, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked, onRefresh = viewModel::onRefresh, @@ -140,8 +147,45 @@ fun MessagesHistoryRoute( onActionButtonClicked = viewModel::onActionButtonClicked, onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked, onMessageClicked = viewModel::onMessageClicked, - onMessageLongClicked = viewModel::onMessageLongClicked + onMessageLongClicked = viewModel::onMessageLongClicked, + onPinnedMessageClicked = viewModel::onPinnedMessageClicked, + onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked ) + + if (showMessageOptions != null) { + val message = showMessageOptions!! + + val messageOptions = mutableListOf( + stringResource(UiR.string.message_context_action_reply), + stringResource(UiR.string.message_context_action_forward) + ) + + if (message.isPeerChat() && screenState.conversation.canChangePin) { + messageOptions += stringResource( + if (message.isPinned) UiR.string.message_context_action_unpin + else UiR.string.message_context_action_pin + ) + } + + messageOptions += stringResource(UiR.string.message_context_action_copy) + messageOptions += stringResource( + if (message.isImportant) UiR.string.message_context_action_unmark_as_important + else UiR.string.message_context_action_mark_as_important + ) + +// if (!message.isOut) { +// messageOptions += "Mark as spam" +// } + + messageOptions += stringResource(UiR.string.message_context_action_delete) + + MaterialDialog( + onDismissRequest = viewModel::onMessageOptionsDialogDismissed, + selectionType = SelectionType.None, + items = ImmutableList.copyOf(messageOptions), + confirmText = stringResource(UiR.string.ok) + ) + } } @OptIn( @@ -152,12 +196,14 @@ fun MessagesHistoryRoute( @Composable fun MessagesHistoryScreen( screenState: MessagesHistoryScreenState = MessagesHistoryScreenState.EMPTY, + scrollIndex: Int? = null, selectedMessages: ImmutableList = ImmutableList.empty(), baseError: BaseError? = null, canPaginate: Boolean = false, showEmojiButton: Boolean = false, onBack: () -> Unit = {}, onClose: () -> Unit = {}, + onScrolledToIndex: () -> Unit = {}, onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> }, onRefresh: () -> Unit = {}, @@ -167,7 +213,9 @@ fun MessagesHistoryScreen( onActionButtonClicked: () -> Unit = {}, onEmojiButtonLongClicked: () -> Unit = {}, onMessageClicked: (Int) -> Unit = {}, - onMessageLongClicked: (Int) -> Unit = {} + onMessageLongClicked: (Int) -> Unit = {}, + onPinnedMessageClicked: (Int) -> Unit = {}, + onUnpinMessageButtonClicked: () -> Unit = {} ) { val view = LocalView.current val coroutineScope = rememberCoroutineScope() @@ -175,6 +223,15 @@ fun MessagesHistoryScreen( val listState = rememberLazyListState() val hazeState = remember { HazeState() } + LaunchedEffect(scrollIndex) { + if (scrollIndex != null) { + coroutineScope.launch { + listState.animateScrollToItem(scrollIndex) + onScrolledToIndex() + } + } + } + BackHandler( enabled = selectedMessages.isNotEmpty(), onBack = onClose @@ -440,14 +497,14 @@ fun MessagesHistoryScreen( modifier = Modifier .fillMaxWidth() .height(56.dp) - .clickable { - - } + .clickable { onPinnedMessageClicked(pinnedMessage!!.id) } .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( - modifier = Modifier.rotate(45f), + modifier = Modifier + .rotate(45f) + .alpha(0.5f), painter = painterResource(UiR.drawable.ic_round_push_pin_24), contentDescription = null ) @@ -468,6 +525,18 @@ fun MessagesHistoryScreen( } } } + + if (screenState.conversation.canChangePin) { + Spacer(modifier = Modifier.width(16.dp)) + + IconButton(onClick = onUnpinMessageButtonClicked) { + Icon( + modifier = Modifier.alpha(0.5f), + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + } } HorizontalDivider() } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index 6134fd3d..41fceda5 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -92,6 +92,12 @@ fun MessagesList( when (item) { is UiItem.ActionMessage -> { ActionMessageItem( + modifier = Modifier.then( + if (enableAnimations) Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ) else Modifier + ), item = item, onClick = { if (item.actionCmId != null) { @@ -112,6 +118,12 @@ fun MessagesList( Surface( modifier = Modifier + .then( + if (enableAnimations) Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ) else Modifier + ) .combinedClickable( onLongClick = { if (AppSettings.General.enableHaptic) { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt index f25a1bdb..4c65b28a 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.messageshistory.presentation +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -19,14 +20,14 @@ fun OutgoingMessageBubble( animate: Boolean ) { Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().then(if (animate) Modifier.animateContentSize() else Modifier), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { Column( modifier = Modifier .padding(end = 16.dp) - .fillMaxWidth(0.75f), + .fillMaxWidth(0.85f), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.End, ) { @@ -38,7 +39,8 @@ fun OutgoingMessageBubble( edited = message.isEdited, animate = animate, isRead = message.isRead, - sendingStatus = message.sendingStatus + sendingStatus = message.sendingStatus, + pinned = message.isPinned ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt index 94c8ac6f..966901d5 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt @@ -128,7 +128,9 @@ fun VkMessage.asPresentation( sendingStatus = when { id <= 0 -> SendingStatus.SENDING else -> SendingStatus.SENT - } + }, + isSelected = false, + isPinned = isPinned ) }