From fb76b46b2202507fb31a631404799ad31b06e050 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Fri, 12 Jul 2024 00:51:24 +0300 Subject: [PATCH] [wip] chat materials; some experiments with local composition and blur --- .../kotlin/com/meloda/app/fast/MainGraph.kt | 119 +++--- .../app/fast/common/di/ApplicationModule.kt | 4 +- .../app/fast/data/LongPollUpdatesParser.kt | 32 +- .../data/api/messages/MessagesHistoryInfo.kt | 9 + .../api/messages/MessagesLocalDataSource.kt | 16 - .../messages/MessagesLocalDataSourceImpl.kt | 24 -- .../api/messages/MessagesNetworkDataSource.kt | 36 -- .../messages/MessagesNetworkDataSourceImpl.kt | 164 -------- .../data/api/messages/MessagesRepository.kt | 22 +- .../api/messages/MessagesRepositoryImpl.kt | 231 ++++++++---- .../fast/data/api/messages/MessagesUseCase.kt | 17 +- .../com/meloda/app/fast/data/di/DataModule.kt | 6 - .../meloda/app/fast/database/CacheDatabase.kt | 2 +- .../meloda/app/fast/designsystem/AppTheme.kt | 10 + .../data/VkAttachmentHistoryMessageData.kt | 25 ++ .../model/api/data/VkAttachmentItemData.kt | 33 +- .../app/fast/model/api/data/VkMessageData.kt | 128 +------ .../model/api/data/VkPinnedMessageData.kt | 1 + .../api/domain/VkAttachmentHistoryMessage.kt | 10 + .../app/fast/model/api/domain/VkMessage.kt | 2 + .../model/api/domain/VkUnknownAttachment.kt | 7 + .../model/api/requests/MessagesRequest.kt | 24 ++ .../model/api/responses/MessagesResponse.kt | 11 + .../fast/model/database/VkMessageEntity.kt | 2 + .../service/messages/MessagesService.kt | 7 + .../network/service/messages/MessagesUrls.kt | 1 + feature/chatmaterials/build.gradle.kts | 1 + .../ChatMaterialsScreenContent.kt | 91 ----- .../chatmaterials/ChatMaterialsViewModel.kt | 130 +++++++ .../chatmaterials/di/ChatMaterialsModule.kt | 9 + .../model/ChatMaterialsScreenState.kt | 24 ++ .../chatmaterials/model/UiChatMaterial.kt | 28 ++ .../navigation/ChatMaterialsRoute.kt | 23 +- .../presentation/ChatMaterialItem.kt | 68 ++++ .../presentation/ChatMaterialsScreen.kt | 352 ++++++++++++++++++ .../chatmaterials/util/ChatMaterialMapper.kt | 59 +++ .../presentation/ConversationsList.kt | 7 + .../presentation/ConversationsScreen.kt | 64 ++-- .../app/fast/friends/FriendsViewModel.kt | 10 +- .../MessagesHistoryViewModel.kt | 1 + .../domain/MessagesUseCaseImpl.kt | 99 ++--- .../fast/messageshistory/model/UiMessage.kt | 1 + .../navigation/MessagesHistoryRoute.kt | 2 +- .../presentation/MessagesHistoryScreen.kt | 9 +- .../messageshistory/util/MessageMapper.kt | 1 + gradle/libs.versions.toml | 5 +- 46 files changed, 1210 insertions(+), 717 deletions(-) create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesHistoryInfo.kt delete mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt delete mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt delete mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt delete mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentHistoryMessageData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAttachmentHistoryMessage.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkUnknownAttachment.kt delete mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsScreenContent.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsViewModel.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/di/ChatMaterialsModule.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/ChatMaterialsScreenState.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/UiChatMaterial.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialItem.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/util/ChatMaterialMapper.kt diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt index 8253d8b1..c5f47284 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.slideOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Settings @@ -16,19 +17,23 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -36,9 +41,16 @@ import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import com.meloda.app.fast.conversations.navigation.Conversations import com.meloda.app.fast.conversations.navigation.conversationsRoute +import com.meloda.app.fast.designsystem.LocalBottomPadding +import com.meloda.app.fast.designsystem.LocalHazeState +import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.friends.navigation.Friends import com.meloda.app.fast.friends.navigation.friendsRoute import com.meloda.app.fast.model.BaseError +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials import kotlinx.serialization.Serializable import com.meloda.app.fast.designsystem.R as UiR @@ -58,7 +70,7 @@ data class BottomNavigationItem( val route: Any, ) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) fun NavGraphBuilder.mainScreen( onError: (BaseError) -> Unit, onNavigateToSettings: () -> Unit, @@ -87,6 +99,8 @@ fun NavGraphBuilder.mainScreen( val routes = items.map(BottomNavigationItem::route) composable
{ + val currentTheme = LocalTheme.current + val hazeState = remember { HazeState() } val navController = rememberNavController() var selectedItemIndex by rememberSaveable { @@ -104,7 +118,21 @@ fun NavGraphBuilder.mainScreen( enter = slideIn { IntOffset(0, 400) }, exit = slideOut { IntOffset(0, 400) } ) { - NavigationBar { + NavigationBar( + modifier = Modifier + .then( + if (currentTheme.usingBlur) { + Modifier.hazeChild( + state = hazeState, + style = HazeMaterials.thick() + ) + } else Modifier + ) + .fillMaxWidth(), + containerColor = NavigationBarDefaults.containerColor.copy( + alpha = if (currentTheme.usingBlur) 0f else 1f + ) + ) { items.forEachIndexed { index, item -> NavigationBarItem( selected = selectedItemIndex == index, @@ -139,52 +167,57 @@ fun NavGraphBuilder.mainScreen( Box( modifier = Modifier .fillMaxSize() - .padding(bottom = padding.calculateBottomPadding()) + .padding(bottom = if (currentTheme.usingBlur) 0.dp else padding.calculateBottomPadding()) ) { - NavHost( - navController = navController, - startDestination = MainGraph, - enterTransition = { fadeIn(animationSpec = tween(200)) }, - exitTransition = { fadeOut(animationSpec = tween(200)) } + CompositionLocalProvider( + LocalHazeState provides hazeState, + LocalBottomPadding provides if (currentTheme.usingBlur) padding.calculateBottomPadding() else 0.dp ) { - navigation(startDestination = Conversations) { - friendsRoute( - onError = onError, - navController = navController - ) - conversationsRoute( - onError = onError, - onNavigateToMessagesHistory = onNavigateToMessagesHistory, - navController = navController, - onListScrollingUp = { isScrolling -> + NavHost( + navController = navController, + startDestination = MainGraph, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + navigation(startDestination = Conversations) { + friendsRoute( + onError = onError, + navController = navController + ) + conversationsRoute( + onError = onError, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, + navController = navController, + onListScrollingUp = { isScrolling -> // isBottomBarVisible = isScrolling - } - ) - - composable { - Scaffold( - topBar = { - TopAppBar( - title = { - Text(text = stringResource(id = UiR.string.title_profile)) - }, - actions = { - IconButton(onClick = onNavigateToSettings) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) - } - } - ) } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { + ) + composable { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = stringResource(id = UiR.string.title_profile)) + }, + actions = { + IconButton(onClick = onNavigateToSettings) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null + ) + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + + } } } } diff --git a/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt index 411accb4..7751dd78 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt @@ -6,6 +6,7 @@ import android.os.PowerManager import androidx.preference.PreferenceManager import com.meloda.app.fast.MainViewModelImpl import com.meloda.app.fast.auth.authModule +import com.meloda.app.fast.chatmaterials.di.chatMaterialsModule import com.meloda.app.fast.conversations.di.conversationsModule import com.meloda.app.fast.data.di.dataModule import com.meloda.app.fast.friends.di.friendsModule @@ -32,7 +33,8 @@ val applicationModule = module { languagePickerModule, longPollModule, friendsModule, - profileModule + profileModule, + chatMaterialsModule ) // TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt index 59c7638f..fe21602a 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt @@ -231,13 +231,13 @@ class LongPollUpdatesParser( Log.d("LongPollUpdatesParser", "$eventType: $event") } - private suspend fun loadNormalMessage( + private suspend inline fun loadNormalMessage( eventType: ApiEvent, messageId: Int - ): T? = suspendCoroutine { + ): T? = suspendCoroutine { continuation -> coroutineScope.launch(Dispatchers.IO) { messagesUseCase.getById( - messageId = messageId, + messageIds = listOf(messageId), extended = true, fields = VkConstants.ALL_FIELDS ).listenValue(this) { state -> @@ -245,20 +245,26 @@ class LongPollUpdatesParser( error = { error -> Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error") }, - success = { response -> - response?.let { message -> - VkMemoryCache[message.id] = message - messagesUseCase.storeMessage(message) + success = { messages -> + val message = messages.singleOrNull() ?: run { + continuation.resume(null) + return@listenValue + } - val resumeValue: LongPollEvent? = when (eventType) { - ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message) - ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message) + VkMemoryCache[message.id] = message + messagesUseCase.storeMessage(message) - else -> null + val resumeValue: LongPollEvent? = when (eventType) { + ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message) + ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message) + + else -> { + continuation.resume(null) + null } + } - resumeValue?.let { value -> it.resume(value as T) } - } ?: it.resume(null) + resumeValue?.let { value -> continuation.resume(value as T) } } ) } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesHistoryInfo.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesHistoryInfo.kt new file mode 100644 index 00000000..da247855 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesHistoryInfo.kt @@ -0,0 +1,9 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkMessage + +data class MessagesHistoryInfo( + val messages: List, + val conversations: List +) diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt deleted file mode 100644 index 9d3cd7ff..00000000 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.meloda.app.fast.data.api.messages - -import com.meloda.app.fast.model.database.VkMessageEntity - -interface MessagesLocalDataSource { - - suspend fun getMessages( - conversationId: Int, - offset: Int?, - count: Int? - ): List - - suspend fun getMessage(messageId: Int): VkMessageEntity? - - suspend fun storeMessages(messages: List) -} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt deleted file mode 100644 index c60632f0..00000000 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.meloda.app.fast.data.api.messages - -import com.meloda.app.fast.database.dao.MessageDao -import com.meloda.app.fast.model.database.VkMessageEntity - -// TODO: 05/05/2024, Danil Nikolaev: use paging for room -class MessagesLocalDataSourceImpl( - private val messageDao: MessageDao -) : MessagesLocalDataSource { - - override suspend fun getMessages( - conversationId: Int, - offset: Int?, - count: Int? - ): List = messageDao.getAll(conversationId) - - override suspend fun getMessage( - messageId: Int - ): VkMessageEntity? = messageDao.getById(messageId) - - override suspend fun storeMessages(messages: List) { - messageDao.insertAll(messages) - } -} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt deleted file mode 100644 index 2ff8faa4..00000000 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.app.fast.data.api.messages - -import com.meloda.app.fast.model.api.domain.VkAttachment -import com.meloda.app.fast.model.api.domain.VkMessage -import com.meloda.app.fast.network.RestApiErrorDomain -import com.slack.eithernet.ApiResult - -interface MessagesNetworkDataSource { - - suspend fun getMessagesHistory( - conversationId: Int, - offset: Int?, - count: Int?, - ): ApiResult - - suspend fun getMessageById( - messagesIds: List, - extended: Boolean?, - fields: String? - ): ApiResult - - suspend fun send( - peerId: Int, - randomId: Int, - message: String?, - replyTo: Int?, - attachments: List? - ): ApiResult - - suspend fun markAsRead( - peerId: Int, - startMessageId: Int? - ): ApiResult - - suspend fun getMessage(messageId: Int): VkMessage? -} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt deleted file mode 100644 index 2f009484..00000000 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.meloda.app.fast.data.api.messages - -import com.meloda.app.fast.common.VkConstants -import com.meloda.app.fast.data.VkGroupsMap -import com.meloda.app.fast.data.VkMemoryCache -import com.meloda.app.fast.data.VkUsersMap -import com.meloda.app.fast.model.api.data.VkContactData -import com.meloda.app.fast.model.api.data.VkGroupData -import com.meloda.app.fast.model.api.data.VkUserData -import com.meloda.app.fast.model.api.data.asDomain -import com.meloda.app.fast.model.api.domain.VkAttachment -import com.meloda.app.fast.model.api.domain.VkConversation -import com.meloda.app.fast.model.api.domain.VkMessage -import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest -import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest -import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest -import com.meloda.app.fast.model.api.requests.MessagesSendRequest -import com.meloda.app.fast.network.RestApiErrorDomain -import com.meloda.app.fast.network.mapApiDefault -import com.meloda.app.fast.network.mapApiResult -import com.meloda.app.fast.network.service.messages.MessagesService -import com.slack.eithernet.ApiResult -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class MessagesNetworkDataSourceImpl( - private val messagesService: MessagesService -) : MessagesNetworkDataSource { - - override suspend fun getMessagesHistory( - conversationId: Int, - offset: Int?, - count: Int? - ): ApiResult = withContext(Dispatchers.IO) { - val requestModel = MessagesGetHistoryRequest( - count = count, - offset = offset, - peerId = conversationId, - extended = true, - startMessageId = null, - rev = null, - fields = VkConstants.ALL_FIELDS - ) - - messagesService.getHistory(requestModel.map).mapApiResult( - successMapper = { apiResponse -> - val response = apiResponse.requireResponse() - - val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) - val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) - val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) - - val usersMap = VkUsersMap.forUsers(profilesList) - val groupsMap = VkGroupsMap.forGroups(groupsList) - - VkMemoryCache.appendUsers(profilesList) - VkMemoryCache.appendGroups(groupsList) - VkMemoryCache.appendContacts(contactsList) - - val messages = response.items.map { item -> - item.asDomain().let { message -> - message.copy( - user = usersMap.messageUser(message), - group = groupsMap.messageGroup(message), - actionUser = usersMap.messageActionUser(message), - actionGroup = groupsMap.messageActionGroup(message) - ).also { VkMemoryCache[message.id] = it } - } - } - - val conversations = response.conversations.orEmpty().map { item -> - val message = messages.firstOrNull { it.id == item.lastMessageId } - item.asDomain(message) - .let { conversation -> - conversation.copy( - user = usersMap.conversationUser(conversation), - group = groupsMap.conversationGroup(conversation) - ).also { VkMemoryCache[conversation.id] = it } - } - } - - MessagesHistoryDomain( - messages = messages, - conversations = conversations - ) - }, - errorMapper = { error -> - error?.toDomain() - } - ) - } - - override suspend fun getMessageById( - messagesIds: List, - extended: Boolean?, - fields: String? - ): ApiResult = withContext(Dispatchers.IO) { - val requestModel = MessagesGetByIdRequest( - messagesIds = messagesIds, - extended = extended, - fields = fields - ) - - messagesService.getById(requestModel.map).mapApiResult( - successMapper = { apiResponse -> - val response = apiResponse.requireResponse() - - val message = response.items.single() - val usersMap = - VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain)) - val groupsMap = - VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain)) - - message.asDomain().copy( - user = usersMap.messageUser(message), - group = groupsMap.messageGroup(message), - actionUser = usersMap.messageActionUser(message), - actionGroup = groupsMap.messageActionGroup(message) - ) - }, - errorMapper = { error -> error?.toDomain() } - ) - } - - override suspend fun send( - peerId: Int, - randomId: Int, - message: String?, - replyTo: Int?, - attachments: List? - ): ApiResult = withContext(Dispatchers.IO) { - val requestModel = MessagesSendRequest( - peerId = peerId, - randomId = randomId, - message = message, - replyTo = replyTo, - attachments = attachments - ) - - messagesService.send(requestModel.map).mapApiDefault() - } - - override suspend fun markAsRead( - peerId: Int, - startMessageId: Int? - ): ApiResult = withContext(Dispatchers.IO) { - val requestModel = MessagesMarkAsReadRequest( - peerId = peerId, - startMessageId = startMessageId - ) - - messagesService.markAsRead(requestModel.map).mapApiDefault() - } - - override suspend fun getMessage(messageId: Int): VkMessage? = withContext(Dispatchers.IO) { - // TODO: 05/05/2024, Danil Nikolaev: get message - null - } -} - -data class MessagesHistoryDomain( - val messages: List, - val conversations: List -) diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt index 71b421f8..ef5c5f99 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt @@ -1,24 +1,24 @@ package com.meloda.app.fast.data.api.messages import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage import com.meloda.app.fast.model.api.domain.VkMessage import com.meloda.app.fast.network.RestApiErrorDomain import com.slack.eithernet.ApiResult -import kotlinx.coroutines.flow.Flow interface MessagesRepository { - suspend fun getMessagesHistory( + suspend fun getHistory( conversationId: Int, offset: Int?, count: Int? - ): ApiResult + ): ApiResult - suspend fun getMessageById( + suspend fun getById( messagesIds: List, extended: Boolean?, fields: String? - ): ApiResult + ): ApiResult, RestApiErrorDomain> suspend fun send( peerId: Int, @@ -33,14 +33,16 @@ interface MessagesRepository { startMessageId: Int? ): ApiResult - suspend fun getMessage(messageId: Int): Flow + suspend fun getHistoryAttachments( + peerId: Int, + count: Int?, + offset: Int?, + attachmentTypes: List, + conversationMessageId: Int + ): ApiResult, RestApiErrorDomain> suspend fun storeMessages(messages: List) -// suspend fun getHistory( -// params: MessagesGetHistoryRequest -// ): ApiResult - // suspend fun markAsImportant( // params: MessagesMarkAsImportantRequest // ): ApiResult, RestApiErrorDomain> diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt index 4eb00f8d..86cebb3e 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -1,56 +1,132 @@ package com.meloda.app.fast.data.api.messages +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkGroupsMap +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.data.VkUsersMap +import com.meloda.app.fast.database.dao.MessageDao +import com.meloda.app.fast.model.api.data.VkAttachmentHistoryMessageData +import com.meloda.app.fast.model.api.data.VkContactData +import com.meloda.app.fast.model.api.data.VkGroupData +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.data.asDomain import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage import com.meloda.app.fast.model.api.domain.VkMessage import com.meloda.app.fast.model.api.domain.asEntity -import com.meloda.app.fast.model.database.asExternalModel +import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest +import com.meloda.app.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest +import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest +import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest +import com.meloda.app.fast.model.api.requests.MessagesSendRequest import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.messages.MessagesService import com.slack.eithernet.ApiResult import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext -// TODO: 05/05/2024, Danil Nikolaev: implement syncing class MessagesRepositoryImpl( - private val networkDataSource: MessagesNetworkDataSource, - private val localDataSource: MessagesLocalDataSource + private val messagesService: MessagesService, + private val messageDao: MessageDao, ) : MessagesRepository { - override suspend fun getMessagesHistory( + override suspend fun getHistory( conversationId: Int, offset: Int?, count: Int? - ): ApiResult = withContext(Dispatchers.IO) { -// val localMessages = localDataSource.getMessages( -// conversationId = conversationId, -// offset = offset, -// count = count -// ).map(VkMessageEntity::asExternalModel) -// -// emit(localMessages) -// -// val networkMessages = networkDataSource.getMessagesHistory( -// conversationId = conversationId, -// offset = offset, -// count = count -// ) -// -// emit(networkMessages) + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetHistoryRequest( + count = count, + offset = offset, + peerId = conversationId, + extended = true, + startMessageId = null, + rev = null, + fields = VkConstants.ALL_FIELDS + ) - networkDataSource.getMessagesHistory(conversationId, offset, count) + messagesService.getHistory(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) + + val usersMap = VkUsersMap.forUsers(profilesList) + val groupsMap = VkGroupsMap.forGroups(groupsList) + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + val messages = response.items.map { item -> + item.asDomain().let { message -> + message.copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ).also { VkMemoryCache[message.id] = it } + } + } + + val conversations = response.conversations.orEmpty().map { item -> + val message = messages.firstOrNull { it.id == item.lastMessageId } + item.asDomain(message) + .let { conversation -> + conversation.copy( + user = usersMap.conversationUser(conversation), + group = groupsMap.conversationGroup(conversation) + ).also { VkMemoryCache[conversation.id] = it } + } + } + + MessagesHistoryInfo( + messages = messages, + conversations = conversations + ) + }, + errorMapper = { error -> + error?.toDomain() + } + ) } - override suspend fun getMessageById( + override suspend fun getById( messagesIds: List, extended: Boolean?, fields: String? - ): ApiResult = withContext(Dispatchers.IO) { - networkDataSource.getMessageById( + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = MessagesGetByIdRequest( messagesIds = messagesIds, extended = extended, fields = fields ) + + messagesService.getById(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val messages = response.items + val usersMap = + VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain)) + val groupsMap = + VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain)) + + messages.map { message -> + message.asDomain().copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ) + } + }, + errorMapper = { error -> error?.toDomain() } + ) } override suspend fun send( @@ -60,54 +136,72 @@ class MessagesRepositoryImpl( replyTo: Int?, attachments: List? ): ApiResult = withContext(Dispatchers.IO) { - networkDataSource.send( - peerId, - randomId, - message, - replyTo, - attachments + val requestModel = MessagesSendRequest( + peerId = peerId, + randomId = randomId, + message = message, + replyTo = replyTo, + attachments = attachments ) + + messagesService.send(requestModel.map).mapApiDefault() } override suspend fun markAsRead( peerId: Int, startMessageId: Int? ): ApiResult = withContext(Dispatchers.IO) { - networkDataSource.markAsRead(peerId, startMessageId) + val requestModel = MessagesMarkAsReadRequest( + peerId = peerId, + startMessageId = startMessageId + ) + + messagesService.markAsRead(requestModel.map).mapApiDefault() } - override suspend fun getMessage(messageId: Int): Flow = flow { - val localMessage = localDataSource.getMessage(messageId)?.asExternalModel() + override suspend fun getHistoryAttachments( + peerId: Int, + count: Int?, + offset: Int?, + attachmentTypes: List, + conversationMessageId: Int + ): ApiResult, RestApiErrorDomain> = + withContext(Dispatchers.IO) { + val requestModel = MessagesGetHistoryAttachmentsRequest( + peerId = peerId, + extended = true, + count = count, + offset = offset, + preserveOrder = true, + attachmentTypes = attachmentTypes, + conversationMessageId = conversationMessageId, + fields = VkConstants.ALL_FIELDS + ) - emit(localMessage) + messagesService.getHistoryAttachments(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() - val networkMessage = networkDataSource.getMessage(messageId) + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) - emit(networkMessage) - } + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + response.items.map(VkAttachmentHistoryMessageData::toDomain) + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } override suspend fun storeMessages(messages: List) { - localDataSource.storeMessages(messages.map(VkMessage::asEntity)) + messageDao.insertAll(messages.map(VkMessage::asEntity)) } - // override suspend fun getHistory( -// params: MessagesGetHistoryRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.getHistory(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun send( -// params: MessagesSendRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.send(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// // override suspend fun markAsImportant( // params: MessagesMarkAsImportantRequest // ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { @@ -153,24 +247,6 @@ class MessagesRepositoryImpl( // ) // } // -// override suspend fun getById( -// params: MessagesGetByIdRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.getById(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun markAsRead( -// params: MessagesMarkAsReadRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.markAsRead(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// // override suspend fun getChat( // params: MessagesGetChatRequest // ): ApiResult = withContext(Dispatchers.IO) { @@ -199,3 +275,4 @@ class MessagesRepositoryImpl( // ) // } } + diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt index 14435ebc..48924102 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt @@ -2,6 +2,7 @@ package com.meloda.app.fast.data.api.messages import com.meloda.app.fast.data.State import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage import com.meloda.app.fast.model.api.domain.VkMessage import kotlinx.coroutines.flow.Flow @@ -11,15 +12,9 @@ interface MessagesUseCase { conversationId: Int, count: Int?, offset: Int? - ): Flow> + ): Flow> fun getById( - messageId: Int, - extended: Boolean?, - fields: String? - ): Flow> - - fun getByIds( messageIds: List, extended: Boolean?, fields: String? @@ -38,6 +33,14 @@ interface MessagesUseCase { startMessageId: Int ): Flow> + fun getHistoryAttachments( + peerId: Int, + count: Int?, + offset: Int?, + attachmentTypes: List, + conversationMessageId: Int + ): Flow>> + suspend fun storeMessage(message: VkMessage) suspend fun storeMessages(messages: List) } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt index 76d1c4d1..5db78378 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt @@ -15,10 +15,6 @@ import com.meloda.app.fast.data.api.friends.FriendsRepository import com.meloda.app.fast.data.api.friends.FriendsRepositoryImpl import com.meloda.app.fast.data.api.longpoll.LongPollRepository import com.meloda.app.fast.data.api.longpoll.LongPollRepositoryImpl -import com.meloda.app.fast.data.api.messages.MessagesLocalDataSource -import com.meloda.app.fast.data.api.messages.MessagesLocalDataSourceImpl -import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSource -import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSourceImpl import com.meloda.app.fast.data.api.messages.MessagesRepository import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl import com.meloda.app.fast.data.api.oauth.OAuthRepository @@ -59,8 +55,6 @@ val dataModule = module { singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class - singleOf(::MessagesLocalDataSourceImpl) bind MessagesLocalDataSource::class - singleOf(::MessagesNetworkDataSourceImpl) bind MessagesNetworkDataSource::class singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt index 35521021..842f835f 100644 --- a/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt @@ -21,7 +21,7 @@ import com.meloda.app.fast.model.database.VkUserEntity VkConversationEntity::class ], - version = 5 + version = 6 ) @TypeConverters(Converters::class) abstract class CacheDatabase : RoomDatabase() { diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt index 7ec28bcc..a45208f5 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt @@ -18,12 +18,14 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import com.meloda.app.fast.datastore.isUsingAmoledBackground import com.meloda.app.fast.datastore.isUsingDynamicColors import com.meloda.app.fast.datastore.model.ThemeConfig import com.meloda.app.fast.datastore.selectedColorScheme import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme +import dev.chrisbanes.haze.HazeState private val googleSansFonts = FontFamily( Font(resId = R.font.google_sans_regular), @@ -115,6 +117,14 @@ val LocalTheme = compositionLocalOf { ) } +val LocalHazeState = compositionLocalOf { + HazeState() +} + +val LocalBottomPadding = compositionLocalOf { + 0.dp +} + @Composable fun AppTheme( predefinedColorScheme: ColorScheme? = null, diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentHistoryMessageData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentHistoryMessageData.kt new file mode 100644 index 00000000..016dd5eb --- /dev/null +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentHistoryMessageData.kt @@ -0,0 +1,25 @@ +package com.meloda.app.fast.model.api.data + +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class VkAttachmentHistoryMessageData( + @Json(name = "message_id") val messageId: Int, + @Json(name = "date") val date: Int, + @Json(name = "cmid") val conversationMessageId: Int, + @Json(name = "from_id") val fromId: Int, + @Json(name = "position") val position: Int, + @Json(name = "attachment") val attachment: VkAttachmentItemData +) { + + fun toDomain(): VkAttachmentHistoryMessage = VkAttachmentHistoryMessage( + messageId = messageId, + conversationMessageId = conversationMessageId, + date = date, + fromId = fromId, + position = position, + attachment = attachment.toDomain() + ) +} diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt index 561b60b7..78cfc152 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt @@ -1,5 +1,7 @@ package com.meloda.app.fast.model.api.data +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkUnknownAttachment import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -12,7 +14,7 @@ data class VkAttachmentItemData( @Json(name = "doc") val file: VkFileData?, @Json(name = "link") val link: VkLinkData?, @Json(name = "mini_app") val miniApp: VkMiniAppData?, - @Json(name = "audio_message") val voiceMessage: VkAudioMessageData?, + @Json(name = "audio_message") val audioMessage: VkAudioMessageData?, @Json(name = "sticker") val sticker: VkStickerData?, @Json(name = "gift") val gift: VkGiftData?, @Json(name = "wall") val wall: VkWallData?, @@ -20,7 +22,7 @@ data class VkAttachmentItemData( @Json(name = "poll") val poll: VkPollData?, @Json(name = "wall_reply") val wallReply: VkWallReplyData?, @Json(name = "call") val call: VkCallData?, - @Json(name = "group_call_in_progress") val groupCall: VkGroupCallData?, + @Json(name = "group_call_in_progress") val groupCallInProgress: VkGroupCallData?, @Json(name = "curator") val curator: VkCuratorData?, @Json(name = "event") val event: VkEventData?, @Json(name = "story") val story: VkStoryData?, @@ -30,6 +32,29 @@ data class VkAttachmentItemData( @Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?, @Json(name = "podcast") val podcast: VkPodcastData? ) { - - fun getPreparedType(): AttachmentType = AttachmentType.parse(type) + fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) { + AttachmentType.UNKNOWN -> VkUnknownAttachment + AttachmentType.PHOTO -> photo?.toDomain() + AttachmentType.VIDEO -> video?.toDomain() + AttachmentType.AUDIO -> audio?.toDomain() + AttachmentType.FILE -> file?.toDomain() + AttachmentType.LINK -> link?.toDomain() + AttachmentType.MINI_APP -> miniApp?.toDomain() + AttachmentType.AUDIO_MESSAGE -> audioMessage?.toDomain() + AttachmentType.STICKER -> sticker?.toDomain() + AttachmentType.GIFT -> gift?.toDomain() + AttachmentType.WALL -> wall?.toDomain() + AttachmentType.GRAFFITI -> graffiti?.toDomain() + AttachmentType.POLL -> poll?.toDomain() + AttachmentType.WALL_REPLY -> wallReply?.toDomain() + AttachmentType.CALL -> call?.toDomain() + AttachmentType.GROUP_CALL_IN_PROGRESS -> groupCallInProgress?.toDomain() + AttachmentType.CURATOR -> curator?.toDomain() + AttachmentType.EVENT -> event?.toDomain() + AttachmentType.STORY -> story?.toDomain() + AttachmentType.WIDGET -> widget?.toDomain() + AttachmentType.ARTIST -> artist?.toDomain() + AttachmentType.AUDIO_PLAYLIST -> audioPlaylist?.toDomain() + AttachmentType.PODCAST -> podcast?.toDomain() + } ?: VkUnknownAttachment } diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkMessageData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkMessageData.kt index d7e75a02..479e87be 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkMessageData.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkMessageData.kt @@ -1,6 +1,5 @@ package com.meloda.app.fast.model.api.data -import com.meloda.app.fast.model.api.domain.VkAttachment import com.meloda.app.fast.model.api.domain.VkMessage import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -60,6 +59,7 @@ data class VkMessageData( fun VkMessageData.asDomain(): VkMessage = VkMessage( id = id ?: -1, + conversationMessageId = conversationMessageId, text = text.ifBlank { null }, isOut = out == 1, peerId = peerId ?: -1, @@ -75,134 +75,10 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage( important = important, updateTime = updateTime, forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain), - attachments = parseAttachments(), + attachments = attachments.map(VkAttachmentItemData::toDomain), replyMessage = replyMessage?.asDomain(), user = null, group = null, actionUser = null, actionGroup = null, ) - -private fun VkMessageData.parseAttachments(): List { - if (attachments.isEmpty()) return emptyList() - - val attachments = mutableListOf() - - for (baseAttachment in this.attachments) { - when (baseAttachment.getPreparedType()) { - AttachmentType.UNKNOWN -> continue - AttachmentType.PHOTO -> { - val photo = baseAttachment.photo ?: continue - attachments += photo.toDomain() - } - - AttachmentType.VIDEO -> { - val video = baseAttachment.video ?: continue - attachments += video.toDomain() - } - - AttachmentType.AUDIO -> { - val audio = baseAttachment.audio ?: continue - attachments += audio.toDomain() - } - - AttachmentType.FILE -> { - val file = baseAttachment.file ?: continue - attachments += file.toDomain() - } - - AttachmentType.LINK -> { - val link = baseAttachment.link ?: continue - attachments += link.toDomain() - } - - AttachmentType.MINI_APP -> { - val miniApp = baseAttachment.miniApp ?: continue - attachments += miniApp.toDomain() - } - - AttachmentType.AUDIO_MESSAGE -> { - val voiceMessage = baseAttachment.voiceMessage ?: continue - attachments += voiceMessage.toDomain() - } - - AttachmentType.STICKER -> { - val sticker = baseAttachment.sticker ?: continue - attachments += sticker.toDomain() - } - - AttachmentType.GIFT -> { - val gift = baseAttachment.gift ?: continue - attachments += gift.toDomain() - } - - AttachmentType.WALL -> { - val wall = baseAttachment.wall ?: continue - attachments += wall.toDomain() - } - - AttachmentType.GRAFFITI -> { - val graffiti = baseAttachment.graffiti ?: continue - attachments += graffiti.toDomain() - } - - AttachmentType.POLL -> { - val poll = baseAttachment.poll ?: continue - attachments += poll.toDomain() - } - - AttachmentType.WALL_REPLY -> { - val wallReply = baseAttachment.wallReply ?: continue - attachments += wallReply.toDomain() - } - - AttachmentType.CALL -> { - val call = baseAttachment.call ?: continue - attachments += call.toDomain() - } - - AttachmentType.GROUP_CALL_IN_PROGRESS -> { - val groupCall = baseAttachment.groupCall ?: continue - attachments += groupCall.toDomain() - } - - AttachmentType.CURATOR -> { - val curator = baseAttachment.curator ?: continue - attachments += curator.toDomain() - } - - AttachmentType.EVENT -> { - val event = baseAttachment.event ?: continue - attachments += event.toDomain() - } - - AttachmentType.STORY -> { - val story = baseAttachment.story ?: continue - attachments += story.toDomain() - } - - AttachmentType.WIDGET -> { - val widget = baseAttachment.widget ?: continue - attachments += widget.toDomain() - } - - AttachmentType.ARTIST -> { - val artist = baseAttachment.artist ?: continue - attachments += artist.toDomain() - val audios = baseAttachment.audios ?: continue - audios.map(VkAudioData::toDomain).let(attachments::addAll) - } - - AttachmentType.AUDIO_PLAYLIST -> { - val audioPlaylist = baseAttachment.audioPlaylist ?: continue - attachments += audioPlaylist.toDomain() - } - - AttachmentType.PODCAST -> { - val podcast = baseAttachment.podcast ?: continue - attachments += podcast.toDomain() - } - } - } - return attachments -} diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPinnedMessageData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPinnedMessageData.kt index c1e62a10..94537848 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPinnedMessageData.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPinnedMessageData.kt @@ -28,6 +28,7 @@ data class VkPinnedMessageData( fun mapToDomain(): VkMessage = VkMessage( id = id ?: -1, + conversationMessageId = conversationMessageId, text = text.ifBlank { null }, isOut = out == true, peerId = peerId ?: -1, diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAttachmentHistoryMessage.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAttachmentHistoryMessage.kt new file mode 100644 index 00000000..66bac811 --- /dev/null +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAttachmentHistoryMessage.kt @@ -0,0 +1,10 @@ +package com.meloda.app.fast.model.api.domain + +data class VkAttachmentHistoryMessage( + val messageId: Int, + val conversationMessageId: Int, + val date: Int, + val fromId: Int, + val position: Int, + val attachment: VkAttachment +) diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkMessage.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkMessage.kt index 2048a95f..cacd2982 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkMessage.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkMessage.kt @@ -4,6 +4,7 @@ import com.meloda.app.fast.model.database.VkMessageEntity data class VkMessage( val id: Int, + val conversationMessageId: Int, val text: String?, val isOut: Boolean, val peerId: Int, @@ -78,6 +79,7 @@ data class VkMessage( fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity( id = id, + conversationMessageId = conversationMessageId, text = text, isOut = isOut, peerId = peerId, diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkUnknownAttachment.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkUnknownAttachment.kt new file mode 100644 index 00000000..bc507d5a --- /dev/null +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkUnknownAttachment.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.model.api.domain + +import com.meloda.app.fast.model.api.data.AttachmentType + +data object VkUnknownAttachment : VkAttachment { + override val type: AttachmentType = AttachmentType.UNKNOWN +} diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/MessagesRequest.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/MessagesRequest.kt index eb41cf3f..8ca4b34b 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/MessagesRequest.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/MessagesRequest.kt @@ -243,3 +243,27 @@ data class MessagesRemoveChatUserRequest( "member_id" to memberId.toString() ) } + +data class MessagesGetHistoryAttachmentsRequest( + val peerId: Int, + val extended: Boolean?, + val count: Int?, + val offset: Int?, + val preserveOrder: Boolean?, + val attachmentTypes: List, + val conversationMessageId: Int, + val fields: String? +) { + + val map = mutableMapOf( + "peer_id" to peerId.toString(), + "attachment_types" to attachmentTypes.joinToString(","), + "cmid" to conversationMessageId.toString() + ).apply { + extended?.let { this["extended"] = it.toString() } + count?.let { this["count"] = it.toString() } + offset?.let { this["offset"] = it.toString() } + preserveOrder?.let { this["preserve_order"] = it.toString() } + fields?.let { this["fields"] = it } + } +} diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/MessagesResponse.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/MessagesResponse.kt index 94500cd1..558ae7e4 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/MessagesResponse.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/MessagesResponse.kt @@ -1,11 +1,13 @@ package com.meloda.app.fast.model.api.responses +import com.meloda.app.fast.model.api.data.VkAttachmentHistoryMessageData import com.meloda.app.fast.model.api.data.VkChatMemberData import com.meloda.app.fast.model.api.data.VkContactData import com.meloda.app.fast.model.api.data.VkConversationData import com.meloda.app.fast.model.api.data.VkGroupData import com.meloda.app.fast.model.api.data.VkMessageData import com.meloda.app.fast.model.api.data.VkUserData +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) @@ -33,3 +35,12 @@ data class MessagesGetConversationMembersResponse( val profiles: List?, val groups: List? ) + +@JsonClass(generateAdapter = true) +data class MessagesGetHistoryAttachmentsResponse( + @Json(name = "items") val items: List, + @Json(name = "next_from") val nextFrom: String?, + @Json(name = "profiles") val profiles: List?, + @Json(name = "groups") val groups: List?, + @Json(name = "contacts") val contacts: List? +) diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkMessageEntity.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkMessageEntity.kt index 417ddffd..4debac6c 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkMessageEntity.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkMessageEntity.kt @@ -8,6 +8,7 @@ import com.meloda.app.fast.model.api.domain.VkMessage @Entity(tableName = "messages") data class VkMessageEntity( @PrimaryKey val id: Int, + val conversationMessageId: Int, val text: String?, val isOut: Boolean, val peerId: Int, @@ -29,6 +30,7 @@ data class VkMessageEntity( fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( id = id, + conversationMessageId = conversationMessageId, text = text, isOut = isOut, peerId = peerId, diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesService.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesService.kt index c8b35bbd..d579520c 100644 --- a/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesService.kt +++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesService.kt @@ -2,6 +2,7 @@ package com.meloda.app.fast.network.service.messages import com.meloda.app.fast.model.api.data.VkLongPollData import com.meloda.app.fast.model.api.responses.MessagesGetByIdResponse +import com.meloda.app.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse import com.meloda.app.fast.model.api.responses.MessagesGetHistoryResponse import com.meloda.app.fast.network.ApiResponse import com.meloda.app.fast.network.RestApiError @@ -42,6 +43,12 @@ interface MessagesService { @FieldMap params: Map ): ApiResult, RestApiError> + @FormUrlEncoded + @POST(MessagesUrls.GET_HISTORY_ATTACHMENTS) + suspend fun getHistoryAttachments( + @FieldMap params: Map + ): ApiResult, RestApiError> + // @FormUrlEncoded // @POST(MessagesUrls.MarkAsImportant) // suspend fun markAsImportant( diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesUrls.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesUrls.kt index 6dda3c58..13d77f21 100644 --- a/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesUrls.kt +++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesUrls.kt @@ -18,4 +18,5 @@ object MessagesUrls { const val GET_CHAT = "${AppConstants.URL_API}/messages.getChat" const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers" const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser" + const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments" } diff --git a/feature/chatmaterials/build.gradle.kts b/feature/chatmaterials/build.gradle.kts index d6c028a7..1ede0187 100644 --- a/feature/chatmaterials/build.gradle.kts +++ b/feature/chatmaterials/build.gradle.kts @@ -27,6 +27,7 @@ android { } kotlinOptions { jvmTarget = Configs.java.toString() + freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers") } buildFeatures { compose = true diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsScreenContent.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsScreenContent.kt deleted file mode 100644 index e9178f76..00000000 --- a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsScreenContent.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.meloda.app.fast.chatmaterials - -import android.annotation.SuppressLint -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.haze -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials - -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalHazeMaterialsApi::class -) -@Composable -fun ChatMaterialsScreen( - onBack: () -> Unit -) { - val hazeState = remember { HazeState() } - - Scaffold( - topBar = { - TopAppBar( - title = { - Text(text = "Chat Materials") - }, - colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent), - modifier = Modifier - .hazeChild( - state = hazeState, - style = HazeMaterials.ultraThin() - ) - .fillMaxWidth(), - navigationIcon = { - IconButton( - onClick = onBack - ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = null - ) - } - } - ) - } - ) { - LazyVerticalGrid( - columns = GridCells.Adaptive(200.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .haze( - state = hazeState, - style = HazeMaterials.ultraThin() - ) - ) { - items(100) { index -> - val link = "https://random.imagecdn.app/500/150" - - AsyncImage( - model = link, - contentDescription = "Image", - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) - } - } - } -} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsViewModel.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsViewModel.kt new file mode 100644 index 00000000..9d0d02ce --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsViewModel.kt @@ -0,0 +1,130 @@ +package com.meloda.app.fast.chatmaterials + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.meloda.app.fast.chatmaterials.model.ChatMaterialsScreenState +import com.meloda.app.fast.chatmaterials.navigation.ChatMaterials +import com.meloda.app.fast.chatmaterials.util.asPresentation +import com.meloda.app.fast.common.extensions.listenValue +import com.meloda.app.fast.common.extensions.setValue +import com.meloda.app.fast.data.api.messages.MessagesUseCase +import com.meloda.app.fast.data.processState +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +interface ChatMaterialsViewModel { + val screenState: StateFlow + val baseError: StateFlow + val imagesToPreload: StateFlow> + val currentOffset: StateFlow + val canPaginate: StateFlow + + fun onMetPaginationCondition() + + fun onRefresh() + + fun onErrorConsumed() + + fun onTypeChanged(newType: String) +} + +class ChatMaterialsViewModelImpl( + private val messagesUseCase: MessagesUseCase, + savedStateHandle: SavedStateHandle +) : ViewModel(), ChatMaterialsViewModel { + + override val screenState = MutableStateFlow(ChatMaterialsScreenState.EMPTY) + + override val baseError = MutableStateFlow(null) + override val imagesToPreload = MutableStateFlow>(emptyList()) + override val currentOffset = MutableStateFlow(0) + override val canPaginate = MutableStateFlow(false) + + init { + val arguments = ChatMaterials.from(savedStateHandle) + + screenState.setValue { old -> + old.copy( + peerId = arguments.peerId, + conversationMessageId = arguments.conversationMessageId + ) + } + + loadChatMaterials() + } + + override fun onMetPaginationCondition() { + currentOffset.update { screenState.value.materials.size } + loadChatMaterials() + } + + override fun onRefresh() { + loadChatMaterials(offset = 0) + } + + override fun onErrorConsumed() { + baseError.setValue { null } + } + + override fun onTypeChanged(newType: String) { + screenState.setValue { old -> old.copy(attachmentType = newType) } + loadChatMaterials(0) + } + + private fun loadChatMaterials( + offset: Int = currentOffset.value + ) { + messagesUseCase.getHistoryAttachments( + peerId = screenState.value.peerId, + count = LOAD_COUNT, + offset = offset, + attachmentTypes = listOf(screenState.value.attachmentType), + conversationMessageId = screenState.value.conversationMessageId + ).listenValue { state -> + state.processState( + error = { error -> + + }, + success = { response -> + val itemsCountSufficient = response.size == LOAD_COUNT + canPaginate.setValue { itemsCountSufficient } + + val paginationExhausted = !itemsCountSufficient && + screenState.value.materials.size >= LOAD_COUNT + + val loadedMaterials = response.map(VkAttachmentHistoryMessage::asPresentation) + + val newState = screenState.value.copy( + isPaginationExhausted = paginationExhausted + ) + + if (offset == 0) { + screenState.setValue { + newState.copy(materials = loadedMaterials) + } + } else { + screenState.setValue { + newState.copy( + materials = newState.materials.plus(loadedMaterials) + ) + } + } + } + ) + + screenState.setValue { old -> + old.copy( + isLoading = offset == 0 && state.isLoading(), + isPaginating = offset > 0 && state.isLoading() + ) + } + } + } + + companion object { + const val LOAD_COUNT = 100 + } +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/di/ChatMaterialsModule.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/di/ChatMaterialsModule.kt new file mode 100644 index 00000000..9179ed12 --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/di/ChatMaterialsModule.kt @@ -0,0 +1,9 @@ +package com.meloda.app.fast.chatmaterials.di + +import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val chatMaterialsModule = module { + viewModelOf(::ChatMaterialsViewModelImpl) +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/ChatMaterialsScreenState.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/ChatMaterialsScreenState.kt new file mode 100644 index 00000000..41b1dfdb --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/ChatMaterialsScreenState.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.chatmaterials.model + +data class ChatMaterialsScreenState( + val isLoading: Boolean, + val materials: List, + val attachmentType: String, + val isPaginating: Boolean, + val isPaginationExhausted: Boolean, + val peerId: Int, + val conversationMessageId: Int +) { + + companion object { + val EMPTY: ChatMaterialsScreenState = ChatMaterialsScreenState( + isLoading = true, + materials = emptyList(), + attachmentType = "photo", + isPaginating = false, + isPaginationExhausted = false, + peerId = -1, + conversationMessageId = -1 + ) + } +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/UiChatMaterial.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/UiChatMaterial.kt new file mode 100644 index 00000000..1084b29f --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/model/UiChatMaterial.kt @@ -0,0 +1,28 @@ +package com.meloda.app.fast.chatmaterials.model + +sealed class UiChatMaterial { + + data class Photo( + val previewUrl: String + ) : UiChatMaterial() + + data class Video( + val previewUrl: String + ) : UiChatMaterial() + + data class Audio( + val previewUrl: String?, + val title: String, + val artist: String, + val duration: String + ) : UiChatMaterial() + + data class File( + val title: String + ) : UiChatMaterial() + + data class Link( + val title: String, + val previewUrl: String? + ) : UiChatMaterial() +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt index 2e976cc2..5a12ecde 100644 --- a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt @@ -1,13 +1,23 @@ package com.meloda.app.fast.chatmaterials.navigation +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.meloda.app.fast.chatmaterials.ChatMaterialsScreen +import androidx.navigation.toRoute +import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsScreen import kotlinx.serialization.Serializable @Serializable -data class ChatMaterials(val a: String) +data class ChatMaterials( + val peerId: Int, + val conversationMessageId: Int +) { + companion object { + fun from(savedStateHandle: SavedStateHandle) = + savedStateHandle.toRoute() + } +} fun NavGraphBuilder.chatMaterialsRoute( onBack: () -> Unit @@ -19,6 +29,11 @@ fun NavGraphBuilder.chatMaterialsRoute( } } -fun NavController.navigateToChatMaterials() { - this.navigate(ChatMaterials("")) +fun NavController.navigateToChatMaterials(peerId: Int, conversationMessageId: Int) { + this.navigate( + ChatMaterials( + peerId = peerId, + conversationMessageId = conversationMessageId + ) + ) } diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialItem.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialItem.kt new file mode 100644 index 00000000..5feb6fd7 --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialItem.kt @@ -0,0 +1,68 @@ +package com.meloda.app.fast.chatmaterials.presentation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import coil.ImageLoader +import coil.compose.AsyncImage +import com.meloda.app.fast.chatmaterials.model.UiChatMaterial + +@Composable +fun ChatMaterialItem( + item: UiChatMaterial, + imageLoader: ImageLoader +) { + when (item) { + is UiChatMaterial.Photo -> { + AsyncImage( + model = item.previewUrl, + contentDescription = null, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + } + + is UiChatMaterial.Video -> { + AsyncImage( + model = item.previewUrl, + contentDescription = null, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + } + + is UiChatMaterial.Audio -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = item.title, + style = MaterialTheme.typography.bodyLarge + ) + Text(text = item.artist) + } + + Text(text = item.duration) + } + } + + is UiChatMaterial.File -> {} + + is UiChatMaterial.Link -> {} + } +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt new file mode 100644 index 00000000..cde1102f --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/presentation/ChatMaterialsScreen.kt @@ -0,0 +1,352 @@ +package com.meloda.app.fast.chatmaterials.presentation + +import android.annotation.SuppressLint +import android.util.Log +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.imageLoader +import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModel +import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl +import com.meloda.app.fast.designsystem.LocalTheme +import com.meloda.app.fast.designsystem.R +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import org.koin.androidx.compose.koinViewModel + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class +) +@Composable +fun ChatMaterialsScreen( + onBack: () -> Unit, + viewModel: ChatMaterialsViewModel = koinViewModel() +) { + val currentTheme = LocalTheme.current + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val attachments = screenState.materials + + val imageLoader = LocalContext.current.imageLoader + + var moreClearBlur by rememberSaveable { + mutableStateOf(false) + } + + val hazeState = remember { HazeState() } + val hazeStyle = if (moreClearBlur) HazeMaterials.ultraThin() else HazeMaterials.regular() + + var dropDownMenuExpanded by remember { + mutableStateOf(false) + } + var checkedTypeIndex by rememberSaveable { + mutableIntStateOf(0) + } + + LaunchedEffect(checkedTypeIndex) { + viewModel.onTypeChanged( + when (checkedTypeIndex) { + 0 -> "photo" + 1 -> "video" + 2 -> "audio" + 3 -> "doc" + 4 -> "link" + else -> "" + } + ) + } + + val titles = listOf("Photos", "Videos", "Audios", "Files", "Links") + + val listState = rememberLazyListState() + val gridState = rememberLazyGridState() + + val canScrollBackward = when (checkedTypeIndex) { + in 0..1 -> gridState.canScrollBackward + else -> listState.canScrollBackward + } + + Log.d("ChatMaterialsScreen", "ChatMaterialsScreen: canScrollBackward: $canScrollBackward") + + val toolbarColorAlpha by animateFloatAsState( + targetValue = if (!canScrollBackward) 1f else 0f, + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + val toolbarContainerColor by animateColorAsState( + targetValue = + if (currentTheme.usingBlur || !canScrollBackward) + MaterialTheme.colorScheme.surface + else + MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + val pullToRefreshAlpha by animateFloatAsState( + targetValue = if (!canScrollBackward) 1f else 0f, + label = "pullToRefreshAlpha", + animationSpec = tween(durationMillis = 50) + ) + + val pullToRefreshState = rememberPullToRefreshState() + + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = "Chat Materials") + }, + modifier = Modifier + .then( + if (currentTheme.usingBlur) { + Modifier.hazeChild( + state = hazeState, + style = hazeStyle + ) + } else Modifier + ) + .fillMaxWidth(), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = toolbarContainerColor.copy( + alpha = if (currentTheme.usingBlur) toolbarColorAlpha else 1f + ) + ), + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) + } + }, + actions = { + IconButton( + onClick = { + dropDownMenuExpanded = true + } + ) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = "Options button" + ) + } + + DropdownMenu( + modifier = Modifier.defaultMinSize(minWidth = 140.dp), + expanded = dropDownMenuExpanded, + onDismissRequest = { + dropDownMenuExpanded = false + }, + offset = DpOffset(x = (-4).dp, y = (-60).dp) + ) { + DropdownMenuItem( + onClick = { + viewModel.onRefresh() + dropDownMenuExpanded = false + }, + text = { + Text(text = stringResource(id = R.string.action_refresh)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + } + ) + + if (currentTheme.usingBlur) { + DropdownMenuItem( + text = { + Text(text = if (moreClearBlur) "Default blur" else "Clearer blur") + }, + onClick = { + moreClearBlur = !moreClearBlur + dropDownMenuExpanded = false + } + ) + } + + HorizontalDivider() + + titles.forEachIndexed { index, title -> + DropdownMenuItem( + leadingIcon = { + RadioButton( + selected = checkedTypeIndex == index, + onClick = null + ) + }, + text = { + Text(text = title) + }, + onClick = { + checkedTypeIndex = index + dropDownMenuExpanded = false + } + ) + } + + } + } + ) + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) + .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + .nestedScroll(pullToRefreshState.nestedScrollConnection) + ) { + if (checkedTypeIndex in listOf(0, 1)) { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + state = gridState, + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier + .then( + if (currentTheme.usingBlur) { + Modifier.haze( + state = hazeState, + style = hazeStyle + ) + } else { + Modifier + } + ) + .fillMaxSize() + + ) { + repeat(3) { + item { + Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + } + } + items(attachments) { item -> + ChatMaterialItem( + item = item, + imageLoader = imageLoader + ) + } + repeat(3) { + item { + Spacer(modifier = Modifier.height(padding.calculateBottomPadding())) + } + } + } + } else { + LazyColumn( + state = listState, + modifier = Modifier + .then( + if (currentTheme.usingBlur) { + Modifier.haze( + state = hazeState, + style = hazeStyle + ) + } else { + Modifier + } + ) + .fillMaxSize() + + ) { + item { + Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + } + items(attachments) { item -> + ChatMaterialItem( + item = item, + imageLoader = imageLoader + ) + } + item { + Spacer(modifier = Modifier.height(padding.calculateBottomPadding())) + } + } + } + + if (pullToRefreshState.isRefreshing) { + LaunchedEffect(true) { + viewModel.onRefresh() + } + } + + LaunchedEffect(screenState.isLoading) { + if (!screenState.isLoading) { + pullToRefreshState.endRefresh() + } + } + + PullToRefreshContainer( + state = pullToRefreshState, + modifier = Modifier + .alpha(pullToRefreshAlpha) + .align(Alignment.TopCenter) + .padding(top = padding.calculateTopPadding()), + contentColor = MaterialTheme.colorScheme.primary + ) + } + } +} diff --git a/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/util/ChatMaterialMapper.kt b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/util/ChatMaterialMapper.kt new file mode 100644 index 00000000..6c1d99a7 --- /dev/null +++ b/feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/util/ChatMaterialMapper.kt @@ -0,0 +1,59 @@ +package com.meloda.app.fast.chatmaterials.util + +import com.meloda.app.fast.chatmaterials.model.UiChatMaterial +import com.meloda.app.fast.model.api.data.AttachmentType +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage +import com.meloda.app.fast.model.api.domain.VkAudioDomain +import com.meloda.app.fast.model.api.domain.VkFileDomain +import com.meloda.app.fast.model.api.domain.VkLinkDomain +import com.meloda.app.fast.model.api.domain.VkPhotoDomain +import com.meloda.app.fast.model.api.domain.VkVideoDomain +import java.text.SimpleDateFormat +import java.util.Locale + +fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial = + when (val type = this.attachment.type) { + AttachmentType.PHOTO -> { + val attachment = this.attachment as VkPhotoDomain + UiChatMaterial.Photo( + previewUrl = attachment.getSizeOrSmaller(VkPhotoDomain.SIZE_TYPE_1080_1024)?.url.orEmpty() + ) + } + + AttachmentType.VIDEO -> { + val attachment = this.attachment as VkVideoDomain + UiChatMaterial.Video( + previewUrl = attachment.images.firstOrNull()?.url.orEmpty() + ) + } + + AttachmentType.AUDIO -> { + val attachment = this.attachment as VkAudioDomain + UiChatMaterial.Audio( + previewUrl = null, + title = attachment.title, + artist = attachment.artist, + duration = SimpleDateFormat( + "mm:ss", + Locale.getDefault() + ).format(attachment.duration) + ) + } + + AttachmentType.FILE -> { + val attachment = this.attachment as VkFileDomain + UiChatMaterial.File( + title = attachment.title + ) + } + + AttachmentType.LINK -> { + val attachment = this.attachment as VkLinkDomain + UiChatMaterial.Link( + title = attachment.title ?: attachment.url, + previewUrl = attachment.photo?.getMaxSize()?.url + ) + } + + else -> throw IllegalArgumentException("Unsupported type: $type") + } diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsList.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsList.kt index a7a82027..006c4648 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsList.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsList.kt @@ -27,6 +27,7 @@ import com.meloda.app.fast.common.UserConfig import com.meloda.app.fast.conversations.model.ConversationOption import com.meloda.app.fast.conversations.model.ConversationsScreenState import com.meloda.app.fast.conversations.model.UiConversation +import com.meloda.app.fast.designsystem.LocalBottomPadding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,6 +46,8 @@ fun ConversationsListComposable( val conversations = screenState.conversations + val bottomPadding = LocalBottomPadding.current + LazyColumn( modifier = modifier, state = state @@ -105,5 +108,9 @@ fun ConversationsListComposable( } } } + + item { + Spacer(modifier = Modifier.height(bottomPadding)) + } } } diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt index 9279ed43..896ba3d2 100644 --- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt @@ -5,16 +5,20 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars @@ -61,6 +65,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.imageLoader @@ -70,6 +75,8 @@ import com.meloda.app.fast.conversations.ConversationsViewModel import com.meloda.app.fast.conversations.ConversationsViewModelImpl import com.meloda.app.fast.conversations.model.ConversationsScreenState import com.meloda.app.fast.conversations.model.UiConversation +import com.meloda.app.fast.designsystem.LocalBottomPadding +import com.meloda.app.fast.designsystem.LocalHazeState import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.components.FullScreenLoader @@ -142,7 +149,8 @@ fun ConversationsScreen( } } - val hazeState = remember { HazeState() } +// val hazeState = remember { HazeState() } + val hazeState = LocalHazeState.current var dropDownMenuExpanded by remember { mutableStateOf(false) @@ -252,38 +260,42 @@ fun ConversationsScreen( val scope = rememberCoroutineScope() val rotation = remember { Animatable(0f) } - AnimatedVisibility( - visible = isListScrollingUp, - modifier = Modifier.navigationBarsPadding(), - enter = slideIn { IntOffset(0, 400) }, - exit = slideOut { IntOffset(0, 400) } - ) { - FloatingActionButton( - onClick = { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + Column { + AnimatedVisibility( + visible = isListScrollingUp, + modifier = Modifier.navigationBarsPadding(), + enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)), + exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200)) + ) { + FloatingActionButton( + onClick = { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - scope.launch { - for (i in 20 downTo 0 step 4) { - rotation.animateTo( - targetValue = i.toFloat(), - animationSpec = tween(50) - ) - if (i > 0) { + scope.launch { + for (i in 20 downTo 0 step 4) { rotation.animateTo( - targetValue = -i.toFloat(), + targetValue = i.toFloat(), animationSpec = tween(50) ) + if (i > 0) { + rotation.animateTo( + targetValue = -i.toFloat(), + animationSpec = tween(50) + ) + } } } - } - }, - modifier = Modifier.rotate(rotation.value) - ) { - Icon( - painter = painterResource(id = UiR.drawable.ic_baseline_create_24), - contentDescription = "Add chat button" - ) + }, + modifier = Modifier.rotate(rotation.value) + ) { + Icon( + painter = painterResource(id = UiR.drawable.ic_baseline_create_24), + contentDescription = "Add chat button" + ) + } } + + Spacer(modifier = Modifier.height(LocalBottomPadding.current)) } } ) { padding -> diff --git a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt index e64c5861..ebf49926 100644 --- a/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt @@ -77,7 +77,7 @@ class FriendsViewModelImpl( } private fun loadFriends(offset: Int = currentOffset.value) { - friendsUseCase.getAllFriends(count = 30, offset = offset).listenValue { state -> + friendsUseCase.getAllFriends(count = LOAD_COUNT, offset = offset).listenValue { state -> state.processState( error = { error -> when (error) { @@ -103,11 +103,11 @@ class FriendsViewModelImpl( }, success = { info -> val response = info.friends - val itemsCountSufficient = response.size == 30 + val itemsCountSufficient = response.size == LOAD_COUNT canPaginate.setValue { itemsCountSufficient } val paginationExhausted = !itemsCountSufficient && - screenState.value.friends.size >= 30 + screenState.value.friends.size >= LOAD_COUNT imagesToPreload.setValue { response.mapNotNull(VkUser::photo100) @@ -172,4 +172,8 @@ class FriendsViewModelImpl( } uiOnlineFriends.setValue { onlineUiFriends } } + + companion object { + const val LOAD_COUNT = 30 + } } 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 12021d40..8481cbed 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 @@ -354,6 +354,7 @@ class MessagesHistoryViewModelImpl( val newMessage = VkMessage( id = -1 - sendingMessages.size, + conversationMessageId = -1, text = lastMessageText, isOut = true, peerId = screenState.value.conversationId, diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/domain/MessagesUseCaseImpl.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/domain/MessagesUseCaseImpl.kt index 894f7229..18e6ac68 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/domain/MessagesUseCaseImpl.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/domain/MessagesUseCaseImpl.kt @@ -1,27 +1,28 @@ package com.meloda.app.fast.messageshistory.domain import com.meloda.app.fast.data.State -import com.meloda.app.fast.data.api.messages.MessagesHistoryDomain +import com.meloda.app.fast.data.api.messages.MessagesHistoryInfo import com.meloda.app.fast.data.api.messages.MessagesRepository import com.meloda.app.fast.data.api.messages.MessagesUseCase import com.meloda.app.fast.data.mapToState import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage import com.meloda.app.fast.model.api.domain.VkMessage import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class MessagesUseCaseImpl( - private val messagesRepository: MessagesRepository + private val repository: MessagesRepository ) : MessagesUseCase { override fun getMessagesHistory( conversationId: Int, count: Int?, offset: Int? - ): Flow> = flow { + ): Flow> = flow { emit(State.Loading) - val newState = messagesRepository.getMessagesHistory( + val newState = repository.getHistory( conversationId = conversationId, offset = offset, count = count @@ -31,60 +32,20 @@ class MessagesUseCaseImpl( } override fun getById( - messageId: Int, - extended: Boolean?, - fields: String? - ): Flow> = flow { - emit(State.Loading) - - val newState = messagesRepository.getMessageById( - messagesIds = listOf(messageId), - extended = extended, - fields = fields - ).mapToState() - emit(newState) - } - - override fun getByIds( messageIds: List, extended: Boolean?, fields: String? - ): Flow>> = flow {} -// flow { -// emit(State.Loading) -// -// val newState = messagesRepository.getById( -// params = MessagesGetByIdRequest( -// messagesIds = messageIds, -// extended = extended, -// fields = fields -// ) -// ).fold( -// onSuccess = { response -> -// val messages = response.items -// val usersMap = -// VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain)) -// val groupsMap = -// VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain)) -// -// com.meloda.app.fast.network.State.Success( -// messages.map { message -> -// message.mapToDomain( -// user = usersMap.messageUser(message), -// group = groupsMap.messageGroup(message), -// actionUser = usersMap.messageActionUser(message), -// actionGroup = groupsMap.messageActionGroup(message) -// ) -// } -// ) -// }, -// onNetworkFailure = { com.meloda.app.fast.network.State.Error.ConnectionError }, -// onUnknownFailure = { com.meloda.app.fast.network.State.UNKNOWN_ERROR }, -// onHttpFailure = { result -> result.error.toStateApiError() }, -// onApiFailure = { result -> result.error.toStateApiError() } -// ) -// emit(newState) -// } + ): Flow>> = flow { + emit(State.Loading) + + val newState = repository.getById( + messagesIds = messageIds, + extended = extended, + fields = fields + ).mapToState() + + emit(newState) + } override fun sendMessage( peerId: Int, @@ -95,7 +56,7 @@ class MessagesUseCaseImpl( ): Flow> = flow { emit(State.Loading) - val newState = messagesRepository.send( + val newState = repository.send( peerId = peerId, randomId = randomId, message = message, @@ -112,7 +73,7 @@ class MessagesUseCaseImpl( ): Flow> = flow { emit(State.Loading) - val newState = messagesRepository.markAsRead( + val newState = repository.markAsRead( peerId = peerId, startMessageId = startMessageId ).mapToState() @@ -120,11 +81,31 @@ class MessagesUseCaseImpl( emit(newState) } + override fun getHistoryAttachments( + peerId: Int, + count: Int?, + offset: Int?, + attachmentTypes: List, + conversationMessageId: Int + ): Flow>> = flow { + emit(State.Loading) + + val newState = repository.getHistoryAttachments( + peerId = peerId, + count = count, + offset = offset, + attachmentTypes = attachmentTypes, + conversationMessageId = conversationMessageId + ).mapToState() + + emit(newState) + } + override suspend fun storeMessage(message: VkMessage) { - messagesRepository.storeMessages(listOf(message)) + repository.storeMessages(listOf(message)) } override suspend fun storeMessages(messages: List) { - messagesRepository.storeMessages(messages) + repository.storeMessages(messages) } } 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 index 655db9ef..ffddcfb6 100644 --- 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 @@ -4,6 +4,7 @@ import com.meloda.app.fast.common.UiImage data class UiMessage( val id: Int, + val conversationMessageId: Int, val text: String?, val isOut: Boolean, val fromId: Int, diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt index d2296e2b..4b516198 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt @@ -40,7 +40,7 @@ val MessagesHistoryNavType = object : NavType(isNullab fun NavGraphBuilder.messagesHistoryRoute( onError: (BaseError) -> Unit, onBack: () -> Unit, - onNavigateToChatAttachments: () -> Unit + onNavigateToChatAttachments: (peerId: Int, conversationMessageId: Int) -> Unit ) { composable( typeMap = mapOf(typeOf() to MessagesHistoryNavType) 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 3bc86043..60af7c23 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 @@ -90,7 +90,7 @@ import com.meloda.app.fast.designsystem.R as UiR fun MessagesHistoryScreen( onError: (BaseError) -> Unit, onBack: () -> Unit, - onNavigateToChatMaterials: () -> Unit, + onNavigateToChatMaterials: (peerId: Int, conversationMessageId: Int) -> Unit, viewModel: MessagesHistoryViewModel = koinViewModel() ) { val view = LocalView.current @@ -215,7 +215,12 @@ fun MessagesHistoryScreen( DropdownMenuItem( onClick = { dropDownMenuExpanded = false - onNavigateToChatMaterials() + + // TODO: 11/07/2024, Danil Nikolaev: to VM + onNavigateToChatMaterials( + screenState.conversationId, + screenState.messages.first().conversationMessageId + ) }, text = { Text(text = "Materials") 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 265c6a66..a4c3b44e 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 @@ -91,6 +91,7 @@ fun VkMessage.asPresentation( nextMessage: VkMessage? ): UiMessage = UiMessage( id = id, + conversationMessageId = conversationMessageId, text = text, isOut = isOut, fromId = fromId, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66f39c3c..77b81e3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ agp = "8.5.0" converterMoshi = "2.11.0" eithernet = "1.9.0" -haze = "0.7.2" +haze = "0.7.3" kotlin = "2.0.0" ksp = "2.0.0-1.0.22" @@ -26,7 +26,7 @@ nanokt = "1.2.0" junitVersion = "1.2.1" espressoCore = "3.6.1" appcompat = "1.7.0" -androidx-navigation = "2.8.0-beta04" +androidx-navigation = "2.8.0-beta05" serialization = "1.7.1" [libraries] @@ -47,7 +47,6 @@ compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" } -eithernet-retrofit-integration = { module = "com.slack.eithernet:eithernet-integration-retrofit", version.ref = "eithernet" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } junit = { module = "junit:junit", version.ref = "junit" }