diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 877c0e80..4f27a3fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(projects.feature.friends) implementation(projects.feature.profile) implementation(projects.feature.photoviewer) + implementation(projects.feature.createchat) implementation(projects.core.common) implementation(projects.core.ui) diff --git a/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt index d17330bd..01d0feae 100644 --- a/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt +++ b/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt @@ -16,6 +16,7 @@ import dev.meloda.fast.common.provider.Provider import dev.meloda.fast.common.provider.ResourceProvider import dev.meloda.fast.common.provider.ResourceProviderImpl import dev.meloda.fast.conversations.di.conversationsModule +import dev.meloda.fast.conversations.di.createChatModule import dev.meloda.fast.domain.di.domainModule import dev.meloda.fast.friends.di.friendsModule import dev.meloda.fast.languagepicker.di.languagePickerModule @@ -46,7 +47,8 @@ val applicationModule = module { longPollModule, friendsModule, profileModule, - chatMaterialsModule + chatMaterialsModule, + createChatModule ) // TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors diff --git a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt index d3166245..f4ab6d59 100644 --- a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt +++ b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt @@ -25,6 +25,7 @@ fun NavGraphBuilder.mainScreen( onConversationClicked: (conversationId: Int) -> Unit, onPhotoClicked: (url: String) -> Unit, onMessageClicked: (userId: Int) -> Unit, + onCreateChatClicked: () -> Unit, viewModel: MainViewModel ) { val navigationItems = ImmutableList.of( @@ -56,6 +57,7 @@ fun NavGraphBuilder.mainScreen( onConversationItemClicked = onConversationClicked, onPhotoClicked = onPhotoClicked, onMessageClicked = onMessageClicked, + onCreateChatClicked = onCreateChatClicked, viewModel = viewModel ) } diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt index 42019a82..bf93b82f 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -57,6 +57,7 @@ fun MainScreen( onConversationItemClicked: (conversationId: Int) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {}, onMessageClicked: (userId: Int) -> Unit = {}, + onCreateChatClicked: () -> Unit = {}, viewModel: MainViewModel ) { val currentTheme = LocalThemeConfig.current @@ -172,6 +173,7 @@ fun MainScreen( onError = onError, onConversationItemClicked = onConversationItemClicked, onPhotoClicked = onPhotoClicked, + onCreateChatClicked = onCreateChatClicked, navController = navController, ) profileScreen( diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt index 5fd28fe5..b796f63d 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -25,6 +25,8 @@ import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials +import dev.meloda.fast.conversations.navigation.createChatScreen +import dev.meloda.fast.conversations.navigation.navigateToCreateChat import dev.meloda.fast.languagepicker.navigation.languagePickerScreen import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen @@ -125,6 +127,7 @@ fun RootScreen( onConversationClicked = navController::navigateToMessagesHistory, onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }, onMessageClicked = navController::navigateToMessagesHistory, + onCreateChatClicked = navController::navigateToCreateChat, viewModel = viewModel ) @@ -137,6 +140,13 @@ fun RootScreen( onBack = navController::navigateUp, onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } ) + createChatScreen( + onChatCreated = { conversationId -> + navController.popBackStack() + navController.navigateToMessagesHistory(conversationId) + }, + navController = navController + ) settingsScreen( onBack = navController::navigateUp, diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt index f1721fab..a9713a38 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt @@ -1,10 +1,10 @@ package dev.meloda.fast.data.api.messages +import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.network.RestApiErrorDomain -import com.slack.eithernet.ApiResult interface MessagesRepository { @@ -41,6 +41,11 @@ interface MessagesRepository { conversationMessageId: Int ): ApiResult, RestApiErrorDomain> + suspend fun createChat( + userIds: List?, + title: String? + ): ApiResult + suspend fun storeMessages(messages: List) // suspend fun markAsImportant( diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt index ded9b1ff..0dafa7fe 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.data.api.messages +import com.slack.eithernet.ApiResult import dev.meloda.fast.common.VkConstants import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkMemoryCache @@ -14,6 +15,7 @@ import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.asEntity +import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest @@ -23,7 +25,6 @@ import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.service.messages.MessagesService -import com.slack.eithernet.ApiResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -198,6 +199,23 @@ class MessagesRepositoryImpl( ) } + override suspend fun createChat( + userIds: List?, + title: String? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesCreateChatRequest( + userIds = userIds, + title = title + ) + + messagesService.createChat(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + apiResponse.requireResponse().chatId + }, + errorMapper = { error -> error?.toDomain() } + ) + } + override suspend fun storeMessages(messages: List) { messageDao.insertAll(messages.map(VkMessage::asEntity)) } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUserByIdUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUserByIdUseCase.kt index bf61c2e7..37d322dc 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUserByIdUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUserByIdUseCase.kt @@ -20,4 +20,8 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) { emit(newState) } + + suspend fun proceed(userId: Int): VkUser? { + return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull() + } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt index 84e533c7..7275c87d 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt @@ -42,6 +42,11 @@ interface MessagesUseCase { conversationMessageId: Int ): Flow>> + fun createChat( + userIds: List?, + title: String? + ): Flow> + suspend fun storeMessage(message: VkMessage) suspend fun storeMessages(messages: List) } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt index 9239300e..3c6c9f52 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt @@ -100,6 +100,14 @@ class MessagesUseCaseImpl( emit(newState) } + override fun createChat(userIds: List?, title: String?): Flow> = flow { + emit(State.Loading) + + val newState = repository.createChat(userIds, title).mapToState() + + emit(newState) + } + override suspend fun storeMessage(message: VkMessage) { repository.storeMessages(listOf(message)) } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/util/FriendMapper.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/util/FriendMapper.kt similarity index 70% rename from feature/friends/src/main/kotlin/dev/meloda/fast/friends/util/FriendMapper.kt rename to core/domain/src/main/kotlin/dev/meloda/fast/domain/util/FriendMapper.kt index 4c4d3fb4..0c37194d 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/util/FriendMapper.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/util/FriendMapper.kt @@ -1,9 +1,9 @@ -package dev.meloda.fast.friends.util +package dev.meloda.fast.domain.util import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.data.VkMemoryCache -import dev.meloda.fast.friends.model.UiFriend import dev.meloda.fast.model.api.domain.VkUser +import dev.meloda.fast.ui.model.api.UiFriend fun VkUser.asPresentation( useContactNames: Boolean = false @@ -16,5 +16,7 @@ fun VkUser.asPresentation( fullName }, onlineStatus = onlineStatus, - photo400Orig = photo400Orig?.let(UiImage::Url) + photo400Orig = photo400Orig?.let(UiImage::Url), + firstName = firstName, + lastName = lastName ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/MessagesRequest.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/MessagesRequest.kt index 00ab745f..6bc6f569 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/MessagesRequest.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/MessagesRequest.kt @@ -267,3 +267,14 @@ data class MessagesGetHistoryAttachmentsRequest( fields?.let { this["fields"] = it } } } + +data class MessagesCreateChatRequest( + val userIds: List?, + val title: String? +) { + + val map = mutableMapOf().apply { + userIds?.let { this["user_ids"] = it.joinToString(",") } + title?.let { this["title"] = it } + } +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/MessagesResponse.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/MessagesResponse.kt index cd7df491..672295f5 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/MessagesResponse.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/MessagesResponse.kt @@ -1,5 +1,7 @@ package dev.meloda.fast.model.api.responses +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData import dev.meloda.fast.model.api.data.VkChatMemberData import dev.meloda.fast.model.api.data.VkContactData @@ -7,8 +9,6 @@ import dev.meloda.fast.model.api.data.VkConversationData import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkUserData -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class MessagesGetHistoryResponse( @@ -44,3 +44,9 @@ data class MessagesGetHistoryAttachmentsResponse( @Json(name = "groups") val groups: List?, @Json(name = "contacts") val contacts: List? ) + +@JsonClass(generateAdapter = true) +data class MessagesCreateChatResponse( + @Json(name = "chat_id") val chatId: Int, + @Json(name = "peer_ids") val peerIds: List +) diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt index 25f1b6ad..e5a16e09 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt @@ -1,12 +1,13 @@ package dev.meloda.fast.network.service.messages +import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.data.VkLongPollData +import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse import dev.meloda.fast.network.ApiResponse import dev.meloda.fast.network.RestApiError -import com.slack.eithernet.ApiResult import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -49,6 +50,12 @@ interface MessagesService { @FieldMap params: Map ): ApiResult, RestApiError> + @FormUrlEncoded + @POST(MessagesUrls.CREATE_CHAT) + suspend fun createChat( + @FieldMap params: Map + ): ApiResult, RestApiError> + // @FormUrlEncoded // @POST(MessagesUrls.MarkAsImportant) // suspend fun markAsImportant( diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesUrls.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesUrls.kt index a3785574..2de07a9f 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesUrls.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesUrls.kt @@ -19,4 +19,5 @@ object MessagesUrls { 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" + const val CREATE_CHAT = "${AppConstants.URL_API}/messages.createChat" } diff --git a/core/presentation/.gitignore b/core/presentation/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/presentation/build.gradle.kts b/core/presentation/build.gradle.kts new file mode 100644 index 00000000..d0873abe --- /dev/null +++ b/core/presentation/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.fast.android.library) + alias(libs.plugins.fast.android.library.compose) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "dev.meloda.fast.presentation" +} + +dependencies { +} diff --git a/core/presentation/src/main/AndroidManifest.xml b/core/presentation/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 002e8b00..a9232f56 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -11,6 +11,7 @@ android { dependencies { api(projects.core.common) api(projects.core.model) + implementation(projects.core.presentation) implementation(libs.haze) implementation(libs.haze.materials) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ActionState.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ActionState.kt similarity index 91% rename from feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ActionState.kt rename to core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ActionState.kt index cd947872..73e07a1d 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ActionState.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ActionState.kt @@ -1,4 +1,4 @@ -package dev.meloda.fast.conversations.model +package dev.meloda.fast.ui.model.api enum class ActionState { PHANTOM, CALL_IN_PROGRESS, NONE; diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationOption.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationOption.kt new file mode 100644 index 00000000..775a26a0 --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationOption.kt @@ -0,0 +1,31 @@ +package dev.meloda.fast.ui.model.api + +import dev.meloda.fast.common.model.UiImage +import dev.meloda.fast.common.model.UiText +import dev.meloda.fast.ui.R + +sealed class ConversationOption( + val title: UiText, + val icon: UiImage +) { + + data object MarkAsRead : ConversationOption( + title = UiText.Resource(R.string.action_mark_as_read), + icon = UiImage.Resource(R.drawable.round_done_all_24) + ) + + data object Pin : ConversationOption( + title = UiText.Resource(R.string.action_pin), + icon = UiImage.Resource(R.drawable.pin_outline_24) + ) + + data object Unpin : ConversationOption( + title = UiText.Resource(R.string.action_unpin), + icon = UiImage.Resource(R.drawable.pin_off_outline_24) + ) + + data object Delete : ConversationOption( + title = UiText.Resource(R.string.action_delete), + icon = UiImage.Resource(R.drawable.round_delete_outline_24) + ) +} diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsShowOptions.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationsShowOptions.kt similarity index 87% rename from feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsShowOptions.kt rename to core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationsShowOptions.kt index df0700ec..66a77a8b 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsShowOptions.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationsShowOptions.kt @@ -1,4 +1,4 @@ -package dev.meloda.fast.conversations.model +package dev.meloda.fast.ui.model.api data class ConversationsShowOptions( val showDeleteDialog: Int?, diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/UiConversation.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt similarity index 95% rename from feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/UiConversation.kt rename to core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt index 172df568..9944284f 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/UiConversation.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt @@ -1,4 +1,4 @@ -package dev.meloda.fast.conversations.model +package dev.meloda.fast.ui.model.api import androidx.compose.runtime.Immutable import androidx.compose.ui.text.AnnotatedString diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/UiFriend.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt similarity index 64% rename from feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/UiFriend.kt rename to core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt index 75b33bea..3e64e990 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/UiFriend.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt @@ -1,11 +1,15 @@ -package dev.meloda.fast.friends.model +package dev.meloda.fast.ui.model.api +import androidx.compose.runtime.Immutable import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.model.api.domain.OnlineStatus +@Immutable data class UiFriend( val userId: Int, val avatar: UiImage?, + val firstName: String, + val lastName: String, val title: String, val onlineStatus: OnlineStatus, val photo400Orig: UiImage? diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index 379b9ba6..c137d3b7 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -215,4 +215,7 @@ Никого в сети Попробовать ещё раз Срок действия сессии истёк + Создать чат + Создать + Название diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 59b6edc8..a9f1c0f4 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -280,4 +280,7 @@ No one is online Try again Session expired + Create chat + Create + Title diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index 1540e0a0..2551d953 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -11,10 +11,7 @@ import dev.meloda.fast.common.extensions.createTimerFlow import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue -import dev.meloda.fast.conversations.model.ConversationOption import dev.meloda.fast.conversations.model.ConversationsScreenState -import dev.meloda.fast.conversations.model.ConversationsShowOptions -import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.conversations.util.asPresentation import dev.meloda.fast.conversations.util.extractAvatar import dev.meloda.fast.data.State @@ -29,6 +26,9 @@ import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.network.VkErrorCode +import dev.meloda.fast.ui.model.api.ConversationOption +import dev.meloda.fast.ui.model.api.ConversationsShowOptions +import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -84,10 +84,7 @@ class ConversationsViewModelImpl( override val currentOffset = MutableStateFlow(0) override val canPaginate = MutableStateFlow(false) - // TODO: 22-Dec-24, Danil Nikolaev: rewrite - private val useContactNames = { - userSettings.useContactNames.value - } + private val useContactNames: Boolean get() = userSettings.useContactNames.value override fun onPaginationConditionsMet() { currentOffset.update { screenState.value.conversations.size } @@ -168,11 +165,11 @@ class ConversationsViewModelImpl( conversations = old.conversations.map { item -> item.copy( isExpanded = - if (item.id == conversation.id) { - !item.isExpanded - } else { - false - }, + if (item.id == conversation.id) { + !item.isExpanded + } else { + false + }, options = ImmutableList.copyOf(options) ) } @@ -191,7 +188,10 @@ class ConversationsViewModelImpl( onPinDialogDismissed() } - override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) { + override fun onOptionClicked( + conversation: UiConversation, + option: ConversationOption + ) { when (option) { ConversationOption.Delete -> { emitShowOptions { old -> @@ -322,21 +322,25 @@ class ConversationsViewModelImpl( } } } + State.Error.ConnectionError -> { baseError.setValue { BaseError.SimpleError(message = "Connection error") } } + State.Error.InternalError -> { baseError.setValue { BaseError.SimpleError(message = "Internal error") } } + State.Error.UnknownError -> { baseError.setValue { BaseError.SimpleError(message = "Unknown error") } } + else -> Unit } } @@ -360,7 +364,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -424,7 +428,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -475,7 +479,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -504,7 +508,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -534,7 +538,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -563,7 +567,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -609,7 +613,7 @@ class ConversationsViewModelImpl( old.copy(conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) }) } @@ -650,7 +654,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -701,7 +705,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) @@ -735,7 +739,7 @@ class ConversationsViewModelImpl( conversations = newConversations.map { it.asPresentation( resources = resources, - useContactName = useContactNames() + useContactName = useContactNames ) } ) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationOption.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationOption.kt deleted file mode 100644 index 451ffa1a..00000000 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationOption.kt +++ /dev/null @@ -1,31 +0,0 @@ -package dev.meloda.fast.conversations.model - -import dev.meloda.fast.common.model.UiImage -import dev.meloda.fast.common.model.UiText -import dev.meloda.fast.ui.R as UiR - -sealed class ConversationOption( - val title: UiText, - val icon: UiImage -) { - - data object MarkAsRead : ConversationOption( - title = UiText.Resource(UiR.string.action_mark_as_read), - icon = UiImage.Resource(UiR.drawable.round_done_all_24) - ) - - data object Pin : ConversationOption( - title = UiText.Resource(UiR.string.action_pin), - icon = UiImage.Resource(UiR.drawable.pin_outline_24) - ) - - data object Unpin : ConversationOption( - title = UiText.Resource(UiR.string.action_unpin), - icon = UiImage.Resource(UiR.drawable.pin_off_outline_24) - ) - - data object Delete : ConversationOption( - title = UiText.Resource(UiR.string.action_delete), - icon = UiImage.Resource(UiR.drawable.round_delete_outline_24) - ) -} diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt index 27277405..c8026d52 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt @@ -1,6 +1,8 @@ package dev.meloda.fast.conversations.model import androidx.compose.runtime.Immutable +import dev.meloda.fast.ui.model.api.ConversationsShowOptions +import dev.meloda.fast.ui.model.api.UiConversation @Immutable data class ConversationsScreenState( diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt index 66805ed8..0cf05c68 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt @@ -17,6 +17,7 @@ fun NavGraphBuilder.conversationsScreen( onError: (BaseError) -> Unit, onConversationItemClicked: (id: Int) -> Unit, onPhotoClicked: (url: String) -> Unit, + onCreateChatClicked: () -> Unit, navController: NavController, ) { composable { @@ -27,6 +28,7 @@ fun NavGraphBuilder.conversationsScreen( onError = onError, onConversationItemClicked = onConversationItemClicked, onConversationPhotoClicked = onPhotoClicked, + onCreateChatButtonClicked = onCreateChatClicked, viewModel = viewModel ) } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt index 9e270ff6..ed0751f0 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationItem.kt @@ -48,11 +48,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage -import dev.meloda.fast.conversations.model.ConversationOption -import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha import dev.meloda.fast.ui.components.DotsFlashing +import dev.meloda.fast.ui.model.api.ConversationOption +import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.util.getImage import dev.meloda.fast.ui.util.getResourcePainter import dev.meloda.fast.ui.util.getString @@ -256,7 +256,7 @@ fun ConversationItem( Row { if (conversation.interactionText != null) { Text( - text = conversation.interactionText, + text = conversation.interactionText.orEmpty(), color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.width(4.dp)) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt index 7b65c752..666827c6 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsList.kt @@ -22,10 +22,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import dev.meloda.fast.conversations.model.ConversationOption import dev.meloda.fast.conversations.model.ConversationsScreenState -import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.data.UserConfig +import dev.meloda.fast.ui.model.api.ConversationOption +import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.theme.LocalBottomPadding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index 0312b5d4..863fafef 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -2,7 +2,6 @@ package dev.meloda.fast.conversations.presentation import androidx.compose.animation.AnimatedVisibility 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 @@ -48,12 +47,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -62,29 +59,26 @@ 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.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.conversations.ConversationsViewModel -import dev.meloda.fast.conversations.model.ConversationOption import dev.meloda.fast.conversations.model.ConversationsScreenState -import dev.meloda.fast.conversations.model.UiConversation -import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.model.api.ConversationOption +import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.isScrollingUp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.launch import dev.meloda.fast.ui.R as UiR @Composable @@ -92,6 +86,7 @@ fun ConversationsRoute( onError: (BaseError) -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit, onConversationPhotoClicked: (url: String) -> Unit, + onCreateChatButtonClicked: () -> Unit, viewModel: ConversationsViewModel ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() @@ -113,6 +108,7 @@ fun ConversationsRoute( onRefreshDropdownItemClicked = viewModel::onRefresh, onRefresh = viewModel::onRefresh, onConversationPhotoClicked = onConversationPhotoClicked, + onCreateChatButtonClicked = onCreateChatButtonClicked, setScrollIndex = viewModel::setScrollIndex, setScrollOffset = viewModel::setScrollOffset ) @@ -132,7 +128,7 @@ fun ConversationsScreen( screenState: ConversationsScreenState = ConversationsScreenState.EMPTY, baseError: BaseError? = null, canPaginate: Boolean = false, - onSessionExpiredLogOutButtonClicked: () -> Unit, + onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onConversationItemClicked: (conversationId: Int) -> Unit = {}, onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {}, onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> }, @@ -140,6 +136,7 @@ fun ConversationsScreen( onRefreshDropdownItemClicked: () -> Unit = {}, onRefresh: () -> Unit = {}, onConversationPhotoClicked: (url: String) -> Unit = {}, + onCreateChatButtonClicked: () -> Unit = {}, setScrollIndex: (Int) -> Unit = {}, setScrollOffset: (Int) -> Unit = {} ) { @@ -284,37 +281,13 @@ fun ConversationsScreen( } }, floatingActionButton = { - val scope = rememberCoroutineScope() - val rotation = remember { Animatable(0f) } - Column { AnimatedVisibility( visible = listState.isScrollingUp(), enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)), exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200)) ) { - FloatingActionButton( - onClick = { - if (AppSettings.General.enableHaptic) { - 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) { - rotation.animateTo( - targetValue = -i.toFloat(), - animationSpec = tween(50) - ) - } - } - } - }, - modifier = Modifier.rotate(rotation.value) - ) { + FloatingActionButton(onClick = onCreateChatButtonClicked) { Icon( painter = painterResource(id = UiR.drawable.ic_baseline_create_24), contentDescription = "Add chat button" @@ -417,9 +390,7 @@ fun HandleDialogs( ) } - if (showOptions.showPinDialog != null) { - val conversation = showOptions.showPinDialog - + showOptions.showPinDialog?.let { conversation -> MaterialDialog( onDismissRequest = viewModel::onPinDialogDismissed, title = stringResource( diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/util/ConversationDomainMapper.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/util/ConversationDomainMapper.kt index cd267345..56634b9d 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/util/ConversationDomainMapper.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/util/ConversationDomainMapper.kt @@ -14,8 +14,6 @@ import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.parseString import dev.meloda.fast.common.util.TimeUtils -import dev.meloda.fast.conversations.model.ActionState -import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.model.InteractionType @@ -24,6 +22,8 @@ import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.ui.model.api.ActionState +import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.util.ImmutableList import java.util.Calendar import java.util.Locale diff --git a/feature/createchat/.gitignore b/feature/createchat/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/createchat/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/createchat/build.gradle.kts b/feature/createchat/build.gradle.kts new file mode 100644 index 00000000..23b4e5ab --- /dev/null +++ b/feature/createchat/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.fast.android.feature) + alias(libs.plugins.fast.android.library.compose) +} + +android { + namespace = "dev.meloda.fast.createchat" +} + +dependencies { + implementation(projects.core.common) + implementation(projects.core.domain) + implementation(projects.core.model) + implementation(projects.core.ui) + + implementation(libs.bundles.nanokt) + + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + + implementation(libs.coil.compose) + + implementation(libs.haze) + implementation(libs.haze.materials) + + implementation(libs.eithernet) + + implementation(libs.androidx.navigation.compose) + + implementation(libs.kotlin.serialization) +} diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/CreateChatViewModel.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/CreateChatViewModel.kt new file mode 100644 index 00000000..c9847dbe --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/CreateChatViewModel.kt @@ -0,0 +1,243 @@ +package dev.meloda.fast.conversations + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import coil.ImageLoader +import coil.request.ImageRequest +import dev.meloda.fast.common.extensions.listenValue +import dev.meloda.fast.common.extensions.setValue +import dev.meloda.fast.conversations.model.CreateChatScreenState +import dev.meloda.fast.data.State +import dev.meloda.fast.data.UserConfig +import dev.meloda.fast.data.processState +import dev.meloda.fast.datastore.UserSettings +import dev.meloda.fast.domain.FriendsUseCase +import dev.meloda.fast.domain.GetLocalUserByIdUseCase +import dev.meloda.fast.domain.MessagesUseCase +import dev.meloda.fast.domain.util.asPresentation +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.network.VkErrorCode +import dev.meloda.fast.ui.model.api.UiFriend +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface CreateChatViewModel { + + val screenState: StateFlow + val baseError: StateFlow + val currentOffset: StateFlow + val canPaginate: StateFlow + + val isChatCreated: StateFlow + + fun onPaginationConditionsMet() + fun onRefresh() + fun onErrorConsumed() + + fun toggleFriendSelection(userId: Int) + + fun onTitleTextInputChanged(newTitle: String) + + fun onCreateChatButtonClicked() + + fun onNavigatedBack() +} + +class CreateChatViewModelImpl( + private val friendsUseCase: FriendsUseCase, + private val messagesUseCase: MessagesUseCase, + private val imageLoader: ImageLoader, + private val applicationContext: Context, + private val getLocalUserByIdUseCase: GetLocalUserByIdUseCase, + private val userSettings: UserSettings +) : CreateChatViewModel, ViewModel() { + + override val screenState = MutableStateFlow(CreateChatScreenState.EMPTY) + override val baseError = MutableStateFlow(null) + override val currentOffset = MutableStateFlow(0) + override val canPaginate = MutableStateFlow(false) + + override val isChatCreated = MutableStateFlow(null) + + private val useContactNames: Boolean = userSettings.useContactNames.value + + init { + loadFriends() + } + + override fun onPaginationConditionsMet() { + currentOffset.update { screenState.value.friends.size } + loadFriends() + } + + override fun onRefresh() { + onErrorConsumed() + loadFriends(offset = 0) + } + + override fun onErrorConsumed() { + baseError.setValue { null } + } + + override fun toggleFriendSelection(userId: Int) { + val newSelectionList = screenState.value.selectedFriendsIds.toMutableList() + + if (newSelectionList.contains(userId)) { + newSelectionList.remove(userId) + } else { + newSelectionList.add(userId) + } + + screenState.setValue { old -> + old.copy(selectedFriendsIds = newSelectionList) + } + } + + override fun onTitleTextInputChanged(newTitle: String) { + screenState.setValue { old -> old.copy(chatTitle = newTitle) } + } + + override fun onCreateChatButtonClicked() { + createChat() + } + + override fun onNavigatedBack() { + viewModelScope.launch(Dispatchers.Main) { + isChatCreated.emit(null) + } + } + + private fun loadFriends( + offset: Int = currentOffset.value + ) { + friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset) + .listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { response -> + val itemsCountSufficient = response.size == LOAD_COUNT + canPaginate.setValue { itemsCountSufficient } + + val paginationExhausted = !itemsCountSufficient && + screenState.value.friends.isNotEmpty() + + val imagesToPreload = + response.mapNotNull { it.photo100.takeIf { !it.isNullOrEmpty() } } + + imagesToPreload.forEach { url -> + imageLoader.enqueue( + ImageRequest.Builder(applicationContext) + .data(url) + .build() + ) + } + + friendsUseCase.storeUsers(response) + + val loadedFriends = response.map { + it.asPresentation(useContactNames) + } + + val newState = screenState.value.copy( + isPaginationExhausted = paginationExhausted + ) + if (offset == 0) { + screenState.setValue { + newState.copy(friends = loadedFriends) + } + } else { + screenState.setValue { + newState.copy( + friends = newState.friends.plus(loadedFriends) + ) + } + } + } + ) + + screenState.setValue { old -> + old.copy( + isLoading = offset == 0 && state.isLoading(), + isPaginating = offset > 0 && state.isLoading() + ) + } + } + } + + private fun createChat() { + viewModelScope.launch { + val title = screenState.value.chatTitle.takeUnless(String::isBlank) + + val accountAsFriend = + getLocalUserByIdUseCase.proceed(UserConfig.userId)?.asPresentation(useContactNames) + + val accountList = accountAsFriend?.let(::listOf) ?: emptyList() + + val selectedFriends = screenState.value.selectedFriendsIds + .takeIf { it.isNotEmpty() } + ?.mapNotNull { userId -> screenState.value.friends.find { it.userId == userId } } + + messagesUseCase.createChat( + userIds = selectedFriends?.map { it.userId }, + title = title + ?: (accountList + selectedFriends.orEmpty()).joinToString(transform = UiFriend::firstName) + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { response -> + withContext(Dispatchers.Main) { + isChatCreated.emit(2_000_000_000 + response) + } + } + ) + } + } + } + + private fun handleError(error: State.Error) { + when (error) { + is State.Error.ApiError -> { + when (error.errorCode) { + VkErrorCode.USER_AUTHORIZATION_FAILED -> { + baseError.setValue { BaseError.SessionExpired } + } + + else -> { + baseError.setValue { + BaseError.SimpleError(message = error.errorMessage) + } + } + } + } + + State.Error.ConnectionError -> { + baseError.setValue { + BaseError.SimpleError(message = "Connection error") + } + } + + State.Error.InternalError -> { + baseError.setValue { + BaseError.SimpleError(message = "Internal error") + } + } + + State.Error.UnknownError -> { + baseError.setValue { + BaseError.SimpleError(message = "Unknown error") + } + } + + else -> Unit + } + } + + companion object { + const val LOAD_COUNT = 30 + } +} diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/di/CreateChatModule.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/di/CreateChatModule.kt new file mode 100644 index 00000000..2b9ec1b5 --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/di/CreateChatModule.kt @@ -0,0 +1,9 @@ +package dev.meloda.fast.conversations.di + +import dev.meloda.fast.conversations.CreateChatViewModelImpl +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val createChatModule = module { + viewModelOf(::CreateChatViewModelImpl) +} diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/model/CreateChatScreenState.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/model/CreateChatScreenState.kt new file mode 100644 index 00000000..03abef2c --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/model/CreateChatScreenState.kt @@ -0,0 +1,25 @@ +package dev.meloda.fast.conversations.model + +import androidx.compose.runtime.Immutable +import dev.meloda.fast.ui.model.api.UiFriend + +@Immutable +data class CreateChatScreenState( + val isLoading: Boolean, + val isPaginating: Boolean, + val isPaginationExhausted: Boolean, + val friends: List, + val selectedFriendsIds: List, + val chatTitle: String +) { + companion object { + val EMPTY: CreateChatScreenState = CreateChatScreenState( + isLoading = true, + isPaginating = false, + isPaginationExhausted = false, + friends = emptyList(), + selectedFriendsIds = emptyList(), + chatTitle = "" + ) + } +} diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/navigation/CreateChatNavigation.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/navigation/CreateChatNavigation.kt new file mode 100644 index 00000000..3a863420 --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/navigation/CreateChatNavigation.kt @@ -0,0 +1,36 @@ +package dev.meloda.fast.conversations.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import dev.meloda.fast.conversations.CreateChatViewModel +import dev.meloda.fast.conversations.CreateChatViewModelImpl +import dev.meloda.fast.conversations.presentation.CreateChatRoute +import dev.meloda.fast.ui.extensions.sharedViewModel +import kotlinx.serialization.Serializable + +@Serializable +object CreateChat + +fun NavGraphBuilder.createChatScreen( + onChatCreated: (Int) -> Unit, + navController: NavController, +) { + composable { + val viewModel: CreateChatViewModel = + it.sharedViewModel(navController = navController) + + CreateChatRoute( + onError = { + + }, + onBack = navController::popBackStack, + onChatCreated = onChatCreated, + viewModel = viewModel + ) + } +} + +fun NavController.navigateToCreateChat() { + this.navigate(CreateChat) +} diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatItem.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatItem.kt new file mode 100644 index 00000000..e5ff6457 --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatItem.kt @@ -0,0 +1,110 @@ +package dev.meloda.fast.conversations.presentation + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Checkbox +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.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.model.api.UiFriend + + +@Composable +fun CreateChatItem( + modifier: Modifier = Modifier, + friend: UiFriend, + maxLines: Int, + isSelected: Boolean, + onItemClicked: (Int) -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onItemClicked(friend.userId) } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(16.dp)) + + val friendAvatar = friend.avatar?.extractUrl() + + Box(modifier = Modifier.size(56.dp)) { + if (friendAvatar == null) { + Image( + modifier = Modifier + .fillMaxSize() + .clip(CircleShape), + painter = painterResource(id = R.drawable.ic_account_circle_cut), + contentDescription = "Avatar", + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) + ) + } else { + AsyncImage( + model = friendAvatar, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape), + placeholder = painterResource(id = R.drawable.ic_account_circle_cut) + ) + } + + if (friend.onlineStatus.isOnline()) { + Box( + modifier = Modifier + .clip(CircleShape) + .size(18.dp) + .background(MaterialTheme.colorScheme.background) + .padding(2.dp) + .align(Alignment.BottomEnd) + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .matchParentSize() + .background(MaterialTheme.colorScheme.primary) + ) + } + } + } + + Spacer(modifier = Modifier.width(16.dp)) + + Text( + text = friend.title, + minLines = 1, + maxLines = maxLines, + style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp), + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Checkbox( + checked = isSelected, + onCheckedChange = { onItemClicked(friend.userId) }, + ) + + Spacer(modifier = Modifier.width(16.dp)) + } +} + diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatList.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatList.kt new file mode 100644 index 00000000..ff2c00f2 --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatList.kt @@ -0,0 +1,101 @@ +package dev.meloda.fast.conversations.presentation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import dev.meloda.fast.conversations.model.CreateChatScreenState +import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.model.api.UiFriend +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun CreateChatList( + screenState: CreateChatScreenState, + state: LazyListState, + maxLines: Int, + modifier: Modifier, + padding: PaddingValues, + onItemClicked: (Int) -> Unit, + onTitleTextInputChanged: (String) -> Unit +) { + val coroutineScope = rememberCoroutineScope() + + LazyColumn( + modifier = modifier, + state = state + ) { + item { + Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + + } + items( + items = screenState.friends, + key = UiFriend::userId, + ) { friend -> + CreateChatItem( + maxLines = maxLines, + modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + friend = friend, + isSelected = screenState.selectedFriendsIds.contains(friend.userId), + onItemClicked = onItemClicked + ) + } + + item { + Column( + modifier = Modifier + .fillMaxWidth() + .animateItem(fadeInSpec = null, fadeOutSpec = null), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (screenState.isPaginating) { + CircularProgressIndicator() + } + + if (screenState.isPaginationExhausted) { + IconButton( + onClick = { + coroutineScope.launch(Dispatchers.Main) { + state.scrollToItem(14) + state.animateScrollToItem(0) + } + }, + colors = IconButtonDefaults.filledIconButtonColors() + ) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowUp, + contentDescription = null + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } + } + } +} diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt new file mode 100644 index 00000000..b2d331e4 --- /dev/null +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt @@ -0,0 +1,342 @@ +package dev.meloda.fast.conversations.presentation + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Done +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +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.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.conversations.CreateChatViewModel +import dev.meloda.fast.conversations.model.CreateChatScreenState +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.ui.components.ErrorView +import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.IconButton +import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.theme.LocalHazeState +import dev.meloda.fast.ui.theme.LocalThemeConfig +import dev.meloda.fast.ui.util.isScrollingUp +import dev.meloda.fast.ui.R as UiR + +@Composable +fun CreateChatRoute( + onError: (BaseError) -> Unit, + onBack: () -> Unit, + onChatCreated: (Int) -> Unit, + viewModel: CreateChatViewModel +) { + val context = LocalContext.current + + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + val isChatCreated by viewModel.isChatCreated.collectAsStateWithLifecycle() + + LaunchedEffect(isChatCreated) { + if (isChatCreated != null) { + onChatCreated(isChatCreated ?: -1) + viewModel.onNavigatedBack() + } + } + + CreateChatScreen( + screenState = screenState, + baseError = baseError, + canPaginate = canPaginate, + onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onBack = onBack, + onRefresh = viewModel::onRefresh, + onCreateChatButtonClicked = viewModel::onCreateChatButtonClicked, + onItemClicked = viewModel::toggleFriendSelection, + onTitleTextInputChanged = viewModel::onTitleTextInputChanged + ) +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class, +) +@Composable +fun CreateChatScreen( + screenState: CreateChatScreenState = CreateChatScreenState.EMPTY, + baseError: BaseError? = null, + canPaginate: Boolean = false, + onSessionExpiredLogOutButtonClicked: () -> Unit = {}, + onPaginationConditionsMet: () -> Unit = {}, + onBack: () -> Unit = {}, + onRefresh: () -> Unit = {}, + onCreateChatButtonClicked: () -> Unit = {}, + onItemClicked: (Int) -> Unit = {}, + onTitleTextInputChanged: (String) -> Unit = {} +) { + val currentTheme = LocalThemeConfig.current + + val maxLines by remember(currentTheme) { + mutableIntStateOf(if (currentTheme.enableMultiline) 2 else 1) + } + + val listState = rememberLazyListState() + + val paginationConditionMet by remember(canPaginate, listState) { + derivedStateOf { + canPaginate && + (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index + ?: -9) >= (listState.layoutInfo.totalItemsCount - 6) + } + } + + LaunchedEffect(paginationConditionMet) { + if (paginationConditionMet && !screenState.isPaginating) { + onPaginationConditionsMet() + } + } + + val hazeState = LocalHazeState.current + + val toolbarColorAlpha by animateFloatAsState( + targetValue = if (!listState.canScrollBackward) 1f else 0f, + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + val toolbarContainerColor by animateColorAsState( + targetValue = + if (currentTheme.enableBlur || !listState.canScrollBackward) + MaterialTheme.colorScheme.surface + else + MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + Scaffold( + modifier = Modifier.fillMaxSize(), + contentWindowInsets = WindowInsets.statusBars, + topBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + toolbarContainerColor.copy( + alpha = if (currentTheme.enableBlur) toolbarColorAlpha else 1f + ) + ) + .then( + if (currentTheme.enableBlur) { + Modifier.hazeEffect( + state = hazeState, + style = HazeMaterials.thick() + ) + } else Modifier + ) + ) { + TopAppBar( + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) + } + }, + title = { + Text( + text = stringResource( + id = if (screenState.isLoading) UiR.string.title_loading + else UiR.string.title_create_chat + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineSmall + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = toolbarContainerColor.copy( + alpha = 0f + ) + ), + modifier = Modifier.fillMaxWidth(), + ) + + var isTextFieldFocused by remember { + mutableStateOf(false) + } + + val borderWidth by animateDpAsState(if (isTextFieldFocused) 1.5.dp else 0.dp) + val borderColor by animateColorAsState( + if (isTextFieldFocused) MaterialTheme.colorScheme.primary + else Color.Transparent + ) + + TextField( + modifier = Modifier + .height(58.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp) + .border( + borderWidth, + borderColor, + RoundedCornerShape(16.dp) + ) + .clip(RoundedCornerShape(16.dp)) + .onFocusChanged { isTextFieldFocused = it.hasFocus }, + value = screenState.chatTitle, + onValueChange = onTitleTextInputChanged, + label = { Text(text = stringResource(UiR.string.create_chat_title)) }, + placeholder = { Text(text = stringResource(UiR.string.create_chat_title)) }, + singleLine = true, + colors = TextFieldDefaults.colors( + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ) + ) + Spacer(Modifier.height(16.dp)) + } + }, + floatingActionButton = { + if (baseError == null) { + Column( + modifier = Modifier + .imePadding() + .navigationBarsPadding() + ) { + ExtendedFloatingActionButton( + onClick = onCreateChatButtonClicked, + expanded = listState.isScrollingUp(), + text = { Text(text = stringResource(UiR.string.action_create)) }, + icon = { + Icon( + imageVector = Icons.Rounded.Done, + contentDescription = null + ) + } + ) + } + } + } + ) { padding -> + when { + baseError != null -> { + when (baseError) { + is BaseError.SessionExpired -> { + ErrorView( + text = stringResource(UiR.string.session_expired), + buttonText = stringResource(UiR.string.action_log_out), + onButtonClick = onSessionExpiredLogOutButtonClicked + ) + } + + is BaseError.SimpleError -> { + ErrorView( + text = baseError.message, + buttonText = stringResource(UiR.string.try_again), + onButtonClick = onRefresh + ) + } + } + } + + screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader() + + else -> { + val pullToRefreshState = rememberPullToRefreshState() + + PullToRefreshBox( + modifier = Modifier + .fillMaxSize() + .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) + .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + .padding(bottom = padding.calculateBottomPadding()), + state = pullToRefreshState, + isRefreshing = screenState.isLoading, + onRefresh = onRefresh, + indicator = { + PullToRefreshDefaults.Indicator( + state = pullToRefreshState, + isRefreshing = screenState.isLoading, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = padding.calculateTopPadding()), + ) + } + ) { + CreateChatList( + screenState = screenState, + state = listState, + maxLines = maxLines, + modifier = if (currentTheme.enableBlur) { + Modifier.hazeSource(state = hazeState) + } else { + Modifier + }.fillMaxSize(), + padding = padding, + onItemClicked = onItemClicked, + onTitleTextInputChanged = onTitleTextInputChanged + ) + + if (screenState.friends.isEmpty()) { + NoItemsView( + buttonText = stringResource(UiR.string.action_refresh), + onButtonClick = onRefresh + ) + } + } + } + } + } +} diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt index 0bac308f..bf953845 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt @@ -9,8 +9,8 @@ import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.FriendsUseCase import dev.meloda.fast.domain.LoadUsersByIdsUseCase +import dev.meloda.fast.domain.util.asPresentation import dev.meloda.fast.friends.model.FriendsScreenState -import dev.meloda.fast.friends.util.asPresentation import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.network.VkErrorCode diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt index 59d5ca9a..f2583c3a 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/model/FriendsScreenState.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.friends.model import androidx.compose.runtime.Immutable +import dev.meloda.fast.ui.model.api.UiFriend @Immutable data class FriendsScreenState( diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt index 376a8b68..78e40aff 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage -import dev.meloda.fast.friends.model.UiFriend import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.model.api.UiFriend @Composable fun FriendItem( diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt index 752e8997..c63075ee 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt @@ -22,8 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dev.meloda.fast.friends.model.FriendsScreenState -import dev.meloda.fast.friends.model.UiFriend -import dev.meloda.fast.ui.theme.LocalBottomPadding +import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -49,8 +48,6 @@ fun FriendsList( val friends = uiFriends.toList() - val bottomPadding = LocalBottomPadding.current - LazyColumn( modifier = modifier, state = listState diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index c0f542d5..24596d07 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -29,6 +29,7 @@ import dev.meloda.fast.messageshistory.navigation.MessagesHistory import dev.meloda.fast.messageshistory.util.asPresentation import dev.meloda.fast.messageshistory.util.extractAvatar import dev.meloda.fast.messageshistory.util.extractTitle +import dev.meloda.fast.messageshistory.util.findMessageById import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.api.domain.VkAttachment @@ -160,7 +161,9 @@ class MessagesHistoryViewModelImpl( val message = event.message Log.d("MessagesHistoryViewModel", "handleNewMessage: $message") + if (message.peerId != screenState.value.conversationId) return + if (screenState.value.messages.findMessageById(message.id) != null) return val randomIds = messages.value.map(VkMessage::randomId) if (message.randomId != 0 && message.randomId in randomIds) return diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 36a9f46c..a52702c2 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -282,6 +282,7 @@ fun MessagesHistoryScreen( // TODO: 11/07/2024, Danil Nikolaev: to VM + // TODO: 23-Mar-25, Danil Nikolaev: crash if not messages (ex. new chat) onChatMaterialsDropdownItemClicked( screenState.conversationId, screenState.messages.firstMessage().conversationMessageId diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt index de916b08..36de0c13 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/Ext.kt @@ -4,11 +4,13 @@ import dev.meloda.fast.messageshistory.model.UiItem fun List.firstMessage(): UiItem.Message = filterIsInstance().first() +fun List.firstMessageOrNull(): UiItem.Message? = filterIsInstance().firstOrNull() + fun List.indexOfMessageById(messageId: Int): Int = indexOfFirst { it.id == messageId } -fun List.findMessageById(messageId: Int): UiItem.Message = - first { it.id == messageId } as UiItem.Message +fun List.findMessageById(messageId: Int): UiItem.Message? = + firstOrNull { it.id == messageId } as UiItem.Message? fun List.indexOfMessageByCmId(cmId: Int): Int = indexOfFirst { it.cmId == cmId } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f5298d3..affd20d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,13 +19,16 @@ dependencyResolutionManagement { rootProject.name = "fast-messenger" include(":app") + include(":core:network") include(":core:data") include(":core:database") include(":core:datastore") include(":core:ui") include(":core:common") +include(":core:domain") include(":core:model") + include(":feature:messageshistory") include(":feature:conversations") include(":feature:auth") @@ -35,4 +38,5 @@ include(":feature:photoviewer") include(":feature:settings") include(":feature:friends") include(":feature:profile") -include(":core:domain") +include(":feature:createchat") +include(":core:presentation")